Separate ActiveAndroid from models

This commit is contained in:
2016-06-10 13:30:33 -04:00
parent 18e8390aed
commit 78d4f86cab
152 changed files with 6494 additions and 4060 deletions

View File

@@ -23,6 +23,9 @@ import javax.inject.Singleton;
import dagger.Component;
/**
* Dependency injection component for classes that are specific to Android.
*/
@Singleton
@Component(modules = {AndroidModule.class})
public interface AndroidComponent extends BaseComponent

View File

@@ -20,6 +20,10 @@
package org.isoron.uhabits;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ModelFactory;
import org.isoron.uhabits.models.sqlite.SQLModelFactory;
import org.isoron.uhabits.models.sqlite.SQLiteHabitList;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache;
import org.isoron.uhabits.utils.Preferences;
@@ -28,16 +32,15 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
/**
* Module that provides dependencies when the application is running on
* Android.
* <p>
* This module is also used for instrumented tests.
*/
@Module
public class AndroidModule
{
@Provides
@Singleton
Preferences providePreferences()
{
return new Preferences();
}
@Provides
@Singleton
CommandRunner provideCommandRunner()
@@ -51,4 +54,24 @@ public class AndroidModule
{
return new HabitCardListCache();
}
@Provides
@Singleton
HabitList provideHabitList()
{
return SQLiteHabitList.getInstance();
}
@Provides
ModelFactory provideModelFactory()
{
return new SQLModelFactory();
}
@Provides
@Singleton
Preferences providePreferences()
{
return new Preferences();
}
}

View File

@@ -19,16 +19,34 @@
package org.isoron.uhabits;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.CreateHabitCommand;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.io.AbstractImporter;
import org.isoron.uhabits.io.HabitsCSVExporter;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ToggleRepetitionTask;
import org.isoron.uhabits.ui.BaseSystem;
import org.isoron.uhabits.ui.habits.edit.BaseDialogFragment;
import org.isoron.uhabits.ui.habits.edit.HistoryEditorDialog;
import org.isoron.uhabits.ui.habits.list.ListHabitsActivity;
import org.isoron.uhabits.ui.habits.list.ListHabitsController;
import org.isoron.uhabits.ui.habits.list.ListHabitsSelectionMenu;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache;
import org.isoron.uhabits.ui.habits.list.ListHabitsController;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.ui.habits.list.model.HintList;
import org.isoron.uhabits.ui.habits.list.views.CheckmarkPanelView;
import org.isoron.uhabits.ui.habits.show.ShowHabitActivity;
import org.isoron.uhabits.widgets.BaseWidgetProvider;
import org.isoron.uhabits.widgets.HabitPickerDialog;
/**
* Base component for dependency injection.
*/
public interface BaseComponent
{
void inject(CheckmarkButtonController checkmarkButtonController);
@@ -50,4 +68,36 @@ public interface BaseComponent
void inject(HintList hintList);
void inject(HabitCardListAdapter habitCardListAdapter);
void inject(ArchiveHabitsCommand archiveHabitsCommand);
void inject(ChangeHabitColorCommand changeHabitColorCommand);
void inject(UnarchiveHabitsCommand unarchiveHabitsCommand);
void inject(EditHabitCommand editHabitCommand);
void inject(CreateHabitCommand createHabitCommand);
void inject(HabitPickerDialog habitPickerDialog);
void inject(BaseWidgetProvider baseWidgetProvider);
void inject(ShowHabitActivity showHabitActivity);
void inject(DeleteHabitsCommand deleteHabitsCommand);
void inject(ListHabitsActivity listHabitsActivity);
void inject(BaseSystem baseSystem);
void inject(HistoryEditorDialog historyEditorDialog);
void inject(HabitsApplication application);
void inject(Habit habit);
void inject(AbstractImporter abstractImporter);
void inject(HabitsCSVExporter habitsCSVExporter);
}

View File

@@ -40,6 +40,7 @@ import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.ui.habits.show.ShowHabitActivity;
import org.isoron.uhabits.utils.DateUtils;
@@ -50,12 +51,26 @@ import java.util.Date;
import javax.inject.Inject;
/**
* The Android BroadacastReceiver for Loop Habit Tracker.
* <p>
* Currently, all broadcast messages are received and processed by this class.
*/
public class HabitBroadcastReceiver extends BroadcastReceiver
{
public static final String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK";
public static final String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS";
public static final String ACTION_SHOW_REMINDER = "org.isoron.uhabits.ACTION_SHOW_REMINDER";
public static final String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE";
public static final String ACTION_DISMISS =
"org.isoron.uhabits.ACTION_DISMISS";
public static final String ACTION_SHOW_REMINDER =
"org.isoron.uhabits.ACTION_SHOW_REMINDER";
public static final String ACTION_SNOOZE =
"org.isoron.uhabits.ACTION_SNOOZE";
@Inject
HabitList habitList;
@Inject
CommandRunner commandRunner;
@@ -66,6 +81,68 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
HabitsApplication.getComponent().inject(this);
}
public static PendingIntent buildCheckIntent(Context context,
Habit habit,
Long timestamp)
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class);
checkIntent.setData(data);
checkIntent.setAction(ACTION_CHECK);
if (timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, 0, checkIntent,
PendingIntent.FLAG_ONE_SHOT);
}
public static PendingIntent buildDismissIntent(Context context)
{
Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class);
deleteIntent.setAction(ACTION_DISMISS);
return PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
}
public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
{
Uri data = habit.getUri();
Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class);
snoozeIntent.setData(data);
snoozeIntent.setAction(ACTION_SNOOZE);
return PendingIntent.getBroadcast(context, 0, snoozeIntent, 0);
}
public static PendingIntent buildViewHabitIntent(Context context,
Habit habit)
{
Intent intent = new Intent(context, ShowHabitActivity.class);
intent.setData(
Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
return TaskStackBuilder
.create(context.getApplicationContext())
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
public static void dismissNotification(Context context, Habit habit)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
}
public static void sendRefreshBroadcast(Context context)
{
LocalBroadcastManager manager =
LocalBroadcastManager.getInstance(context);
Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH);
manager.sendBroadcast(refreshIntent);
WidgetManager.updateWidgets(context);
}
@Override
public void onReceive(final Context context, Intent intent)
{
@@ -89,40 +166,23 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
break;
case Intent.ACTION_BOOT_COMPLETED:
ReminderUtils.createReminderAlarms(context);
ReminderUtils.createReminderAlarms(context, habitList);
break;
}
}
private void createReminderAlarmsDelayed(final Context context)
{
new Handler().postDelayed(() -> ReminderUtils.createReminderAlarms(context), 5000);
}
private void snoozeHabit(Context context, Intent intent)
{
Uri data = intent.getData();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long delayMinutes = Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
long habitId = ContentUris.parseId(data);
Habit habit = Habit.get(habitId);
if(habit != null)
ReminderUtils.createReminderAlarm(context, habit,
new Date().getTime() + delayMinutes * 60 * 1000);
dismissNotification(context, habitId);
}
private void checkHabit(Context context, Intent intent)
{
Uri data = intent.getData();
Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
long habitId = ContentUris.parseId(data);
Habit habit = Habit.get(habitId);
if(habit != null)
Habit habit = habitList.getById(habitId);
if (habit != null)
{
ToggleRepetitionCommand command = new ToggleRepetitionCommand(habit, timestamp);
ToggleRepetitionCommand command =
new ToggleRepetitionCommand(habit, timestamp);
commandRunner.execute(command, habitId);
}
@@ -130,36 +190,26 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
sendRefreshBroadcast(context);
}
public static void sendRefreshBroadcast(Context context)
private boolean checkWeekday(Intent intent, Habit habit)
{
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH);
manager.sendBroadcast(refreshIntent);
Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
WidgetManager.updateWidgets(context);
boolean reminderDays[] =
DateUtils.unpackWeekdayList(habit.getReminderDays());
int weekday = DateUtils.getWeekday(timestamp);
return reminderDays[weekday];
}
private void dismissAllHabits()
{
}
private void dismissNotification(Context context, Long habitId)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habitId % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
}
private void createNotification(final Context context, final Intent intent)
{
final Uri data = intent.getData();
final Habit habit = Habit.get(ContentUris.parseId(data));
final Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
final Long reminderTime = intent.getLongExtra("reminderTime", DateUtils.getStartOfToday());
final Habit habit = habitList.getById(ContentUris.parseId(data));
final Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
final Long reminderTime =
intent.getLongExtra("reminderTime", DateUtils.getStartOfToday());
if (habit == null) return;
@@ -170,7 +220,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
@Override
protected void doInBackground()
{
todayValue = habit.checkmarks.getTodayValue();
todayValue = habit.getCheckmarks().getTodayValue();
}
@Override
@@ -183,41 +233,46 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
Intent contentIntent = new Intent(context, MainActivity.class);
contentIntent.setData(data);
PendingIntent contentPendingIntent =
PendingIntent.getActivity(context, 0, contentIntent, 0);
PendingIntent.getActivity(context, 0, contentIntent, 0);
PendingIntent dismissPendingIntent = buildDismissIntent(context);
PendingIntent checkIntentPending = buildCheckIntent(context, habit, timestamp);
PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit);
PendingIntent dismissPendingIntent =
buildDismissIntent(context);
PendingIntent checkIntentPending =
buildCheckIntent(context, habit, timestamp);
PendingIntent snoozeIntentPending =
buildSnoozeIntent(context, habit);
Uri ringtoneUri = ReminderUtils.getRingtoneUri(context);
NotificationCompat.WearableExtender wearableExtender =
new NotificationCompat.WearableExtender().setBackground(
BitmapFactory.decodeResource(context.getResources(),
R.drawable.stripe));
new NotificationCompat.WearableExtender().setBackground(
BitmapFactory.decodeResource(context.getResources(),
R.drawable.stripe));
Notification notification =
new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.name)
.setContentText(habit.description)
.setContentIntent(contentPendingIntent)
.setDeleteIntent(dismissPendingIntent)
.addAction(R.drawable.ic_action_check,
context.getString(R.string.check), checkIntentPending)
.addAction(R.drawable.ic_action_snooze,
context.getString(R.string.snooze), snoozeIntentPending)
.setSound(ringtoneUri)
.extend(wearableExtender)
.setWhen(reminderTime)
.setShowWhen(true)
.build();
new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.getName())
.setContentText(habit.getDescription())
.setContentIntent(contentPendingIntent)
.setDeleteIntent(dismissPendingIntent)
.addAction(R.drawable.ic_action_check,
context.getString(R.string.check),
checkIntentPending)
.addAction(R.drawable.ic_action_snooze,
context.getString(R.string.snooze),
snoozeIntentPending)
.setSound(ringtoneUri)
.extend(wearableExtender)
.setWhen(reminderTime)
.setShowWhen(true)
.build();
notification.flags |= Notification.FLAG_AUTO_CANCEL;
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.notify(notificationId, notification);
@@ -227,59 +282,39 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
}.execute();
}
public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
private void createReminderAlarmsDelayed(final Context context)
{
Uri data = habit.getUri();
Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class);
snoozeIntent.setData(data);
snoozeIntent.setAction(ACTION_SNOOZE);
return PendingIntent.getBroadcast(context, 0, snoozeIntent, 0);
new Handler().postDelayed(
() -> ReminderUtils.createReminderAlarms(context, habitList), 5000);
}
public static PendingIntent buildCheckIntent(Context context, Habit habit, Long timestamp)
private void dismissAllHabits()
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class);
checkIntent.setData(data);
checkIntent.setAction(ACTION_CHECK);
if(timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, 0, checkIntent, PendingIntent.FLAG_ONE_SHOT);
}
public static PendingIntent buildDismissIntent(Context context)
{
Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class);
deleteIntent.setAction(ACTION_DISMISS);
return PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
}
public static PendingIntent buildViewHabitIntent(Context context, Habit habit)
{
Intent intent = new Intent(context, ShowHabitActivity.class);
intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
return TaskStackBuilder.create(context.getApplicationContext())
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
private boolean checkWeekday(Intent intent, Habit habit)
{
Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
boolean reminderDays[] = DateUtils.unpackWeekdayList(habit.reminderDays);
int weekday = DateUtils.getWeekday(timestamp);
return reminderDays[weekday];
}
public static void dismissNotification(Context context, Habit habit)
private void dismissNotification(Context context, Long habitId)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
int notificationId = (int) (habitId % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
}
private void snoozeHabit(Context context, Intent intent)
{
Uri data = intent.getData();
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
long delayMinutes =
Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
long habitId = ContentUris.parseId(data);
Habit habit = habitList.getById(habitId);
if (habit != null) ReminderUtils.createReminderAlarm(context, habit,
new Date().getTime() + delayMinutes * 60 * 1000);
dismissNotification(context, habitId);
}
}

View File

@@ -25,37 +25,53 @@ import android.support.annotation.Nullable;
import com.activeandroid.ActiveAndroid;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.utils.DatabaseUtils;
import java.io.File;
import javax.inject.Inject;
/**
* The Android application for Loop Habit Tracker.
*/
public class HabitsApplication extends Application
{
public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH";
public static final int RESULT_IMPORT_DATA = 1;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
public static final String ACTION_REFRESH =
"org.isoron.uhabits.ACTION_REFRESH";
public static final int RESULT_BUG_REPORT = 4;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
public static final int RESULT_IMPORT_DATA = 1;
@Nullable
private static HabitsApplication application;
@Nullable
private static Context context;
private static BaseComponent component;
public static boolean isTestMode()
@Nullable
private static Context context;
@Inject
HabitList habitList;
public static BaseComponent getComponent()
{
try
{
if(context != null)
context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest");
return true;
}
catch (final Exception e)
{
return false;
}
return component;
}
public HabitList getHabitList()
{
return habitList;
}
public static void setComponent(BaseComponent component)
{
HabitsApplication.component = component;
}
@Nullable
@@ -70,14 +86,19 @@ public class HabitsApplication extends Application
return application;
}
public static BaseComponent getComponent()
public static boolean isTestMode()
{
return component;
}
public static void setComponent(BaseComponent component)
{
HabitsApplication.component = component;
try
{
if (context != null) context
.getClassLoader()
.loadClass("org.isoron.uhabits.unit.models.HabitTest");
return true;
}
catch (final Exception e)
{
return false;
}
}
@Override
@@ -91,9 +112,10 @@ public class HabitsApplication extends Application
if (isTestMode())
{
File db = DatabaseUtils.getDatabaseFile();
if(db.exists()) db.delete();
if (db.exists()) db.delete();
}
component.inject(this);
DatabaseUtils.initializeActiveAndroid();
}

View File

@@ -23,6 +23,9 @@ import android.app.backup.BackupAgentHelper;
import android.app.backup.FileBackupHelper;
import android.app.backup.SharedPreferencesBackupHelper;
/**
* An Android BackupAgentHelper customized for this application.
*/
public class HabitsBackupAgent extends BackupAgentHelper
{
@Override

View File

@@ -21,6 +21,9 @@ package org.isoron.uhabits;
import org.isoron.uhabits.ui.habits.list.ListHabitsActivity;
/**
* Application that starts upon clicking the launcher icon.
*/
public class MainActivity extends ListHabitsActivity
{
/*

View File

@@ -19,38 +19,52 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to archive a list of habits.
*/
public class ArchiveHabitsCommand extends Command
{
@Inject
HabitList habitList;
private List<Habit> habits;
public ArchiveHabitsCommand(List<Habit> habits)
{
HabitsApplication.getComponent().inject(this);
this.habits = habits;
}
@Override
public void execute()
{
Habit.archive(habits);
for(Habit h : habits) h.setArchived(1);
habitList.update(habits);
}
@Override
public void undo()
{
Habit.unarchive(habits);
for(Habit h : habits) h.setArchived(0);
habitList.update(habits);
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_archived;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_unarchived;

View File

@@ -19,60 +19,65 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to change the color of a list of habits.
*/
public class ChangeHabitColorCommand extends Command
{
@Inject
HabitList habitList;
List<Habit> habits;
List<Integer> originalColors;
Integer newColor;
public ChangeHabitColorCommand(List<Habit> habits, Integer newColor)
{
HabitsApplication.getComponent().inject(this);
this.habits = habits;
this.newColor = newColor;
this.originalColors = new ArrayList<>(habits.size());
for(Habit h : habits)
originalColors.add(h.color);
for (Habit h : habits) originalColors.add(h.getColor());
}
@Override
public void execute()
{
Habit.setColor(habits, newColor);
for(Habit h : habits) h.setColor(newColor);
habitList.update(habits);
}
@Override
public void undo()
{
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
{
@Override
public void execute()
{
int k = 0;
for(Habit h : habits)
{
h.color = originalColors.get(k++);
h.save();
}
}
});
}
public Integer getExecuteStringId()
{
return R.string.toast_habit_changed;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_changed;
}
@Override
public void undo()
{
int k = 0;
for (Habit h : habits) h.setColor(originalColors.get(k++));
habitList.update(habits);
}
}

View File

@@ -19,12 +19,19 @@
package org.isoron.uhabits.commands;
/**
* A Command represents a desired set of changes that should be performed on the
* models.
* <p>
* A command can be executed and undone. Each of these operations also provide
* an string that should be displayed to the user upon their completion.
* <p>
* In general, commands should always be executed by a {@link CommandRunner}.
*/
public abstract class Command
{
public abstract void execute();
public abstract void undo();
public Integer getExecuteStringId()
{
return null;
@@ -34,4 +41,6 @@ public abstract class Command
{
return null;
}
public abstract void undo();
}

View File

@@ -26,6 +26,12 @@ import org.isoron.uhabits.tasks.BaseTask;
import java.util.LinkedList;
/**
* A CommandRunner executes and undoes commands.
* <p>
* CommandRunners also allows objects to subscribe to it, and receive events
* whenever a command is performed.
*/
public class CommandRunner
{
private LinkedList<Listener> listeners;
@@ -71,6 +77,10 @@ public class CommandRunner
listeners.remove(l);
}
/**
* Interface implemented by objects that want to receive an event whenever a
* command is executed.
*/
public interface Listener
{
void onCommandExecuted(@NonNull Command command,

View File

@@ -19,41 +19,48 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import javax.inject.Inject;
/**
* Command to create a habit.
*/
public class CreateHabitCommand extends Command
{
@Inject
HabitList habitList;
private Habit model;
private Long savedId;
public CreateHabitCommand(Habit model)
{
this.model = model;
HabitsApplication.getComponent().inject(this);
}
@Override
public void execute()
{
Habit savedHabit = new Habit(model);
if (savedId == null)
{
savedHabit.save();
savedId = savedHabit.getId();
}
else
{
savedHabit.save(savedId);
}
Habit savedHabit = new Habit();
savedHabit.copyFrom(model);
savedHabit.setId(savedId);
habitList.add(savedHabit);
savedId = savedHabit.getId();
}
@Override
public void undo()
{
Habit habit = Habit.get(savedId);
Habit habit = habitList.getById(savedId);
if(habit == null) throw new RuntimeException("Habit not found");
habit.cascadeDelete();
habitList.remove(habit);
}
@Override

View File

@@ -19,27 +19,36 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to delete a list of habits.
*/
public class DeleteHabitsCommand extends Command
{
@Inject
HabitList habitList;
private List<Habit> habits;
public DeleteHabitsCommand(List<Habit> habits)
{
this.habits = habits;
HabitsApplication.getComponent().inject(this);
}
@Override
public void execute()
{
for(Habit h : habits)
h.cascadeDelete();
Habit.rebuildOrder();
habitList.remove(h);
}
@Override
@@ -48,11 +57,13 @@ public class DeleteHabitsCommand extends Command
throw new UnsupportedOperationException();
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_deleted;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_restored;

View File

@@ -19,24 +19,43 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import javax.inject.Inject;
/**
* Command to modify a habit.
*/
public class EditHabitCommand extends Command
{
@Inject
HabitList habitList;
private Habit original;
private Habit modified;
private long savedId;
private boolean hasIntervalChanged;
public EditHabitCommand(Habit original, Habit modified)
{
this.savedId = original.getId();
this.modified = new Habit(modified);
this.original = new Habit(original);
HabitsApplication.getComponent().inject(this);
hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) ||
!this.original.freqNum.equals(this.modified.freqNum));
this.savedId = original.getId();
this.modified = new Habit();
this.original = new Habit();
this.modified.copyFrom(modified);
this.original.copyFrom(original);
hasIntervalChanged =
(!this.original.getFreqDen().equals(this.modified.getFreqDen()) ||
!this.original.getFreqNum().equals(this.modified.getFreqNum()));
}
@Override
@@ -45,6 +64,18 @@ public class EditHabitCommand extends Command
copyAttributes(this.modified);
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_changed;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_changed_back;
}
@Override
public void undo()
{
@@ -53,11 +84,11 @@ public class EditHabitCommand extends Command
private void copyAttributes(Habit model)
{
Habit habit = Habit.get(savedId);
if(habit == null) throw new RuntimeException("Habit not found");
Habit habit = habitList.getById(savedId);
if (habit == null) throw new RuntimeException("Habit not found");
habit.copyAttributes(model);
habit.save();
habit.copyFrom(model);
habitList.update(habit);
invalidateIfNeeded(habit);
}
@@ -66,19 +97,9 @@ public class EditHabitCommand extends Command
{
if (hasIntervalChanged)
{
habit.checkmarks.deleteNewerThan(0);
habit.streaks.deleteNewerThan(0);
habit.scores.invalidateNewerThan(0);
habit.getCheckmarks().invalidateNewerThan(0);
habit.getStreaks().invalidateNewerThan(0);
habit.getScores().invalidateNewerThan(0);
}
}
public Integer getExecuteStringId()
{
return R.string.toast_habit_changed;
}
public Integer getUndoStringId()
{
return R.string.toast_habit_changed_back;
}
}

View File

@@ -21,6 +21,9 @@ package org.isoron.uhabits.commands;
import org.isoron.uhabits.models.Habit;
/**
* Command to toggle a repetition.
*/
public class ToggleRepetitionCommand extends Command
{
private Long offset;
@@ -35,7 +38,7 @@ public class ToggleRepetitionCommand extends Command
@Override
public void execute()
{
habit.repetitions.toggle(offset);
habit.getRepetitions().toggleTimestamp(offset);
}
@Override

View File

@@ -19,38 +19,52 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to unarchive a list of habits.
*/
public class UnarchiveHabitsCommand extends Command
{
@Inject
HabitList habitList;
private List<Habit> habits;
public UnarchiveHabitsCommand(List<Habit> habits)
{
this.habits = habits;
HabitsApplication.getComponent().inject(this);
}
@Override
public void execute()
{
Habit.unarchive(habits);
for(Habit h : habits) h.setArchived(0);
habitList.update(habits);
}
@Override
public void undo()
{
Habit.archive(habits);
for(Habit h : habits) h.setArchived(1);
habitList.update(habits);
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_unarchived;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_archived;

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides commands to modify the models, such as {@link
* org.isoron.uhabits.commands.CreateHabitCommand}.
*/
package org.isoron.uhabits.commands;

View File

@@ -21,13 +21,30 @@ package org.isoron.uhabits.io;
import android.support.annotation.NonNull;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.HabitList;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import javax.inject.Inject;
/**
* AbstractImporter is the base class for all classes that import data from
* files into the app.
*/
public abstract class AbstractImporter
{
@Inject
HabitList habitList;
public AbstractImporter()
{
HabitsApplication.getComponent().inject(this);
}
public abstract boolean canHandle(@NonNull File file) throws IOException;
public abstract void importHabitsFromFile(@NonNull File file) throws IOException;

View File

@@ -26,6 +26,10 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* A GenericImporter decides which implementation of AbstractImporter is able to
* handle a given file and delegates to it the task of importing the data.
*/
public class GenericImporter extends AbstractImporter
{
List<AbstractImporter> importers;
@@ -42,8 +46,8 @@ public class GenericImporter extends AbstractImporter
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file)) return true;
for (AbstractImporter importer : importers)
if (importer.canHandle(file)) return true;
return false;
}
@@ -51,8 +55,7 @@ public class GenericImporter extends AbstractImporter
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file))
importer.importHabitsFromFile(file);
for (AbstractImporter importer : importers)
if (importer.canHandle(file)) importer.importHabitsFromFile(file);
}
}

View File

@@ -24,8 +24,8 @@ import android.support.annotation.NonNull;
import com.activeandroid.ActiveAndroid;
import com.opencsv.CSVReader;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
import java.io.BufferedReader;
import java.io.File;
@@ -34,6 +34,9 @@ import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
/**
* Class that imports data from HabitBull CSV files.
*/
public class HabitBullCSVImporter extends AbstractImporter
{
@Override
@@ -89,16 +92,16 @@ public class HabitBullCSVImporter extends AbstractImporter
if(h == null)
{
h = new Habit();
h.name = name;
h.description = description;
h.freqNum = h.freqDen = 1;
h.save();
h.setName(name);
h.setDescription(description);
h.setFreqDen(1);
h.setFreqNum(1);
habitList.add(h);
habits.put(name, h);
}
if(!h.repetitions.contains(timestamp))
h.repetitions.toggle(timestamp);
if(!h.getRepetitions().containsTimestamp(timestamp))
h.getRepetitions().toggleTimestamp(timestamp);
}
}
}

View File

@@ -21,8 +21,10 @@ package org.isoron.uhabits.io;
import android.support.annotation.NonNull;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.utils.DateUtils;
@@ -37,6 +39,11 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.inject.Inject;
/**
* Class that exports the application data to CSV files.
*/
public class HabitsCSVExporter
{
private List<Habit> habits;
@@ -46,8 +53,13 @@ public class HabitsCSVExporter
private String exportDirName;
@Inject
HabitList habitList;
public HabitsCSVExporter(List<Habit> habits, File dir)
{
HabitsApplication.getComponent().inject(this);
this.habits = habits;
this.exportDirName = dir.getAbsolutePath() + "/";
@@ -61,20 +73,20 @@ public class HabitsCSVExporter
new File(exportDirName).mkdirs();
FileWriter out = new FileWriter(exportDirName + filename);
generateFilenames.add(filename);
Habit.writeCSV(habits, out);
habitList.writeCSV(out);
out.close();
for(Habit h : habits)
{
String sane = sanitizeFilename(h.name);
String habitDirName = String.format("%03d %s", h.position + 1, sane);
String sane = sanitizeFilename(h.getName());
String habitDirName = String.format("%03d %s", habitList.indexOf(h) + 1, sane);
habitDirName = habitDirName.trim() + "/";
new File(exportDirName + habitDirName).mkdirs();
generateDirs.add(habitDirName);
writeScores(habitDirName, h.scores);
writeCheckmarks(habitDirName, h.checkmarks);
writeScores(habitDirName, h.getScores());
writeCheckmarks(habitDirName, h.getCheckmarks());
}
}

View File

@@ -31,18 +31,22 @@ import org.isoron.uhabits.utils.FileUtils;
import java.io.File;
import java.io.IOException;
/**
* Class that imports data from database files exported by Loop Habit Tracker.
*/
public class LoopDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
if (!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"Checkmarks", "Repetitions"});
Cursor c = db.rawQuery(
"select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"Checkmarks", "Repetitions"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);

View File

@@ -23,14 +23,17 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
/**
* Class that imports database files exported by Rewire.
*/
public class RewireDBImporter extends AbstractImporter
{
@Override
@@ -57,7 +60,7 @@ public class RewireDBImporter extends AbstractImporter
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Callback()
{
@Override
public void execute()
@@ -91,30 +94,30 @@ public class RewireDBImporter extends AbstractImporter
int periodIndex = c.getInt(7);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
habit.setName(name);
habit.setDescription(description);
int periods[] = { 7, 31, 365 };
switch (schedule)
{
case 0:
habit.freqNum = activeDays.split(",").length;
habit.freqDen = 7;
habit.setFreqNum(activeDays.split(",").length);
habit.setFreqDen(7);
break;
case 1:
habit.freqNum = days;
habit.freqDen = periods[periodIndex];
habit.setFreqNum(days);
habit.setFreqDen(periods[periodIndex]);
break;
case 2:
habit.freqNum = 1;
habit.freqDen = repeatingCount;
habit.setFreqNum(1);
habit.setFreqDen(repeatingCount);
break;
}
habit.save();
habitList.add(habit);
createReminder(db, habit, id);
createCheckmarks(db, habit, id);
@@ -150,10 +153,10 @@ public class RewireDBImporter extends AbstractImporter
reminderDays[idx] = true;
}
habit.reminderDays = DateUtils.packWeekdayList(reminderDays);
habit.reminderHour = rewireReminder / 60;
habit.reminderMin = rewireReminder % 60;
habit.save();
habit.setReminderDays(DateUtils.packWeekdayList(reminderDays));
habit.setReminderHour(rewireReminder / 60);
habit.setReminderMin(rewireReminder % 60);
habitList.update(habit);
}
finally
{
@@ -161,7 +164,8 @@ public class RewireDBImporter extends AbstractImporter
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int rewireHabitId)
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull
Habit habit, int rewireHabitId)
{
Cursor c = null;
@@ -181,7 +185,7 @@ public class RewireDBImporter extends AbstractImporter
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, month - 1, day);
habit.repetitions.toggle(cal.getTimeInMillis());
habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis());
}
while (c.moveToNext());
}

View File

@@ -23,26 +23,30 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
/**
* Class that imports data from database files exported by Tickmate.
*/
public class TickmateDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
if (!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"tracks", "track2groups"});
Cursor c = db.rawQuery(
"select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"tracks", "track2groups"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
@@ -54,62 +58,26 @@ public class TickmateDBImporter extends AbstractImporter
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
final SQLiteDatabase db =
SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
{
@Override
public void execute()
{
createHabits(db);
}
});
DatabaseUtils.executeAsTransaction(() -> createHabits(db));
db.close();
}
private void createHabits(SQLiteDatabase db)
private void createCheckmarks(@NonNull SQLiteDatabase db,
@NonNull Habit habit,
int tickmateTrackId)
{
Cursor c = null;
try
{
c = db.rawQuery("select _id, name, description from tracks", new String[0]);
if (!c.moveToFirst()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
habit.freqNum = 1;
habit.freqDen = 1;
habit.save();
createCheckmarks(db, habit, id);
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int tickmateTrackId)
{
Cursor c = null;
try
{
String[] params = { Integer.toString(tickmateTrackId) };
c = db.rawQuery("select distinct year, month, day from ticks where _track_id=?", params);
String[] params = {Integer.toString(tickmateTrackId)};
c = db.rawQuery(
"select distinct year, month, day from ticks where _track_id=?",
params);
if (!c.moveToFirst()) return;
do
@@ -121,9 +89,41 @@ public class TickmateDBImporter extends AbstractImporter
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, month, day);
habit.repetitions.toggle(cal.getTimeInMillis());
}
while (c.moveToNext());
habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis());
} while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createHabits(SQLiteDatabase db)
{
Cursor c = null;
try
{
c = db.rawQuery("select _id, name, description from tracks",
new String[0]);
if (!c.moveToFirst()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
Habit habit = new Habit();
habit.setName(name);
habit.setDescription(description);
habit.setFreqNum(1);
habit.setFreqDen(1);
habitList.add(habit);
createCheckmarks(db, habit, id);
} while (c.moveToNext());
}
finally
{

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides classes that deal with importing from and exporting to files.
*/
package org.isoron.uhabits.io;

View File

@@ -19,48 +19,65 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.apache.commons.lang3.builder.ToStringBuilder;
@Table(name = "Checkmarks")
public class Checkmark extends Model
/**
* A Checkmark represents the completion status of the habit for a given day.
* <p>
* While repetitions simply record that the habit was performed at a given date,
* a checkmark provides more information, such as whether a repetition was
* expected at that day or not.
* <p>
* Checkmarks are computed automatically from the list of repetitions.
*/
public class Checkmark
{
/**
* Indicates that there was no repetition at the timestamp, even though a repetition was
* expected.
*/
public static final int UNCHECKED = 0;
/**
* Indicates that there was no repetition at the timestamp, but one was not expected in any
* case, due to the frequency of the habit.
*/
public static final int CHECKED_IMPLICITLY = 1;
/**
* Indicates that there was a repetition at the timestamp.
*/
public static final int CHECKED_EXPLICITLY = 2;
/**
* The habit to which this checkmark belongs.
* Indicates that there was no repetition at the timestamp, but one was not
* expected in any case, due to the frequency of the habit.
*/
@Column(name = "habit")
public Habit habit;
public static final int CHECKED_IMPLICITLY = 1;
/**
* Timestamp of the day to which this checkmark corresponds. Time of the day must be midnight
* (UTC).
* Indicates that there was no repetition at the timestamp, even though a
* repetition was expected.
*/
@Column(name = "timestamp")
public Long timestamp;
public static final int UNCHECKED = 0;
/**
* Indicates whether there is a repetition at the given timestamp or not, and whether the
* repetition was expected. Assumes one of the values UNCHECKED, CHECKED_EXPLICITLY or
* CHECKED_IMPLICITLY.
*/
@Column(name = "value")
public Integer value;
final Habit habit;
final long timestamp;
final int value;
public Checkmark(Habit habit, long timestamp, int value)
{
this.habit = habit;
this.timestamp = timestamp;
this.value = value;
}
public long getTimestamp()
{
return timestamp;
}
public int getValue()
{
return value;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
}

View File

@@ -19,18 +19,10 @@
package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.IOException;
import java.io.Writer;
@@ -38,9 +30,13 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class CheckmarkList
/**
* The collection of {@link Checkmark}s belonging to a habit.
*/
public abstract class CheckmarkList
{
private Habit habit;
protected Habit habit;
public ModelObservable observable = new ModelObservable();
public CheckmarkList(Habit habit)
@@ -49,200 +45,29 @@ public class CheckmarkList
}
/**
* Deletes every checkmark that has timestamp either equal or newer than a given timestamp.
* These checkmarks will be recomputed at the next time they are queried.
*
* @param timestamp the timestamp
*/
public void deleteNewerThan(long timestamp)
{
new Delete().from(Checkmark.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
observable.notifyListeners();
}
/**
* Returns the values of the checkmarks that fall inside a certain interval of time.
*
* The values are returned in an array containing one integer value for each day of the
* interval. The first entry corresponds to the most recent day in the interval. Each subsequent
* entry corresponds to one day older than the previous entry. The boundaries of the time
* interval are included.
*
* @param fromTimestamp timestamp for the oldest checkmark
* @param toTimestamp timestamp for the newest checkmark
* @return values for the checkmarks inside the given interval
*/
@NonNull
public int[] getValues(long fromTimestamp, long toTimestamp)
{
compute(fromTimestamp, toTimestamp);
if(fromTimestamp > toTimestamp) return new int[0];
String query = "select value, timestamp from Checkmarks where " +
"habit = ? and timestamp >= ? and timestamp <= ?";
SQLiteDatabase db = Cache.openDatabase();
String args[] = { habit.getId().toString(), Long.toString(fromTimestamp),
Long.toString(toTimestamp) };
Cursor cursor = db.rawQuery(query, args);
long day = DateUtils.millisecondsInOneDay;
int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1;
int[] checks = new int[nDays];
if (cursor.moveToFirst())
{
do
{
long timestamp = cursor.getLong(1);
int offset = (int) ((timestamp - fromTimestamp) / day);
checks[nDays - offset - 1] = cursor.getInt(0);
} while (cursor.moveToNext());
}
cursor.close();
return checks;
}
/**
* Returns the values for all the checkmarks, since the oldest repetition of the habit until
* today. If there are no repetitions at all, returns an empty array.
*
* The values are returned in an array containing one integer value for each day since the
* first repetition of the habit until today. The first entry corresponds to today, the second
* entry corresponds to yesterday, and so on.
* Returns the values for all the checkmarks, since the oldest repetition of
* the habit until today. If there are no repetitions at all, returns an
* empty array.
* <p>
* The values are returned in an array containing one integer value for each
* day since the first repetition of the habit until today. The first entry
* corresponds to today, the second entry corresponds to yesterday, and so
* on.
*
* @return values for the checkmarks in the interval
*/
@NonNull
public int[] getAllValues()
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return new int[0];
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0];
Long fromTimestamp = oldestRep.timestamp;
Long fromTimestamp = oldestRep.getTimestamp();
Long toTimestamp = DateUtils.getStartOfToday();
return getValues(fromTimestamp, toTimestamp);
}
/**
* Computes and stores one checkmark for each day, since the first repetition until today.
* Days that already have a corresponding checkmark are skipped.
*/
protected void computeAll()
{
long fromTimestamp = habit.repetitions.getOldestTimestamp();
if(fromTimestamp == 0) return;
Long toTimestamp = DateUtils.getStartOfToday();
compute(fromTimestamp, toTimestamp);
}
/**
* Computes and stores one checkmark for each day that falls inside the specified interval of
* time. Days that already have a corresponding checkmark are skipped.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
*/
protected void compute(long from, final long to)
{
InterfaceUtils.throwIfMainThread();
final long day = DateUtils.millisecondsInOneDay;
Checkmark newestCheckmark = findNewest();
if(newestCheckmark != null) from = newestCheckmark.timestamp + day;
if(from > to) return;
long fromExtended = from - (long) (habit.freqDen) * day;
List<Repetition> reps = habit.repetitions
.selectFromTo(fromExtended, to)
.execute();
final int nDays = (int) ((to - from) / day) + 1;
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
final int checks[] = new int[nDaysExtended];
for (Repetition rep : reps)
{
int offset = (int) ((rep.timestamp - fromExtended) / day);
checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY;
}
for (int i = 0; i < nDays; i++)
{
int counter = 0;
for (int j = 0; j < habit.freqDen; j++)
if (checks[i + j] == 2) counter++;
if (counter >= habit.freqNum)
if(checks[i] != Checkmark.CHECKED_EXPLICITLY)
checks[i] = Checkmark.CHECKED_IMPLICITLY;
}
long timestamps[] = new long[nDays];
for (int i = 0; i < nDays; i++)
timestamps[i] = to - i * day;
insert(timestamps, checks);
}
private void insert(long timestamps[], int values[])
{
String query = "insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (int i = 0; i < timestamps.length; i++)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamps[i]);
statement.bindLong(3, values[i]);
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
/**
* Returns newest checkmark that has already been computed. Ignores any checkmark that has
* timestamp in the future. This does not update the cache.
*
* @return newest checkmark already computed
*/
@Nullable
protected Checkmark findNewest()
{
return new Select().from(Checkmark.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp desc")
.limit(1)
.executeSingle();
}
/**
* Returns the checkmark for today.
*
@@ -253,7 +78,7 @@ public class CheckmarkList
{
long today = DateUtils.getStartOfToday();
compute(today, today);
return findNewest();
return getNewest();
}
/**
@@ -264,41 +89,133 @@ public class CheckmarkList
public int getTodayValue()
{
Checkmark today = getToday();
if(today != null) return today.value;
if (today != null) return today.getValue();
else return Checkmark.UNCHECKED;
}
/**
* Writes the entire list of checkmarks to the given writer, in CSV format. There is one
* line for each checkmark. Each line contains two fields: timestamp and value.
* Returns the values of the checkmarks that fall inside a certain interval
* of time.
* <p>
* The values are returned in an array containing one integer value for each
* day of the interval. The first entry corresponds to the most recent day
* in the interval. Each subsequent entry corresponds to one day older than
* the previous entry. The boundaries of the time interval are included.
*
* @param from timestamp for the oldest checkmark
* @param to timestamp for the newest checkmark
* @return values for the checkmarks inside the given interval
*/
public abstract int[] getValues(long from, long to);
/**
* Marks as invalid every checkmark that has timestamp either equal or newer
* than a given timestamp. These checkmarks will be recomputed at the next
* time they are queried.
*
* @param timestamp the timestamp
*/
public abstract void invalidateNewerThan(long timestamp);
/**
* Writes the entire list of checkmarks to the given writer, in CSV format.
* There is one line for each checkmark. Each line contains two fields:
* timestamp and value.
*
* @param out the writer where the CSV will be output
* @throws IOException in case write operations fail
*/
public void writeCSV(Writer out) throws IOException
{
computeAll();
int values[] = getAllValues();
long timestamp = DateUtils.getStartOfToday();
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
String query = "select timestamp, value from checkmarks where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
do
for (int value : values)
{
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
Integer value = cursor.getInt(1);
out.write(String.format("%s,%d\n", timestamp, value));
} while(cursor.moveToNext());
cursor.close();
out.close();
String date = dateFormat.format(new Date(timestamp));
out.write(String.format("%s,%d\n", date, value));
timestamp -= DateUtils.millisecondsInOneDay;
}
}
/**
* Computes and stores one checkmark for each day that falls inside the
* specified interval of time. Days that already have a corresponding
* checkmark are skipped.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
*/
protected void compute(long from, final long to)
{
final long day = DateUtils.millisecondsInOneDay;
Checkmark newestCheckmark = getNewest();
if (newestCheckmark != null)
from = newestCheckmark.getTimestamp() + day;
if (from > to) return;
long fromExtended = from - (long) (habit.getFreqDen()) * day;
List<Repetition> reps =
habit.getRepetitions().getByInterval(fromExtended, to);
final int nDays = (int) ((to - from) / day) + 1;
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
final int checks[] = new int[nDaysExtended];
for (Repetition rep : reps)
{
int offset = (int) ((rep.getTimestamp() - fromExtended) / day);
checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY;
}
for (int i = 0; i < nDays; i++)
{
int counter = 0;
for (int j = 0; j < habit.getFreqDen(); j++)
if (checks[i + j] == 2) counter++;
if (counter >= habit.getFreqNum())
if (checks[i] != Checkmark.CHECKED_EXPLICITLY)
checks[i] = Checkmark.CHECKED_IMPLICITLY;
}
long timestamps[] = new long[nDays];
for (int i = 0; i < nDays; i++)
timestamps[i] = to - i * day;
insert(timestamps, checks);
}
/**
* Computes and stores one checkmark for each day, since the first
* repetition until today. Days that already have a corresponding checkmark
* are skipped.
*/
protected void computeAll()
{
Repetition oldest = habit.getRepetitions().getOldest();
if (oldest == null) return;
Long today = DateUtils.getStartOfToday();
compute(oldest.getTimestamp(), today);
}
/**
* Returns newest checkmark that has already been computed. Ignores any
* checkmark that has timestamp in the future. This does not update the
* cache.
*
* @return newest checkmark already computed
*/
protected abstract Checkmark getNewest();
protected abstract void insert(long timestamps[], int values[]);
}

View File

@@ -19,135 +19,75 @@
package org.isoron.uhabits.models;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.query.Update;
import com.activeandroid.util.SQLiteUtils;
import com.opencsv.CSVWriter;
import org.isoron.uhabits.utils.ColorUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.utils.DateUtils;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Locale;
@Table(name = "Habits")
public class Habit extends Model
import javax.inject.Inject;
/**
* The thing that the user wants to track.
*/
public class Habit
{
/**
* Name of the habit
*/
@Column(name = "name")
public String name;
public static final String HABIT_URI_FORMAT =
"content://org.isoron.uhabits/habit/%d";
/**
* Description of the habit
*/
@Column(name = "description")
public String description;
/**
* Frequency numerator. If a habit is performed 3 times in 7 days, this field equals 3.
*/
@Column(name = "freq_num")
public Integer freqNum;
/**
* Frequency denominator. If a habit is performed 3 times in 7 days, this field equals 7.
*/
@Column(name = "freq_den")
public Integer freqDen;
/**
* Color of the habit.
*
* This number is not an android.graphics.Color, but an index to the activity color palette,
* which changes according to the theme. To convert this color into an android.graphics.Color,
* use ColorHelper.getColor(context, habit.color).
*/
@Column(name = "color")
public Integer color;
/**
* Position of the habit. Habits are usually sorted by this field.
*/
@Column(name = "position")
public Integer position;
/**
* Hour of the day the reminder should be shown. If there is no reminder, this equals to null.
*/
@Nullable
@Column(name = "reminder_hour")
public Integer reminderHour;
private Long id;
@NonNull
private String name;
@NonNull
private String description;
@NonNull
private Integer freqNum;
@NonNull
private Integer freqDen;
@NonNull
private Integer color;
/**
* Minute the reminder should be shown. If there is no reminder, this equals to null.
*/
@Nullable
@Column(name = "reminder_min")
public Integer reminderMin;
private Integer reminderHour;
@Nullable
private Integer reminderMin;
/**
* Days of the week the reminder should be shown. This field can be converted to a list of
* booleans using the method DateHelper.unpackWeekdayList and converted back to an integer by
* using the method DateHelper.packWeekdayList. If the habit has no reminders, this value
* should be ignored.
*/
@NonNull
@Column(name = "reminder_days")
public Integer reminderDays;
private Integer reminderDays;
/**
* Not currently used.
*/
@Column(name = "highlight")
public Integer highlight;
/**
* Flag that indicates whether the habit is archived. Archived habits are usually omitted from
* listings, unless explicitly included.
*/
@Column(name = "archived")
public Integer archived;
/**
* List of streaks belonging to this habit.
*/
@NonNull
public StreakList streaks;
private Integer highlight;
/**
* List of scores belonging to this habit.
*/
@NonNull
public ScoreList scores;
private Integer archived;
/**
* List of repetitions belonging to this habit.
*/
@NonNull
public RepetitionList repetitions;
private StreakList streaks;
/**
* List of checkmarks belonging to this habit.
*/
@NonNull
public CheckmarkList checkmarks;
private ScoreList scores;
public ModelObservable observable = new ModelObservable();
@NonNull
private RepetitionList repetitions;
@NonNull
private CheckmarkList checkmarks;
private ModelObservable observable = new ModelObservable();
@Inject
ModelFactory factory;
/**
* Constructs a habit with the same attributes as the specified habit.
@@ -156,328 +96,44 @@ public class Habit extends Model
*/
public Habit(Habit model)
{
HabitsApplication.getComponent().inject(this);
reminderDays = DateUtils.ALL_WEEK_DAYS;
copyAttributes(model);
copyFrom(model);
checkmarks = new CheckmarkList(this);
streaks = new StreakList(this);
scores = new ScoreList(this);
repetitions = new RepetitionList(this);
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buidRepetitionList(this);
}
/**
* Constructs a habit with default attributes. The habit is not archived, not highlighted, has
* no reminders and is placed in the last position of the list of habits.
* Constructs a habit with default attributes.
* <p>
* The habit is not archived, not highlighted, has no reminders and is
* placed in the last position of the list of habits.
*/
public Habit()
{
HabitsApplication.getComponent().inject(this);
this.color = 5;
this.position = Habit.countWithArchived();
this.highlight = 0;
this.archived = 0;
this.freqDen = 7;
this.freqNum = 3;
this.reminderDays = DateUtils.ALL_WEEK_DAYS;
checkmarks = new CheckmarkList(this);
streaks = new StreakList(this);
scores = new ScoreList(this);
repetitions = new RepetitionList(this);
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buidRepetitionList(this);
}
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
*/
@Nullable
public static Habit get(long id)
{
return Habit.load(Habit.class, id);
}
/**
* Returns a list of all habits, optionally including archived habits.
*
* @param includeArchive whether archived habits should be included the list
* @return list of all habits
*/
@NonNull
public static List<Habit> getAll(boolean includeArchive)
{
if(includeArchive) return selectWithArchived().execute();
else return select().execute();
}
/**
* Returns the habit that occupies a certain position.
*
* @param position the position of the desired habit
* @return the habit at that position, or null if there is none
*/
@Nullable
public static Habit getByPosition(int position)
{
return selectWithArchived().where("position = ?", position).executeSingle();
}
/**
* Changes the id of a habit on the database.
*
* @param oldId the original id
* @param newId the new 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));
}
@NonNull
protected static From select()
{
return new Select().from(Habit.class).where("archived = 0").orderBy("position");
}
@NonNull
protected static From selectWithArchived()
{
return new Select().from(Habit.class).orderBy("position");
}
/**
* Returns the total number of unarchived habits.
*
* @return number of unarchived habits
*/
public static int count()
{
return select().count();
}
/**
* Returns the total number of habits, including archived habits.
*
* @return number of habits, including archived
*/
public static int countWithArchived()
{
return selectWithArchived().count();
}
/**
* Returns a list the habits that have a reminder. Does not include archived habits.
*
* @return list of habits with reminder
*/
@NonNull
public static List<Habit> getHabitsWithReminder()
{
return select().where("reminder_hour is not null").execute();
}
/**
* Changes the position of a habit on the list.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public static void reorder(Habit from, Habit to)
{
if(from == to) return;
if (to.position < from.position)
{
new Update(Habit.class).set("position = position + 1")
.where("position >= ? and position < ?", to.position, from.position)
.execute();
}
else
{
new Update(Habit.class).set("position = position - 1")
.where("position > ? and position <= ?", from.position, to.position)
.execute();
}
from.position = to.position;
from.save();
}
/**
* Recomputes the position for every habit in the database. It should never be necessary
* to call this method.
*/
public static void rebuildOrder()
{
List<Habit> habits = selectWithArchived().execute();
ActiveAndroid.beginTransaction();
try
{
int i = 0;
for (Habit h : habits)
{
h.position = i++;
h.save();
}
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
}
/**
* Copies all the attributes of the specified habit into this habit
*
* @param model the model whose attributes should be copied from
*/
public void copyAttributes(@NonNull Habit model)
{
this.name = model.name;
this.description = model.description;
this.freqNum = model.freqNum;
this.freqDen = model.freqDen;
this.color = model.color;
this.position = model.position;
this.reminderHour = model.reminderHour;
this.reminderMin = model.reminderMin;
this.reminderDays = model.reminderDays;
this.highlight = model.highlight;
this.archived = model.archived;
observable.notifyListeners();
}
/**
* Saves the habit on the database, and assigns the specified id to it.
*
* @param id the id that the habit should receive
*/
public void save(long id)
{
save();
Habit.updateId(getId(), id);
}
/**
* Deletes the habit and all data associated to it, including checkmarks, repetitions and
* scores.
*/
public void cascadeDelete()
{
Long id = getId();
ActiveAndroid.beginTransaction();
try
{
new Delete().from(Checkmark.class).where("habit = ?", id).execute();
new Delete().from(Repetition.class).where("habit = ?", id).execute();
new Delete().from(Score.class).where("habit = ?", id).execute();
new Delete().from(Streak.class).where("habit = ?", id).execute();
delete();
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
}
/**
* Returns the public URI that identifies this habit
* @return the uri
*/
public Uri getUri()
{
String s = String.format(Locale.US, "content://org.isoron.uhabits/habit/%d", getId());
return Uri.parse(s);
}
/**
* Returns whether the habit is archived or not.
* @return true if archived
*/
public boolean isArchived()
{
return archived != 0;
}
private static void updateAttributes(@NonNull List<Habit> habits, @Nullable Integer color,
@Nullable Integer archived)
{
ActiveAndroid.beginTransaction();
try
{
for (Habit h : habits)
{
if(color != null) h.color = color;
if(archived != null) h.archived = archived;
h.save();
}
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
for(Habit h : habits)
h.observable.notifyListeners();
}
}
/**
* Archives an entire list of habits
*
* @param habits the habits to be archived
*/
public static void archive(@NonNull List<Habit> habits)
{
updateAttributes(habits, null, 1);
}
/**
* Unarchives an entire list of habits
*
* @param habits the habits to be unarchived
*/
public static void unarchive(@NonNull List<Habit> habits)
{
updateAttributes(habits, null, 0);
}
/**
* Sets the color for an entire list of habits.
*
* @param habits the habits to be modified
* @param color the new color to be set
*/
public static void setColor(@NonNull List<Habit> habits, int color)
{
updateAttributes(habits, color, null);
for(Habit h : habits)
h.observable.notifyListeners();
}
/**
* Checks whether the habit has a reminder set.
*
* @return true if habit has reminder
*/
public boolean hasReminder()
{
return (reminderHour != null && reminderMin != null);
}
/**
* Clears the reminder for a habit. This sets all the related fields to null.
* Clears the reminder for a habit. This sets all the related fields to
* null.
*/
public void clearReminder()
{
@@ -488,36 +144,269 @@ public class Habit extends Model
}
/**
* Writes the list of habits to the given writer, in CSV format. There is one line for each
* habit, containing the fields name, description, frequency numerator, frequency denominator
* and color. The color is written in HTML format (#000000).
* Copies all the attributes of the specified habit into this habit
*
* @param habits the list of habits to write
* @param out the writer that will receive the result
* @throws IOException if write operations fail
* @param model the model whose attributes should be copied from
*/
public static void writeCSV(List<Habit> habits, Writer out) throws IOException
public void copyFrom(@NonNull Habit model)
{
String header[] = { "Position", "Name", "Description", "NumRepetitions", "Interval", "Color" };
this.name = model.getName();
this.description = model.getDescription();
this.freqNum = model.getFreqNum();
this.freqDen = model.getFreqDen();
this.color = model.getColor();
this.reminderHour = model.getReminderHour();
this.reminderMin = model.getReminderMin();
this.reminderDays = model.getReminderDays();
this.highlight = model.getHighlight();
this.archived = model.getArchived();
observable.notifyListeners();
}
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
/**
* Flag that indicates whether the habit is archived. Archived habits are
* usually omitted from listings, unless explicitly included.
*/
public Integer getArchived()
{
return archived;
}
for(Habit habit : habits)
{
String[] cols =
{
String.format("%03d", habit.position + 1),
habit.name,
habit.description,
Integer.toString(habit.freqNum),
Integer.toString(habit.freqDen),
ColorUtils.toHTML(ColorUtils.CSV_PALETTE[habit.color])
};
/**
* List of checkmarks belonging to this habit.
*/
@NonNull
public CheckmarkList getCheckmarks()
{
return checkmarks;
}
csv.writeNext(cols, false);
}
/**
* Color of the habit.
* <p>
* This number is not an android.graphics.Color, but an index to the
* activity color palette, which changes according to the theme. To convert
* this color into an android.graphics.Color, use ColorHelper.getColor(context,
* habit.color).
*/
public Integer getColor()
{
return color;
}
csv.close();
public void setColor(Integer color)
{
this.color = color;
}
/**
* Description of the habit
*/
public String getDescription()
{
return description;
}
public void setDescription(String description)
{
this.description = description;
}
/**
* Frequency denominator. If a habit is performed 3 times in 7 days, this
* field equals 7.
*/
public Integer getFreqDen()
{
return freqDen;
}
public void setFreqDen(Integer freqDen)
{
this.freqDen = freqDen;
}
/**
* Frequency numerator. If a habit is performed 3 times in 7 days, this
* field equals 3.
*/
public Integer getFreqNum()
{
return freqNum;
}
public void setFreqNum(Integer freqNum)
{
this.freqNum = freqNum;
}
/**
* Not currently used.
*/
public Integer getHighlight()
{
return highlight;
}
public void setHighlight(Integer highlight)
{
this.highlight = highlight;
}
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
/**
* Name of the habit
*/
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public ModelObservable getObservable()
{
return observable;
}
/**
* Days of the week the reminder should be shown. This field can be
* converted to a list of booleans using the method DateHelper.unpackWeekdayList
* and converted back to an integer by using the method
* DateHelper.packWeekdayList. If the habit has no reminders, this value
* should be ignored.
*/
@NonNull
public Integer getReminderDays()
{
return reminderDays;
}
public void setReminderDays(@NonNull Integer reminderDays)
{
this.reminderDays = reminderDays;
}
/**
* Hour of the day the reminder should be shown. If there is no reminder,
* this equals to null.
*/
@Nullable
public Integer getReminderHour()
{
return reminderHour;
}
public void setReminderHour(@Nullable Integer reminderHour)
{
this.reminderHour = reminderHour;
}
/**
* Minute the reminder should be shown. If there is no reminder, this equals
* to null.
*/
@Nullable
public Integer getReminderMin()
{
return reminderMin;
}
public void setReminderMin(@Nullable Integer reminderMin)
{
this.reminderMin = reminderMin;
}
/**
* List of repetitions belonging to this habit.
*/
@NonNull
public RepetitionList getRepetitions()
{
return repetitions;
}
/**
* List of scores belonging to this habit.
*/
@NonNull
public ScoreList getScores()
{
return scores;
}
/**
* List of streaks belonging to this habit.
*/
@NonNull
public StreakList getStreaks()
{
return streaks;
}
/**
* Returns the public URI that identifies this habit
*
* @return the uri
*/
public Uri getUri()
{
String s = String.format(Locale.US, HABIT_URI_FORMAT, getId());
return Uri.parse(s);
}
/**
* Checks whether the habit has a reminder set.
*
* @return true if habit has reminder, false otherwise
*/
public boolean hasReminder()
{
return (reminderHour != null && reminderMin != null);
}
/**
* Returns whether the habit is archived or not.
*
* @return true if archived
*/
public boolean isArchived()
{
return archived != 0;
}
public void setArchived(Integer archived)
{
this.archived = archived;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("id", id)
.append("name", name)
.append("description", description)
.append("freqNum", freqNum)
.append("freqDen", freqDen)
.append("color", color)
.append("reminderHour", reminderHour)
.append("reminderMin", reminderMin)
.append("reminderDays", reminderDays)
.append("highlight", highlight)
.append("archived", archived)
.toString();
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.opencsv.CSVWriter;
import org.isoron.uhabits.utils.ColorUtils;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* An ordered collection of {@link Habit}s.
*/
public abstract class HabitList
{
private ModelObservable observable;
/**
* Creates a new HabitList.
* <p>
* Depending on the implementation, this list can either be empty or be
* populated by some pre-existing habits.
*/
public HabitList()
{
observable = new ModelObservable();
}
/**
* Inserts a new habit in the list.
*
* @param habit the habit to be inserted
*/
public abstract void add(Habit habit);
/**
* Returns the total number of unarchived habits.
*
* @return number of unarchived habits
*/
public abstract int count();
/**
* Returns the total number of habits, including archived habits.
*
* @return number of habits, including archived
*/
public abstract int countWithArchived();
/**
* Returns a list of all habits, optionally including archived habits.
*
* @param includeArchive whether archived habits should be included the
* list
* @return list of all habits
*/
@NonNull
public abstract List<Habit> getAll(boolean includeArchive);
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
*/
public abstract Habit getById(long id);
/**
* Returns the habit that occupies a certain position.
*
* @param position the position of the desired habit
* @return the habit at that position, or null if there is none
*/
@Nullable
public abstract Habit getByPosition(int position);
/**
* Returns the list of habits that match a given condition.
*
* @param matcher the matcher that checks the condition
* @return the list of matching habits
*/
@NonNull
public List<Habit> getFiltered(HabitMatcher matcher)
{
LinkedList<Habit> habits = new LinkedList<>();
for (Habit h : getAll(true)) if (matcher.matches(h)) habits.add(h);
return habits;
}
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns a list the habits that have a reminder. Does not include archived
* habits.
*
* @return list of habits with reminder
*/
@NonNull
public List<Habit> getWithReminder()
{
return getFiltered(habit -> habit.hasReminder());
}
/**
* Returns the index of the given habit in the list, or -1 if the list does
* not contain the habit.
*
* @param h the habit
* @return the index of the habit, or -1 if not in the list
*/
public abstract int indexOf(Habit h);
/**
* Removes the given habit from the list.
* <p>
* If the given habit is not in the list, does nothing.
*
* @param h the habit to be removed.
*/
public abstract void remove(@NonNull Habit h);
/**
* Changes the position of a habit in the list.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public abstract void reorder(Habit from, Habit to);
/**
* Notifies the list that a certain list of habits has been modified.
* <p>
* Depending on the implementation, this operation might trigger a write to
* disk, or do nothing at all. To make sure that the habits get persisted,
* this operation must be called.
*
* @param habits the list of habits that have been modified.
*/
public abstract void update(List<Habit> habits);
/**
* Notifies the list that a certain habit has been modified.
* <p>
* See {@link #update(List)} for more details.
*
* @param habit the habit that has been modified.
*/
public void update(Habit habit)
{
update(Collections.singletonList(habit));
}
/**
* Writes the list of habits to the given writer, in CSV format. There is
* one line for each habit, containing the fields name, description,
* frequency numerator, frequency denominator and color. The color is
* written in HTML format (#000000).
*
* @param out the writer that will receive the result
* @throws IOException if write operations fail
*/
public void writeCSV(Writer out) throws IOException
{
String header[] = {
"Position",
"Name",
"Description",
"NumRepetitions",
"Interval",
"Color"
};
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
for (Habit habit : getAll(true))
{
String[] cols = {
String.format("%03d", indexOf(habit) + 1),
habit.getName(),
habit.getDescription(),
Integer.toString(habit.getFreqNum()),
Integer.toString(habit.getFreqDen()),
ColorUtils.CSV_PALETTE[habit.getColor()]
};
csv.writeNext(cols, false);
}
csv.close();
}
/**
* A HabitMatcher decides whether habits match or not a certain condition.
* They can be used to produce filtered lists of habits.
*/
public interface HabitMatcher
{
/**
* Returns true if the given habit matches.
*
* @param habit the habit to be checked.
* @return true if matches, false otherwise.
*/
boolean matches(Habit habit);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models;
/**
* Interface implemented by factories that provide concrete implementations
* of the core model classes.
*/
public interface ModelFactory
{
RepetitionList buidRepetitionList(Habit habit);
HabitList buildHabitList();
CheckmarkList buildCheckmarkList(Habit habit);
ScoreList buildScoreList(Habit habit);
StreakList buildStreakList(Habit habit);
}

View File

@@ -22,33 +22,62 @@ package org.isoron.uhabits.models;
import java.util.LinkedList;
import java.util.List;
/**
* A ModelObservable allows objects to subscribe themselves to it and receive
* notifications whenever the model is changed.
*/
public class ModelObservable
{
List<Listener> listeners;
/**
* Creates a new ModelObservable with no listeners.
*/
public ModelObservable()
{
super();
listeners = new LinkedList<>();
}
public interface Listener
{
void onModelChange();
}
/**
* Adds the given listener to the observable.
*
* @param l the listener to be added.
*/
public void addListener(Listener l)
{
listeners.add(l);
}
/**
* Notifies every listener that the model has changed.
* <p>
* Only models should call this method.
*/
public void notifyListeners()
{
for (Listener l : listeners) l.onModelChange();
}
/**
* Removes the given listener.
* <p>
* The listener will no longer be notified when the model changes. If the
* given listener is not subscrined to this observable, does nothing.
*
* @param l the listener to be removed
*/
public void removeListener(Listener l)
{
listeners.remove(l);
}
public void notifyListeners()
/**
* Interface implemented by objects that want to be notified when the model
* changes.
*/
public interface Listener
{
for(Listener l : listeners) l.onModelChange();
void onModelChange();
}
}

View File

@@ -19,22 +19,51 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import android.support.annotation.NonNull;
@Table(name = "Repetitions")
public class Repetition extends Model
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
* Represents a record that the user has performed a certain habit at a certain
* date.
*/
public class Repetition
{
/**
* Habit to which this repetition belong.
*/
@Column(name = "habit")
public Habit habit;
@NonNull
private final Habit habit;
private final long timestamp;
/**
* Timestamp of the day this repetition occurred. Time of day should be midnight (UTC).
* Creates a new repetition with given parameters.
* <p>
* The timestamp corresponds to the days this repetition occurred. Time of
* day must be midnight (UTC).
*
* @param habit the habit to which this repetition belongs.
* @param timestamp the time this repetition occurred.
*/
@Column(name = "timestamp")
public Long timestamp;
public Repetition(Habit habit, long timestamp)
{
this.habit = habit;
this.timestamp = timestamp;
}
public Habit getHabit()
{
return habit;
}
public long getTimestamp()
{
return timestamp;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("timestamp", timestamp)
.toString();
}
}

View File

@@ -19,199 +19,179 @@
package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
public class RepetitionList
/**
* The collection of {@link Repetition}s belonging to a habit.
*/
public abstract class RepetitionList
{
@NonNull
private Habit habit;
public ModelObservable observable = new ModelObservable();
protected final Habit habit;
@NonNull
protected final ModelObservable observable;
public RepetitionList(@NonNull Habit habit)
{
this.habit = habit;
}
@NonNull
protected From select()
{
return new Select().from(Repetition.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp");
}
@NonNull
protected From selectFromTo(long timeFrom, long timeTo)
{
return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo);
this.observable = new ModelObservable();
}
/**
* Checks whether there is a repetition at a given timestamp.
* Adds a repetition to the list.
* <p>
* Any implementation of this method must call observable.notifyListeners()
* after the repetition has been added.
*
* @param timestamp the timestamp to check
* @return true if there is a repetition
* @param repetition the repetition to be added.
*/
public boolean contains(long timestamp)
{
int count = select().where("timestamp = ?", timestamp).count();
return (count > 0);
}
public abstract void add(Repetition repetition);
/**
* Deletes the repetition at a given timestamp, if it exists.
* Returns true if the list contains a repetition that has the given
* timestamp.
*
* @param timestamp the timestamp of the repetition to delete
* @param timestamp the timestamp to find.
* @return true if list contains repetition with given timestamp, false
* otherwise.
*/
public void delete(long timestamp)
public boolean containsTimestamp(long timestamp)
{
new Delete().from(Repetition.class)
.where("habit = ?", habit.getId())
.and("timestamp = ?", timestamp)
.execute();
return (getByTimestamp(timestamp) != null);
}
/**
* Toggles the repetition at a certain timestamp. That is, deletes the repetition if it exists
* or creates one if it does not.
* Returns the list of repetitions that happened within the given time
* interval.
*
* @param timestamp the timestamp of the repetition to toggle
* The list is sorted by timestamp in decreasing order. That is, the first
* element corresponds to the most recent timestamp. The endpoints of the
* interval are included.
*
* @param fromTimestamp timestamp of the beginning of the interval
* @param toTimestamp timestamp of the end of the interval
* @return list of repetitions within given time interval
*/
public void toggle(long timestamp)
{
timestamp = DateUtils.getStartOfDay(timestamp);
if (contains(timestamp))
delete(timestamp);
else
insert(timestamp);
habit.scores.invalidateNewerThan(timestamp);
habit.checkmarks.deleteNewerThan(timestamp);
habit.streaks.deleteNewerThan(timestamp);
observable.notifyListeners();
}
private void insert(long timestamp)
{
String[] args = { habit.getId().toString(), Long.toString(timestamp) };
SQLiteUtils.execSql("insert into Repetitions(habit, timestamp) values (?,?)", args);
}
public abstract List<Repetition> getByInterval(long fromTimestamp,
long toTimestamp);
/**
* Returns the oldest repetition for the habit. If there is no repetition, returns null.
* Repetitions in the future are discarded.
* Returns the repetition that has the given timestamp, or null if none
* exists.
*
* @return oldest repetition for the habit
* @param timestamp the repetition timestamp.
* @return the repetition that has the given timestamp.
*/
@Nullable
public Repetition getOldest()
public abstract Repetition getByTimestamp(long timestamp);
@NonNull
public ModelObservable getObservable()
{
return (Repetition) select().limit(1).executeSingle();
return observable;
}
/**
* Returns the timestamp of the oldest repetition. If there are no repetitions, returns zero.
* Repetitions in the future are discarded.
* Returns the oldest repetition in the list.
* <p>
* If the list is empty, returns null. Repetitions in the future are
* discarded.
*
* @return timestamp of the oldest repetition
* @return oldest repetition in the list, or null if list is empty.
*/
public long getOldestTimestamp()
{
String[] args = { habit.getId().toString(), Long.toString(DateUtils.getStartOfToday()) };
String query = "select timestamp from Repetitions where habit = ? and timestamp <= ? " +
"order by timestamp limit 1";
return DatabaseUtils.longQuery(query, args);
}
@Nullable
public abstract Repetition getOldest();
/**
* Returns the total number of repetitions for each month, from the first repetition until
* today, grouped by day of week. The repetitions are returned in a HashMap. The key is the
* timestamp for the first day of the month, at midnight (00:00). The value is an integer
* array with 7 entries. The first entry contains the total number of repetitions during
* the specified month that occurred on a Saturday. The second entry corresponds to Sunday,
* and so on. If there are no repetitions during a certain month, the value is null.
* Returns the total number of repetitions for each month, from the first
* repetition until today, grouped by day of week.
* <p>
* The repetitions are returned in a HashMap. The key is the timestamp for
* the first day of the month, at midnight (00:00). The value is an integer
* array with 7 entries. The first entry contains the total number of
* repetitions during the specified month that occurred on a Saturday. The
* second entry corresponds to Sunday, and so on. If there are no
* repetitions during a certain month, the value is null.
*
* @return total number of repetitions by month versus day of week
*/
@NonNull
public HashMap<Long, Integer[]> getWeekdayFrequency()
{
Repetition oldestRep = getOldest();
if(oldestRep == null) return new HashMap<>();
List<Repetition> reps = getByInterval(0, DateUtils.getStartOfToday());
HashMap<Long, Integer[]> map = new HashMap<>();
String query = "select strftime('%Y', timestamp / 1000, 'unixepoch') as year," +
"strftime('%m', timestamp / 1000, 'unixepoch') as month," +
"strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " +
"count(*) from repetitions " +
"where habit = ? and timestamp <= ? " +
"group by year, month, weekday";
String[] params = { habit.getId().toString(),
Long.toString(DateUtils.getStartOfToday()) };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return new HashMap<>();
HashMap <Long, Integer[]> map = new HashMap<>();
GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
do
for (Repetition r : reps)
{
int year = Integer.parseInt(cursor.getString(0));
int month = Integer.parseInt(cursor.getString(1));
int weekday = (Integer.parseInt(cursor.getString(2)) + 1) % 7;
int count = cursor.getInt(3);
Calendar date = DateUtils.getCalendar(r.getTimestamp());
int weekday = date.get(Calendar.DAY_OF_WEEK) % 7;
date.set(Calendar.DAY_OF_MONTH, 1);
date.set(year, month - 1, 1);
long timestamp = date.getTimeInMillis();
Integer[] list = map.get(timestamp);
if(list == null)
if (list == null)
{
list = new Integer[7];
Arrays.fill(list, 0);
map.put(timestamp, list);
}
list[weekday] = count;
list[weekday]++;
}
while (cursor.moveToNext());
cursor.close();
return map;
}
/**
* Returns the total number of repetitions that happened within the specified interval of time.
* Removes a given repetition from the list.
* <p>
* If the list does not contain the repetition, it is unchanged.
* <p>
* Any implementation of this method must call observable.notifyListeners()
* after the repetition has been added.
*
* @param from beginning of the interval
* @param to end of the interval
* @return number of repetition in the given interval
* @param repetition the repetition to be removed
*/
public int count(long from, long to)
public abstract void remove(@NonNull Repetition repetition);
/**
* Adds or remove a repetition at a certain timestamp.
* <p>
* If there exists a repetition on the list with the given timestamp, the
* method removes this repetition from the list and returns it. If there are
* no repetitions with the given timestamp, creates and adds one to the
* list, then returns it.
*
* @param timestamp the timestamp for the timestamp that should be added or
* removed.
* @return the repetition that has been added or removed.
*/
@NonNull
public Repetition toggleTimestamp(long timestamp)
{
return selectFromTo(from, to).count();
timestamp = DateUtils.getStartOfDay(timestamp);
Repetition rep = getByTimestamp(timestamp);
if (rep != null) remove(rep);
else
{
rep = new Repetition(habit, timestamp);
add(rep);
}
// habit.getScores().invalidateNewerThan(timestamp);
// habit.getCheckmarks().invalidateNewerThan(timestamp);
// habit.getStreaks().invalidateNewerThan(timestamp);
return rep;
}
}

View File

@@ -19,78 +19,60 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.apache.commons.lang3.builder.ToStringBuilder;
@Table(name = "Score")
public class Score extends Model
/**
* Represents how strong a habit is at a certain date.
*/
public class Score
{
/**
* Minimum score value required to earn half a star.
* Habit to which this score belongs to.
*/
public static final int HALF_STAR_CUTOFF = 9629750;
private Habit habit;
/**
* Minimum score value required to earn a full star.
* Timestamp of the day to which this score applies. Time of day should be
* midnight (UTC).
*/
public static final int FULL_STAR_CUTOFF = 15407600;
private Long timestamp;
/**
* Value of the score.
*/
private Integer value;
/**
* Maximum score value attainable by any habit.
*/
public static final int MAX_VALUE = 19259478;
/**
* Status indicating that the habit has not earned any star.
*/
public static final int EMPTY_STAR = 0;
public Score(Habit habit, Long timestamp, Integer value)
{
this.habit = habit;
this.timestamp = timestamp;
this.value = value;
}
/**
* Status indicating that the habit has earned half a star.
*/
public static final int HALF_STAR = 1;
/**
* Status indicating that the habit has earned a full star.
*/
public static final int FULL_STAR = 2;
/**
* Habit to which this score belongs to.
*/
@Column(name = "habit")
public Habit habit;
/**
* Timestamp of the day to which this score applies. Time of day should be midnight (UTC).
*/
@Column(name = "timestamp")
public Long timestamp;
/**
* Value of the score.
*/
@Column(name = "score")
public Integer score;
/**
* Given the frequency of the habit, the previous score, and the value of the current checkmark,
* computes the current score for the habit.
* Given the frequency of the habit, the previous score, and the value of
* the current checkmark, computes the current score for the habit.
* <p>
* The frequency of the habit is the number of repetitions divided by the
* length of the interval. For example, a habit that should be repeated 3
* times in 8 days has frequency 3.0 / 8.0 = 0.375.
* <p>
* The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or
* CHECK_EXPLICITLY.
*
* The frequency of the habit is the number of repetitions divided by the length of the
* interval. For example, a habit that should be repeated 3 times in 8 days has frequency 3.0 /
* 8.0 = 0.375.
*
* The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or CHECK_EXPLICITLY.
*
* @param frequency the frequency of the habit
* @param previousScore the previous score of the habit
* @param frequency the frequency of the habit
* @param previousScore the previous score of the habit
* @param checkmarkValue the value of the current checkmark
*
* @return the current score
*/
public static int compute(double frequency, int previousScore, int checkmarkValue)
public static int compute(double frequency,
int previousScore,
int checkmarkValue)
{
double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1));
int score = (int) (previousScore * multiplier);
@@ -104,16 +86,27 @@ public class Score extends Model
return score;
}
/**
* Return the current star status for the habit, which can one of EMPTY_STAR, HALF_STAR or
* FULL_STAR.
*
* @return current star status
*/
public int getStarStatus()
public Habit getHabit()
{
if(score >= Score.FULL_STAR_CUTOFF) return Score.FULL_STAR;
if(score >= Score.HALF_STAR_CUTOFF) return Score.HALF_STAR;
return Score.EMPTY_STAR;
return habit;
}
public Long getTimestamp()
{
return timestamp;
}
public Integer getValue()
{
return value;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
}

View File

@@ -21,215 +21,59 @@ package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
public class ScoreList
public abstract class ScoreList
{
@NonNull
private Habit habit;
public ModelObservable observable = new ModelObservable();
protected final Habit habit;
protected ModelObservable observable;
/**
* Constructs a new ScoreList associated with the given habit.
* Creates a new ScoreList for the given habit.
* <p>
* The list is populated automatically according to the repetitions that the
* habit has.
*
* @param habit the habit this list should be associated with
* @param habit the habit to which the scores belong.
*/
public ScoreList(@NonNull Habit habit)
public ScoreList(Habit habit)
{
this.habit = habit;
}
protected From select()
{
return new Select()
.from(Score.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc");
observable = new ModelObservable();
}
/**
* Marks all scores that have timestamp equal to or newer than the given timestamp as invalid.
* Any following getValue calls will trigger the scores to be recomputed.
*
* @param timestamp the oldest timestamp that should be invalidated
*/
public void invalidateNewerThan(long timestamp)
{
new Delete().from(Score.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
observable.notifyListeners();
}
/**
* Computes and saves the scores that are missing since the first repetition of the habit.
*/
private void computeAll()
{
long fromTimestamp = habit.repetitions.getOldestTimestamp();
if(fromTimestamp == 0) return;
long toTimestamp = DateUtils.getStartOfToday();
compute(fromTimestamp, toTimestamp);
}
/**
* Computes and saves the scores that are missing inside a given time interval. Scores that
* have already been computed are skipped, therefore there is no harm in calling this function
* more times, or with larger intervals, than strictly needed. The endpoints of the interval are
* included.
*
* This function assumes that there are no gaps on the scores. That is, if the newest score has
* timestamp t, then every score with timestamp lower than t has already been computed.
*
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the time interval
*/
protected void compute(long from, long to)
{
InterfaceUtils.throwIfMainThread();
final long day = DateUtils.millisecondsInOneDay;
final double freq = ((double) habit.freqNum) / habit.freqDen;
int newestScoreValue = findNewestValue();
long newestTimestamp = findNewestTimestamp();
if(newestTimestamp > 0)
from = newestTimestamp + day;
final int checkmarkValues[] = habit.checkmarks.getValues(from, to);
final long beginning = from;
int lastScore = newestScoreValue;
int size = checkmarkValues.length;
long timestamps[] = new long[size];
long values[] = new long[size];
for (int i = 0; i < checkmarkValues.length; i++)
{
int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1];
lastScore = Score.compute(freq, lastScore, checkmarkValue);
timestamps[i] = beginning + day * i;
values[i] = lastScore;
}
insert(timestamps, values);
}
/**
* Returns the value of the most recent score that was already computed. If no score has been
* computed yet, returns zero.
*
* @return value of newest score, or zero if none exist
*/
protected int findNewestValue()
{
String args[] = { habit.getId().toString() };
String query = "select score from Score where habit = ? order by timestamp desc limit 1";
return SQLiteUtils.intQuery(query, args);
}
private long findNewestTimestamp()
{
String args[] = { habit.getId().toString() };
String query = "select timestamp from Score where habit = ? order by timestamp desc limit 1";
return DatabaseUtils.longQuery(query, args);
}
private void insert(long timestamps[], long values[])
{
String query = "insert into Score(habit, timestamp, score) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (int i = 0; i < timestamps.length; i++)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamps[i]);
statement.bindLong(3, values[i]);
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
/**
* Returns the score for a certain day.
*
* @param timestamp the timestamp for the day
* @return the score for the day
*/
@Nullable
protected Score get(long timestamp)
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return null;
compute(oldestRep.timestamp, timestamp);
return select().where("timestamp = ?", timestamp).executeSingle();
}
/**
* Returns the value of the score for a given day.
*
* @param timestamp the timestamp of a day
* @return score for that day
*/
public int getValue(long timestamp)
{
computeAll();
String[] args = { habit.getId().toString(), Long.toString(timestamp) };
return SQLiteUtils.intQuery("select score from Score where habit = ? and timestamp = ?", args);
}
/**
* Returns the values of all the scores, from day of the first repetition until today, grouped
* in chunks of specified size.
*
* If the group size is one, then the value of each score is returned individually. If the group
* is, for example, seven, then the days are grouped in groups of seven consecutive days.
*
* The values are returned in an array of integers, with one entry for each group of days in the
* interval. This value corresponds to the average of the scores for the days inside the group.
* The first entry corresponds to the ending of the interval (that is, the most recent group of
* days). The last entry corresponds to the beginning of the interval. As usual, the time of the
* day for the timestamps should be midnight (UTC). The endpoints of the interval are included.
*
* The values are returned in an integer array. There is one entry for each day inside the
* interval. The first entry corresponds to today, while the last entry corresponds to the
* day of the oldest repetition.
* Returns the values of all the scores, from day of the first repetition
* until today, grouped in chunks of specified size.
* <p>
* If the group size is one, then the value of each score is returned
* individually. If the group is, for example, seven, then the days are
* grouped in groups of seven consecutive days.
* <p>
* The values are returned in an array of integers, with one entry for each
* group of days in the interval. This value corresponds to the average of
* the scores for the days inside the group. The first entry corresponds to
* the ending of the interval (that is, the most recent group of days). The
* last entry corresponds to the beginning of the interval. As usual, the
* time of the day for the timestamps should be midnight (UTC). The
* endpoints of the interval are included.
* <p>
* The values are returned in an integer array. There is one entry for each
* day inside the interval. The first entry corresponds to today, while the
* last entry corresponds to the day of the oldest repetition.
*
* @param divisor the size of the groups
* @return array of values, with one entry for each group of days
@@ -237,64 +81,17 @@ public class ScoreList
@NonNull
public int[] getAllValues(long divisor)
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return new int[0];
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0];
long fromTimestamp = oldestRep.timestamp;
long fromTimestamp = oldestRep.getTimestamp();
long toTimestamp = DateUtils.getStartOfToday();
return getValues(fromTimestamp, toTimestamp, divisor);
}
/**
* Same as getAllValues(long), but using a specified interval.
*
* @param from beginning of the interval (included)
* @param to end of the interval (included)
* @param divisor size of the groups
* @return array of values, with one entry for each group of days
*/
@NonNull
protected int[] getValues(long from, long to, long divisor)
public ModelObservable getObservable()
{
compute(from, to);
divisor *= DateUtils.millisecondsInOneDay;
Long offset = to + divisor;
String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"group by time order by time desc";
String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(),
Long.toString(from), Long.toString(to) };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return new int[0];
int k = 0;
int[] scores = new int[cursor.getCount()];
do
{
scores[k++] = (int) cursor.getFloat(1);
}
while (cursor.moveToNext());
cursor.close();
return scores;
}
/**
* Returns the score for today.
*
* @return score for today
*/
@Nullable
protected Score getToday()
{
return get(DateUtils.getStartOfToday());
return observable;
}
/**
@@ -308,17 +105,21 @@ public class ScoreList
}
/**
* Returns the star status for today. The returned value is either Score.EMPTY_STAR,
* Score.HALF_STAR or Score.FULL_STAR.
* Returns the value of the score for a given day.
*
* @return star status for today
* @param timestamp the timestamp of a day
* @return score for that day
*/
public int getTodayStarStatus()
{
Score score = getToday();
if(score != null) return score.getStarStatus();
else return Score.EMPTY_STAR;
}
public abstract int getValue(long timestamp);
/**
* Marks all scores that have timestamp equal to or newer than the given
* timestamp as invalid. Any following getValue calls will trigger the
* scores to be recomputed.
*
* @param timestamp the oldest timestamp that should be invalidated
*/
public abstract void invalidateNewerThan(long timestamp);
public void writeCSV(Writer out) throws IOException
{
@@ -326,23 +127,116 @@ public class ScoreList
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
String query = "select timestamp, score from score where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
String query =
"select timestamp, score from score where habit = ? order by timestamp";
String params[] = {habit.getId().toString()};
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
if (!cursor.moveToFirst()) return;
do
{
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
String score = String.format("%.4f", ((float) cursor.getInt(1)) / Score.MAX_VALUE);
String score = String.format("%.4f",
((float) cursor.getInt(1)) / Score.MAX_VALUE);
out.write(String.format("%s,%s\n", timestamp, score));
} while(cursor.moveToNext());
} while (cursor.moveToNext());
cursor.close();
out.close();
}
protected abstract void add(List<Score> scores);
/**
* Computes and saves the scores that are missing inside a given time
* interval.
* <p>
* Scores that have already been computed are skipped, therefore there is no
* harm in calling this function more times, or with larger intervals, than
* strictly needed. The endpoints of the interval are included.
* <p>
* This function assumes that there are no gaps on the scores. That is, if
* the newest score has timestamp t, then every score with timestamp lower
* than t has already been computed.
*
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the time interval
*/
protected void compute(long from, long to)
{
final long day = DateUtils.millisecondsInOneDay;
final double freq = ((double) habit.getFreqNum()) / habit.getFreqDen();
int newestValue = 0;
long newestTimestamp = 0;
Score newest = getNewestComputed();
if(newest != null)
{
newestValue = newest.getValue();
newestTimestamp = newest.getTimestamp();
}
if (newestTimestamp > 0) from = newestTimestamp + day;
final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to);
final long beginning = from;
int lastScore = newestValue;
List<Score> scores = new LinkedList<>();
for (int i = 0; i < checkmarkValues.length; i++)
{
int value = checkmarkValues[checkmarkValues.length - i - 1];
lastScore = Score.compute(freq, lastScore, value);
scores.add(new Score(habit, beginning + day * i, lastScore));
}
add(scores);
}
/**
* Computes and saves the scores that are missing since the first repetition
* of the habit.
*/
protected void computeAll()
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return;
long toTimestamp = DateUtils.getStartOfToday();
compute(oldestRep.getTimestamp(), toTimestamp);
}
/**
* Returns the score for a certain day.
*
* @param timestamp the timestamp for the day
* @return the score for the day
*/
protected abstract Score get(long timestamp);
/**
* Returns the most recent score that was already computed.
* <p>
* If no score has been computed yet, returns null.
*
* @return the newest score computed, or null if none exist
*/
@Nullable
protected abstract Score getNewestComputed();
/**
* Same as getAllValues(long), but using a specified interval.
*
* @param from beginning of the interval (included)
* @param to end of the interval (included)
* @param divisor size of the groups
* @return array of values, with one entry for each group of days
*/
protected abstract int[] getValues(long from, long to, long divisor);
}

View File

@@ -19,20 +19,63 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.isoron.uhabits.utils.DateUtils;
public class Streak extends Model
public class Streak
{
@Column(name = "habit")
public Habit habit;
private Habit habit;
@Column(name = "start")
public Long start;
private long start;
@Column(name = "end")
public Long end;
private long end;
@Column(name = "length")
public Long length;
public Streak(Habit habit, long start, long end)
{
this.habit = habit;
this.start = start;
this.end = end;
}
public int compareLonger(Streak other)
{
if (this.getLength() != other.getLength())
return Long.signum(this.getLength() - other.getLength());
return Long.signum(this.getEnd() - other.getEnd());
}
public int compareNewer(Streak other)
{
return Long.signum(this.getEnd() - other.getEnd());
}
public long getEnd()
{
return end;
}
public Habit getHabit()
{
return habit;
}
public long getLength()
{
return (end - start) / DateUtils.millisecondsInOneDay + 1;
}
public long getStart()
{
return start;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("start", start)
.append("end", end)
.toString();
}
}

View File

@@ -19,100 +19,123 @@
package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class StreakList
/**
* The collection of {@link Streak}s that belong to a habit.
* <p>
* This list is populated automatically from the list of repetitions.
*/
public abstract class StreakList
{
private Habit habit;
public ModelObservable observable = new ModelObservable();
protected final Habit habit;
public StreakList(Habit habit)
protected ModelObservable observable;
protected StreakList(Habit habit)
{
this.habit = habit;
observable = new ModelObservable();
}
public List<Streak> getAll(int limit)
public abstract List<Streak> getAll();
public List<Streak> getBest(int limit)
{
rebuild();
String query = "select * from (select * from streak where habit=? " +
"order by end <> ?, length desc, end desc limit ?) order by end desc";
String params[] = {habit.getId().toString(), Long.toString(DateUtils.getStartOfToday()),
Integer.toString(limit)};
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst())
{
cursor.close();
return new LinkedList<>();
}
List<Streak> streaks = new LinkedList<>();
do
{
Streak s = Streak.load(Streak.class, cursor.getInt(0));
streaks.add(s);
}
while (cursor.moveToNext());
cursor.close();
List<Streak> streaks = getAll();
Collections.sort(streaks, (s1, s2) -> s2.compareLonger(s1));
streaks = streaks.subList(0, Math.min(streaks.size(), limit));
Collections.sort(streaks, (s1, s2) -> s2.compareNewer(s1));
return streaks;
}
public Streak getNewest()
public abstract Streak getNewestComputed();
public ModelObservable getObservable()
{
return new Select().from(Streak.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.limit(1)
.executeSingle();
return observable;
}
public abstract void invalidateNewerThan(long timestamp);
public void rebuild()
{
InterfaceUtils.throwIfMainThread();
long beginning;
long today = DateUtils.getStartOfToday();
Long beginning = findBeginning();
if (beginning == null || beginning > today) return;
int checks[] = habit.getCheckmarks().getValues(beginning, today);
List<Streak> streaks = checkmarksToStreaks(beginning, checks);
removeNewestComputed();
insert(streaks);
}
/**
* Converts a list of checkmark values to a list of streaks.
*
* @param beginning the timestamp corresponding to the first checkmark
* value.
* @param checks the checkmarks values, ordered by decreasing timestamp.
* @return the list of streaks.
*/
@NonNull
protected List<Streak> checkmarksToStreaks(Long beginning, int[] checks)
{
ArrayList<Long> transitions = getTransitions(beginning, checks);
List<Streak> streaks = new LinkedList<>();
for (int i = 0; i < transitions.size(); i += 2)
{
long start = transitions.get(i);
long end = transitions.get(i + 1);
streaks.add(new Streak(habit, start, end));
}
return streaks;
}
/**
* Finds the place where we should start when recomputing the streaks.
*
* @return
*/
@Nullable
protected Long findBeginning()
{
Streak newestStreak = getNewestComputed();
if (newestStreak != null) return newestStreak.getStart();
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep != null) return oldestRep.getTimestamp();
return null;
}
/**
* Returns the timestamps where there was a transition from performing a
* habit to not performing a habit, and vice-versa.
*
* @param beginning the timestamp for the first checkmark
* @param checks the checkmarks, ordered by decresing timestamp
* @return the list of transitions
*/
@NonNull
protected ArrayList<Long> getTransitions(Long beginning, int[] checks)
{
long day = DateUtils.millisecondsInOneDay;
Streak newestStreak = getNewest();
if (newestStreak != null)
{
beginning = newestStreak.start;
}
else
{
Repetition oldestRep = habit.repetitions.getOldest();
if (oldestRep == null) return;
beginning = oldestRep.timestamp;
}
if (beginning > today) return;
int checks[] = habit.checkmarks.getValues(beginning, today);
ArrayList<Long> list = new ArrayList<>();
long current = beginning;
ArrayList<Long> list = new ArrayList<>();
list.add(current);
for (int i = 1; i < checks.length; i++)
@@ -126,38 +149,10 @@ public class StreakList
if (list.size() % 2 == 1) list.add(current);
ActiveAndroid.beginTransaction();
if(newestStreak != null) newestStreak.delete();
try
{
for (int i = 0; i < list.size(); i += 2)
{
Streak streak = new Streak();
streak.habit = habit;
streak.start = list.get(i);
streak.end = list.get(i + 1);
streak.length = (streak.end - streak.start) / day + 1;
streak.save();
}
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
return list;
}
protected abstract void insert(List<Streak> streaks);
public void deleteNewerThan(long timestamp)
{
new Delete().from(Streak.class)
.where("habit = ?", habit.getId())
.and("end >= ?", timestamp - DateUtils.millisecondsInOneDay)
.execute();
observable.notifyListeners();
}
protected abstract void removeNewestComputed();
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Collections;
import java.util.LinkedList;
/**
* In-memory implementation of {@link CheckmarkList}.
*/
public class MemoryCheckmarkList extends CheckmarkList
{
LinkedList<Checkmark> list;
public MemoryCheckmarkList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public int[] getValues(long from, long to)
{
compute(from, to);
if (from > to) return new int[0];
int length = (int) ((to - from) / DateUtils.millisecondsInOneDay + 1);
int values[] = new int[length];
int k = 0;
for (Checkmark c : list)
if(c.getTimestamp() >= from && c.getTimestamp() <= to)
values[k++] = c.getValue();
return values;
}
@Override
public void invalidateNewerThan(long timestamp)
{
LinkedList<Checkmark> invalid = new LinkedList<>();
for (Checkmark c : list)
if (c.getTimestamp() >= timestamp) invalid.add(c);
list.removeAll(invalid);
}
@Override
protected Checkmark getNewest()
{
long newestTimestamp = 0;
Checkmark newestCheck = null;
for (Checkmark c : list)
{
if (c.getTimestamp() > newestTimestamp)
{
newestCheck = c;
newestTimestamp = c.getTimestamp();
}
}
return newestCheck;
}
@Override
protected void insert(long[] timestamps, int[] values)
{
for (int i = 0; i < timestamps.length; i++)
{
long t = timestamps[i];
int v = values[i];
list.add(new Checkmark(habit, t, v));
}
Collections.sort(list,
(c1, c2) -> (int) (c2.getTimestamp() - c1.getTimestamp()));
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.LinkedList;
import java.util.List;
/**
* In-memory implementation of {@link HabitList}.
*/
public class MemoryHabitList extends HabitList
{
@NonNull
private LinkedList<Habit> list;
public MemoryHabitList()
{
list = new LinkedList<>();
}
@Override
public void add(Habit habit)
{
list.addLast(habit);
}
@Override
public int count()
{
int count = 0;
for (Habit h : list) if (!h.isArchived()) count++;
return count;
}
@Override
public int countWithArchived()
{
return list.size();
}
@Override
public Habit getById(long id)
{
for (Habit h : list) if (h.getId() == id) return h;
return null;
}
@NonNull
@Override
public List<Habit> getAll(boolean includeArchive)
{
if (includeArchive) return new LinkedList<>(list);
return getFiltered(habit -> !habit.isArchived());
}
@Nullable
@Override
public Habit getByPosition(int position)
{
return list.get(position);
}
@Override
public int indexOf(Habit h)
{
return list.indexOf(h);
}
@Override
public void remove(@NonNull Habit habit)
{
list.remove(habit);
}
@Override
public void reorder(Habit from, Habit to)
{
int toPos = indexOf(to);
list.remove(from);
list.add(toPos, from);
}
@Override
public void update(List<Habit> habits)
{
// NOP
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ModelFactory;
import org.isoron.uhabits.models.RepetitionList;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.models.StreakList;
public class MemoryModelFactory implements ModelFactory
{
@Override
public RepetitionList buidRepetitionList(Habit habit)
{
return new MemoryRepetitionList(habit);
}
@Override
public HabitList buildHabitList()
{
return new MemoryHabitList();
}
@Override
public CheckmarkList buildCheckmarkList(Habit habit)
{
return new MemoryCheckmarkList(habit);
}
@Override
public ScoreList buildScoreList(Habit habit)
{
return null;
}
@Override
public StreakList buildStreakList(Habit habit)
{
return new MemoryStreakList(habit);
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.RepetitionList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* In-memory implementation of {@link RepetitionList}.
*/
public class MemoryRepetitionList extends RepetitionList
{
LinkedList<Repetition> list;
public MemoryRepetitionList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public void add(Repetition repetition)
{
list.add(repetition);
observable.notifyListeners();
}
@Override
public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp)
{
LinkedList<Repetition> filtered = new LinkedList<>();
for (Repetition r : list)
{
long t = r.getTimestamp();
if (t >= fromTimestamp && t <= toTimestamp) filtered.add(r);
}
Collections.sort(filtered,
(r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp()));
return filtered;
}
@Nullable
@Override
public Repetition getByTimestamp(long timestamp)
{
for (Repetition r : list)
if (r.getTimestamp() == timestamp) return r;
return null;
}
@Nullable
@Override
public Repetition getOldest()
{
long oldestTime = Long.MAX_VALUE;
Repetition oldestRep = null;
for (Repetition rep : list)
{
if (rep.getTimestamp() < oldestTime)
{
oldestRep = rep;
oldestTime = rep.getTimestamp();
}
}
return oldestRep;
}
@Override
public void remove(@NonNull Repetition repetition)
{
list.remove(repetition);
observable.notifyListeners();
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
import org.isoron.uhabits.models.StreakList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class MemoryStreakList extends StreakList
{
LinkedList<Streak> list;
public MemoryStreakList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public Streak getNewestComputed()
{
Streak newest = null;
for(Streak s : list)
if(newest == null || s.getEnd() > newest.getEnd())
newest = s;
return newest;
}
@Override
public void invalidateNewerThan(long timestamp)
{
LinkedList<Streak> discard = new LinkedList<>();
for(Streak s : list)
if(s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay)
discard.add(s);
list.removeAll(discard);
observable.notifyListeners();
}
@Override
protected void insert(List<Streak> streaks)
{
list.addAll(streaks);
Collections.sort(list, (s1, s2) -> s2.compareNewer(s1));
}
@Override
protected void removeNewestComputed()
{
Streak newest = getNewestComputed();
if(newest != null) list.remove(newest);
}
@Override
public List<Streak> getAll()
{
rebuild();
return new LinkedList<>(list);
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides in-memory implementation of core models.
*/
package org.isoron.uhabits.models.memory;

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public Licenses along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides core models classes, such as {@link org.isoron.uhabits.models.Habit}
* and {@link org.isoron.uhabits.models.Repetition}.
*/
package org.isoron.uhabits.models;

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
/**
* The SQLite database record corresponding to a {@link Checkmark}.
*/
@Table(name = "Checkmarks")
public class CheckmarkRecord extends Model
{
/**
* The habit to which this checkmark belongs.
*/
@Column(name = "habit")
public HabitRecord habit;
/**
* Timestamp of the day to which this checkmark corresponds. Time of the day
* must be midnight (UTC).
*/
@Column(name = "timestamp")
public Long timestamp;
/**
* Indicates whether there is a repetition at the given timestamp or not,
* and whether the repetition was expected. Assumes one of the values
* UNCHECKED, CHECKED_EXPLICITLY or CHECKED_IMPLICITLY.
*/
@Column(name = "value")
public Integer value;
public Checkmark toCheckmark()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Checkmark(h, timestamp, value);
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Delete;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DatabaseUtils;
/**
* The SQLite database record corresponding to a {@link Habit}.
*/
@Table(name = "Habits")
public class HabitRecord extends Model
{
public static final String HABIT_URI_FORMAT =
"content://org.isoron.uhabits/habit/%d";
@Column(name = "name")
public String name;
@Column(name = "description")
public String description;
@Column(name = "freq_num")
public Integer freqNum;
@Column(name = "freq_den")
public Integer freqDen;
@Column(name = "color")
public Integer color;
@Column(name = "position")
public Integer position;
@Nullable
@Column(name = "reminder_hour")
public Integer reminderHour;
@Nullable
@Column(name = "reminder_min")
public Integer reminderMin;
@NonNull
@Column(name = "reminder_days")
public Integer reminderDays;
@Column(name = "highlight")
public Integer highlight;
@Column(name = "archived")
public Integer archived;
public HabitRecord()
{
}
@Nullable
public static HabitRecord get(Long id)
{
return HabitRecord.load(HabitRecord.class, id);
}
/**
* Changes the id of a habit on the database.
*
* @param oldId the original id
* @param newId the new 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));
}
/**
* Deletes the habit and all data associated to it, including checkmarks,
* repetitions and scores.
*/
public void cascadeDelete()
{
Long id = getId();
DatabaseUtils.executeAsTransaction(() -> {
new Delete()
.from(CheckmarkRecord.class)
.where("habit = ?", id)
.execute();
new Delete()
.from(RepetitionRecord.class)
.where("habit = ?", id)
.execute();
new Delete()
.from(ScoreRecord.class)
.where("habit = ?", id)
.execute();
new Delete()
.from(StreakRecord.class)
.where("habit = ?", id)
.execute();
delete();
});
}
public void copyFrom(Habit model)
{
this.name = model.getName();
this.description = model.getDescription();
this.freqNum = model.getFreqNum();
this.freqDen = model.getFreqDen();
this.color = model.getColor();
this.reminderHour = model.getReminderHour();
this.reminderMin = model.getReminderMin();
this.reminderDays = model.getReminderDays();
this.highlight = model.getHighlight();
this.archived = model.getArchived();
}
public void copyTo(Habit habit)
{
habit.setName(this.name);
habit.setDescription(this.description);
habit.setFreqNum(this.freqNum);
habit.setFreqDen(this.freqDen);
habit.setColor(this.color);
habit.setReminderHour(this.reminderHour);
habit.setReminderMin(this.reminderMin);
habit.setReminderDays(this.reminderDays);
habit.setHighlight(this.highlight);
habit.setArchived(this.archived);
habit.setId(this.getId());
}
/**
* Saves the habit on the database, and assigns the specified id to it.
*
* @param id the id that the habit should receive
*/
public void save(long id)
{
save();
updateId(getId(), id);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
/**
* The SQLite database record corresponding to a {@link Repetition}.
*/
@Table(name = "Repetitions")
public class RepetitionRecord extends Model
{
@Column(name = "habit")
public HabitRecord habit;
@Column(name = "timestamp")
public Long timestamp;
public void copyFrom(Repetition repetition)
{
habit = HabitRecord.get(repetition.getHabit().getId());
timestamp = repetition.getTimestamp();
}
public static RepetitionRecord get(Long id)
{
return RepetitionRecord.load(RepetitionRecord.class, id);
}
public Repetition toRepetition()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Repetition(h, timestamp);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ModelFactory;
import org.isoron.uhabits.models.RepetitionList;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.models.StreakList;
/**
* Factory that provides models backed by an SQLite database.
*/
public class SQLModelFactory implements ModelFactory
{
@Override
public RepetitionList buidRepetitionList(Habit habit)
{
return new SQLiteRepetitionList(habit);
}
@Override
public CheckmarkList buildCheckmarkList(Habit habit)
{
return new SQLiteCheckmarkList(habit);
}
@Override
public HabitList buildHabitList()
{
return new SQLiteHabitList();
}
@Override
public ScoreList buildScoreList(Habit habit)
{
return new SQLiteScoreList(habit);
}
@Override
public StreakList buildStreakList(Habit habit)
{
return new SQLiteStreakList(habit);
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
/**
* Implementation of a {@link CheckmarkList} that is backed by SQLite.
*/
public class SQLiteCheckmarkList extends CheckmarkList
{
public SQLiteCheckmarkList(Habit habit)
{
super(habit);
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
observable.notifyListeners();
}
@Override
@NonNull
public int[] getValues(long fromTimestamp, long toTimestamp)
{
compute(fromTimestamp, toTimestamp);
if (fromTimestamp > toTimestamp) return new int[0];
String query = "select value, timestamp from Checkmarks where " +
"habit = ? and timestamp >= ? and timestamp <= ?";
SQLiteDatabase db = Cache.openDatabase();
String args[] = {
habit.getId().toString(),
Long.toString(fromTimestamp),
Long.toString(toTimestamp)
};
Cursor cursor = db.rawQuery(query, args);
long day = DateUtils.millisecondsInOneDay;
int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1;
int[] checks = new int[nDays];
if (cursor.moveToFirst())
{
do
{
long timestamp = cursor.getLong(1);
int offset = (int) ((timestamp - fromTimestamp) / day);
checks[nDays - offset - 1] = cursor.getInt(0);
} while (cursor.moveToNext());
}
cursor.close();
return checks;
}
@Override
@Nullable
protected Checkmark getNewest()
{
CheckmarkRecord record = new Select()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp desc")
.limit(1)
.executeSingle();
return record.toCheckmark();
}
@Override
protected void insert(long timestamps[], int values[])
{
String query =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (int i = 0; i < timestamps.length; i++)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamps[i]);
statement.bindLong(3, values[i]);
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.query.Update;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* Implementation of a {@link HabitList} that is backed by SQLite.
*/
public class SQLiteHabitList extends HabitList
{
private static SQLiteHabitList instance;
private HashMap<Long, Habit> cache;
public SQLiteHabitList()
{
cache = new HashMap<>();
}
public static SQLiteHabitList getInstance()
{
if (instance == null) instance = new SQLiteHabitList();
return instance;
}
@Override
public void add(Habit habit)
{
if(cache.containsValue(habit))
throw new RuntimeException("habit already in cache");
HabitRecord record = new HabitRecord();
record.copyFrom(habit);
record.position = countWithArchived();
Long id = habit.getId();
if(id == null) id = record.save();
else record.save(id);
habit.setId(id);
cache.put(id, habit);
}
@Override
public int count()
{
return select().count();
}
@Override
public int countWithArchived()
{
return selectWithArchived().count();
}
@Override
@NonNull
public List<Habit> getAll(boolean includeArchive)
{
List<HabitRecord> recordList;
if (includeArchive) recordList = selectWithArchived().execute();
else recordList = select().execute();
List<Habit> habits = new LinkedList<>();
for (HabitRecord record : recordList)
{
Habit habit = getById(record.getId());
if (habit == null)
throw new RuntimeException("habit not in database");
habits.add(habit);
}
return habits;
}
@Override
@Nullable
public Habit getById(long id)
{
if (!cache.containsKey(id))
{
HabitRecord record = HabitRecord.get(id);
if (record == null) return null;
Habit habit = new Habit();
record.copyTo(habit);
cache.put(id, habit);
}
return cache.get(id);
}
@Override
@Nullable
public Habit getByPosition(int position)
{
HabitRecord record = selectWithArchived()
.where("position = ?", position)
.executeSingle();
return getById(record.getId());
}
@Override
public int indexOf(Habit h)
{
HabitRecord record = HabitRecord.get(h.getId());
if (record == null) return -1;
return record.position;
}
@Deprecated
public void rebuildOrder()
{
List<Habit> habits = getAll(true);
int i = 0;
for (Habit h : habits)
{
HabitRecord record = HabitRecord.get(h.getId());
if (record == null)
throw new RuntimeException("habit not in database");
record.position = i++;
record.save();
}
update(habits);
}
@Override
public void remove(@NonNull Habit habit)
{
if (!cache.containsKey(habit.getId()))
throw new RuntimeException("habit not in cache");
cache.remove(habit.getId());
HabitRecord record = HabitRecord.get(habit.getId());
if (record == null) throw new RuntimeException("habit not in database");
record.cascadeDelete();
rebuildOrder();
}
@Override
public void reorder(Habit from, Habit to)
{
if (from == to) return;
Integer toPos = indexOf(to);
Integer fromPos = indexOf(from);
if (toPos < fromPos)
{
new Update(HabitRecord.class)
.set("position = position + 1")
.where("position >= ? and position < ?", toPos, fromPos)
.execute();
}
else
{
new Update(HabitRecord.class)
.set("position = position - 1")
.where("position > ? and position <= ?", fromPos, toPos)
.execute();
}
HabitRecord record = HabitRecord.get(from.getId());
if (record == null) throw new RuntimeException("habit not in database");
record.position = toPos;
record.save();
update(from);
}
@Override
public void update(List<Habit> habits)
{
for (Habit h : habits)
{
HabitRecord record = HabitRecord.get(h.getId());
if (record == null)
throw new RuntimeException("habit not in database");
record.copyFrom(h);
record.save();
}
}
@NonNull
private From select()
{
return new Select()
.from(HabitRecord.class)
.where("archived = 0")
.orderBy("position");
}
@NonNull
private From selectWithArchived()
{
return new Select().from(HabitRecord.class).orderBy("position");
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.RepetitionList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* Implementation of a {@link RepetitionList} that is backed by SQLite.
*/
public class SQLiteRepetitionList extends RepetitionList
{
HashMap<Long, Repetition> cache;
public SQLiteRepetitionList(@NonNull Habit habit)
{
super(habit);
this.cache = new HashMap<>();
}
@Override
public void add(Repetition rep)
{
RepetitionRecord record = new RepetitionRecord();
record.copyFrom(rep);
long id = record.save();
cache.put(id, rep);
observable.notifyListeners();
}
@Override
public List<Repetition> getByInterval(long timeFrom, long timeTo)
{
return getFromRecord(selectFromTo(timeFrom, timeTo).execute());
}
@Override
public Repetition getByTimestamp(long timestamp)
{
RepetitionRecord record =
select().where("timestamp = ?", timestamp).executeSingle();
return getFromRecord(record);
}
@Override
public Repetition getOldest()
{
RepetitionRecord record = select().limit(1).executeSingle();
return getFromRecord(record);
}
@Override
public void remove(@NonNull Repetition repetition)
{
new Delete()
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp = ?", repetition.getTimestamp())
.execute();
observable.notifyListeners();
}
@NonNull
private List<Repetition> getFromRecord(
@Nullable List<RepetitionRecord> records)
{
List<Repetition> reps = new LinkedList<>();
if (records == null) return reps;
for (RepetitionRecord record : records)
{
Repetition rep = getFromRecord(record);
reps.add(rep);
}
return reps;
}
@Nullable
private Repetition getFromRecord(@Nullable RepetitionRecord record)
{
if (record == null) return null;
Long id = record.getId();
if (!cache.containsKey(id))
{
Repetition repetition = record.toRepetition();
cache.put(id, repetition);
}
return cache.get(id);
}
@NonNull
private From select()
{
return new Select()
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp");
}
@NonNull
private From selectFromTo(long timeFrom, long timeTo)
{
return select()
.and("timestamp >= ?", timeFrom)
.and("timestamp <= ?", timeTo);
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.List;
/**
* Implementation of a ScoreList that is backed by SQLite.
*/
public class SQLiteScoreList extends ScoreList
{
/**
* Constructs a new ScoreList associated with the given habit.
*
* @param habit the habit this list should be associated with
*/
public SQLiteScoreList(@NonNull Habit habit)
{
super(habit);
}
@Override
public int getValue(long timestamp)
{
computeAll();
String[] args = {habit.getId().toString(), Long.toString(timestamp)};
return SQLiteUtils.intQuery(
"select score from Score where habit = ? and timestamp = ?", args);
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
}
@Nullable
@Override
protected Score getNewestComputed()
{
ScoreRecord record = select().limit(1).executeSingle();
return record.toScore();
}
@Override
@Nullable
protected Score get(long timestamp)
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return null;
compute(oldestRep.getTimestamp(), timestamp);
ScoreRecord record =
select().where("timestamp = ?", timestamp).executeSingle();
return record.toScore();
}
@Override
@NonNull
protected int[] getValues(long from, long to, long divisor)
{
compute(from, to);
divisor *= DateUtils.millisecondsInOneDay;
Long offset = to + divisor;
String query =
"select ((timestamp - ?) / ?) as time, avg(score) from Score " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"group by time order by time desc";
String params[] = {
offset.toString(),
Long.toString(divisor),
habit.getId().toString(),
Long.toString(from),
Long.toString(to)
};
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if (!cursor.moveToFirst()) return new int[0];
int k = 0;
int[] scores = new int[cursor.getCount()];
do
{
scores[k++] = (int) cursor.getFloat(1);
} while (cursor.moveToNext());
cursor.close();
return scores;
}
@Override
protected void add(List<Score> scores)
{
String query =
"insert into Score(habit, timestamp, score) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (Score s : scores)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, s.getTimestamp());
statement.bindLong(3, s.getValue());
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
protected From select()
{
return new Select()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc");
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
import org.isoron.uhabits.models.StreakList;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import java.util.LinkedList;
import java.util.List;
/**
* Implementation of a StreakList that is backed by SQLite.
*/
public class SQLiteStreakList extends StreakList
{
public SQLiteStreakList(Habit habit)
{
super(habit);
}
@Override
public List<Streak> getAll()
{
rebuild();
List<StreakRecord> records = new Select()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.execute();
return recordsToStreaks(records);
}
@Override
public Streak getNewestComputed()
{
rebuild();
return getNewestRecord().toStreak();
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.and("end >= ?", timestamp - DateUtils.millisecondsInOneDay)
.execute();
observable.notifyListeners();
}
private StreakRecord getNewestRecord()
{
return new Select()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.limit(1)
.executeSingle();
}
@Override
protected void insert(List<Streak> streaks)
{
DatabaseUtils.executeAsTransaction(() -> {
for (Streak streak : streaks)
{
StreakRecord record = new StreakRecord();
record.copyFrom(streak);
record.save();
}
});
}
private List<Streak> recordsToStreaks(List<StreakRecord> records)
{
LinkedList<Streak> streaks = new LinkedList<>();
for (StreakRecord record : records)
streaks.add(record.toStreak());
return streaks;
}
@Override
protected void removeNewestComputed()
{
StreakRecord newestStreak = getNewestRecord();
if (newestStreak != null) newestStreak.delete();
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
/**
* The SQLite database record corresponding to a Score.
*/
@Table(name = "Score")
public class ScoreRecord extends Model
{
/**
* Habit to which this score belongs to.
*/
@Column(name = "habit")
public HabitRecord habit;
/**
* Timestamp of the day to which this score applies. Time of day should be
* midnight (UTC).
*/
@Column(name = "timestamp")
public Long timestamp;
/**
* Value of the score.
*/
@Column(name = "score")
public Integer score;
/**
* Constructs and returns a {@link Score} based on this record's data.
*
* @return a {@link Score} with this record's data
*/
public Score toScore()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Score(h, timestamp, score);
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
/**
* The SQLite database record corresponding to a Streak.
*/
@Table(name = "Streak")
public class StreakRecord extends Model
{
@Column(name = "habit")
public HabitRecord habit;
@Column(name = "start")
public Long start;
@Column(name = "end")
public Long end;
@Column(name = "length")
public Long length;
public static StreakRecord get(Long id)
{
return StreakRecord.load(StreakRecord.class, id);
}
public void copyFrom(Streak streak)
{
habit = HabitRecord.get(streak.getHabit().getId());
start = streak.getStart();
end = streak.getEnd();
length = streak.getLength();
}
public Streak toStreak()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Streak(h, start, end);
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides SQLite implementations of the core models.
*/
package org.isoron.uhabits.models.sqlite;

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides classes for the Loop Habit Tracker app.
*/
package org.isoron.uhabits;

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides async tasks for useful operations such as {@link
* org.isoron.uhabits.tasks.ExportCSVTask}.
*/
package org.isoron.uhabits.tasks;

View File

@@ -25,6 +25,8 @@ import android.support.annotation.NonNull;
import android.view.WindowManager;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.FileUtils;
@@ -38,13 +40,19 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import javax.inject.Inject;
public class BaseSystem
{
private Context context;
@Inject
HabitList habitList;
public BaseSystem(Context context)
{
this.context = context;
HabitsApplication.getComponent().inject(this);
}
public String getLogcat() throws IOException
@@ -146,7 +154,7 @@ public class BaseSystem
@Override
protected void doInBackground()
{
ReminderUtils.createReminderAlarms(context);
ReminderUtils.createReminderAlarms(context, habitList);
}
}.execute();
}

View File

@@ -18,6 +18,6 @@
*/
/**
* Contains classes for AboutActivity
* Provides activity that shows information about the app.
*/
package org.isoron.uhabits.ui.about;

View File

@@ -85,8 +85,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
if (position < 0 || position > 4) throw new IllegalArgumentException();
int freqNums[] = {1, 1, 2, 5, 3};
int freqDens[] = {1, 7, 7, 7, 7};
modifiedHabit.freqNum = freqNums[position];
modifiedHabit.freqDen = freqDens[position];
modifiedHabit.setFreqNum(freqNums[position]);
modifiedHabit.setFreqDen(freqDens[position]);
helper.populateFrequencyFields(modifiedHabit);
}
@@ -95,12 +95,12 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt("color", modifiedHabit.color);
outState.putInt("color", modifiedHabit.getColor());
if (modifiedHabit.hasReminder())
{
outState.putInt("reminderMin", modifiedHabit.reminderMin);
outState.putInt("reminderHour", modifiedHabit.reminderHour);
outState.putInt("reminderDays", modifiedHabit.reminderDays);
outState.putInt("reminderMin", modifiedHabit.getReminderMin());
outState.putInt("reminderHour", modifiedHabit.getReminderHour());
outState.putInt("reminderDays", modifiedHabit.getReminderDays());
}
}
@@ -123,8 +123,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
if (modifiedHabit.hasReminder())
{
defaultHour = modifiedHabit.reminderHour;
defaultMin = modifiedHabit.reminderMin;
defaultHour = modifiedHabit.getReminderHour();
defaultMin = modifiedHabit.getReminderMin();
}
showTimePicker(defaultHour, defaultMin);
@@ -147,18 +147,19 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
WeekdayPickerDialog dialog = new WeekdayPickerDialog();
dialog.setListener(new OnWeekdaysPickedListener());
dialog.setSelectedDays(
DateUtils.unpackWeekdayList(modifiedHabit.reminderDays));
DateUtils.unpackWeekdayList(modifiedHabit.getReminderDays()));
dialog.show(getFragmentManager(), "weekdayPicker");
}
protected void restoreSavedInstance(@Nullable Bundle bundle)
{
if (bundle == null) return;
modifiedHabit.color = bundle.getInt("color", modifiedHabit.color);
modifiedHabit.reminderMin = bundle.getInt("reminderMin", -1);
modifiedHabit.reminderHour = bundle.getInt("reminderHour", -1);
modifiedHabit.reminderDays = bundle.getInt("reminderDays", -1);
if (modifiedHabit.reminderMin < 0) modifiedHabit.clearReminder();
modifiedHabit.setColor(
bundle.getInt("color", modifiedHabit.getColor()));
modifiedHabit.setReminderMin(bundle.getInt("reminderMin", -1));
modifiedHabit.setReminderHour(bundle.getInt("reminderHour", -1));
modifiedHabit.setReminderDays(bundle.getInt("reminderDays", -1));
if (modifiedHabit.getReminderMin() < 0) modifiedHabit.clearReminder();
}
protected abstract void saveHabit();
@@ -167,7 +168,7 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
void showColorPicker()
{
int androidColor =
ColorUtils.getColor(getContext(), modifiedHabit.color);
ColorUtils.getColor(getContext(), modifiedHabit.getColor());
ColorPickerDialog picker =
ColorPickerDialog.newInstance(R.string.color_picker_default_title,
@@ -196,7 +197,7 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
int paletteColor =
ColorUtils.colorToPaletteIndex(getActivity(), androidColor);
prefs.setDefaultHabitColor(paletteColor);
modifiedHabit.color = paletteColor;
modifiedHabit.setColor(paletteColor);
helper.populateColor(paletteColor);
}
}
@@ -214,9 +215,9 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
@Override
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
{
modifiedHabit.reminderHour = hour;
modifiedHabit.reminderMin = minute;
modifiedHabit.reminderDays = DateUtils.ALL_WEEK_DAYS;
modifiedHabit.setReminderHour(hour);
modifiedHabit.setReminderMin(minute);
modifiedHabit.setReminderDays(DateUtils.ALL_WEEK_DAYS);
helper.populateReminderFields(modifiedHabit);
}
}
@@ -229,8 +230,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
{
if (isSelectionEmpty(selectedDays)) Arrays.fill(selectedDays, true);
modifiedHabit.reminderDays =
DateUtils.packWeekdayList(selectedDays);
modifiedHabit.setReminderDays(
DateUtils.packWeekdayList(selectedDays));
helper.populateReminderFields(modifiedHabit);
}

View File

@@ -73,12 +73,12 @@ public class BaseDialogHelper
void parseFormIntoHabit(Habit habit)
{
habit.name = tvName.getText().toString().trim();
habit.description = tvDescription.getText().toString().trim();
habit.setName(tvName.getText().toString().trim());
habit.setDescription(tvDescription.getText().toString().trim());
String freqNum = tvFreqNum.getText().toString();
String freqDen = tvFreqDen.getText().toString();
if (!freqNum.isEmpty()) habit.freqNum = Integer.parseInt(freqNum);
if (!freqDen.isEmpty()) habit.freqDen = Integer.parseInt(freqDen);
if (!freqNum.isEmpty()) habit.setFreqNum(Integer.parseInt(freqNum));
if (!freqDen.isEmpty()) habit.setFreqDen(Integer.parseInt(freqDen));
}
void populateColor(int paletteColor)
@@ -89,10 +89,11 @@ public class BaseDialogHelper
protected void populateForm(final Habit habit)
{
if (habit.name != null) tvName.setText(habit.name);
if (habit.description != null) tvDescription.setText(habit.description);
if (habit.getName() != null) tvName.setText(habit.getName());
if (habit.getDescription() != null) tvDescription.setText(
habit.getDescription());
populateColor(habit.color);
populateColor(habit.getColor());
populateFrequencyFields(habit);
populateReminderFields(habit);
}
@@ -102,15 +103,15 @@ public class BaseDialogHelper
{
int quickSelectPosition = -1;
if (habit.freqNum.equals(habit.freqDen)) quickSelectPosition = 0;
if (habit.getFreqNum().equals(habit.getFreqDen())) quickSelectPosition = 0;
else if (habit.freqNum == 1 && habit.freqDen == 7)
else if (habit.getFreqNum() == 1 && habit.getFreqDen() == 7)
quickSelectPosition = 1;
else if (habit.freqNum == 2 && habit.freqDen == 7)
else if (habit.getFreqNum() == 2 && habit.getFreqDen() == 7)
quickSelectPosition = 2;
else if (habit.freqNum == 5 && habit.freqDen == 7)
else if (habit.getFreqNum() == 5 && habit.getFreqDen() == 7)
quickSelectPosition = 3;
if (quickSelectPosition >= 0)
@@ -118,8 +119,8 @@ public class BaseDialogHelper
else showCustomFrequency();
tvFreqNum.setText(habit.freqNum.toString());
tvFreqDen.setText(habit.freqDen.toString());
tvFreqNum.setText(habit.getFreqNum().toString());
tvFreqDen.setText(habit.getFreqDen().toString());
}
@SuppressWarnings("ConstantConditions")
@@ -133,12 +134,13 @@ public class BaseDialogHelper
}
String time =
DateUtils.formatTime(frag.getContext(), habit.reminderHour,
habit.reminderMin);
DateUtils.formatTime(frag.getContext(), habit.getReminderHour(),
habit.getReminderMin());
tvReminderTime.setText(time);
llReminderDays.setVisibility(View.VISIBLE);
boolean weekdays[] = DateUtils.unpackWeekdayList(habit.reminderDays);
boolean weekdays[] = DateUtils.unpackWeekdayList(
habit.getReminderDays());
tvReminderDays.setText(
DateUtils.formatWeekdayList(frag.getContext(), weekdays));
}
@@ -161,21 +163,21 @@ public class BaseDialogHelper
{
Boolean valid = true;
if (habit.name.length() == 0)
if (habit.getName().length() == 0)
{
tvName.setError(
frag.getString(R.string.validation_name_should_not_be_blank));
valid = false;
}
if (habit.freqNum <= 0)
if (habit.getFreqNum() <= 0)
{
tvFreqNum.setError(
frag.getString(R.string.validation_number_should_be_positive));
valid = false;
}
if (habit.freqNum > habit.freqDen)
if (habit.getFreqNum() > habit.getFreqDen())
{
tvFreqNum.setError(
frag.getString(R.string.validation_at_most_one_rep_per_day));

View File

@@ -19,6 +19,7 @@
package org.isoron.uhabits.ui.habits.edit;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CreateHabitCommand;
@@ -36,9 +37,10 @@ public class CreateHabitDialogFragment extends BaseDialogFragment
protected void initializeHabits()
{
modifiedHabit = new Habit();
modifiedHabit.freqNum = 1;
modifiedHabit.freqDen = 1;
modifiedHabit.color = prefs.getDefaultHabitColor(modifiedHabit.color);
modifiedHabit.setFreqNum(1);
modifiedHabit.setFreqDen(1);
modifiedHabit.setColor(
prefs.getDefaultHabitColor(modifiedHabit.getColor()));
}
protected void saveHabit()

View File

@@ -21,13 +21,20 @@ package org.isoron.uhabits.ui.habits.edit;
import android.os.Bundle;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import javax.inject.Inject;
public class EditHabitDialogFragment extends BaseDialogFragment
{
@Inject
HabitList habitList;
public static EditHabitDialogFragment newInstance(long habitId)
{
EditHabitDialogFragment frag = new EditHabitDialogFragment();
@@ -46,14 +53,18 @@ public class EditHabitDialogFragment extends BaseDialogFragment
@Override
protected void initializeHabits()
{
HabitsApplication.getComponent().inject(this);
Long habitId = (Long) getArguments().get("habitId");
if (habitId == null)
throw new IllegalArgumentException("habitId must be specified");
originalHabit = Habit.get(habitId);
modifiedHabit = new Habit(originalHabit);
originalHabit = habitList.getById(habitId);
modifiedHabit = new Habit();
modifiedHabit.copyFrom(originalHabit);
}
@Override
protected void saveHabit()
{
Command command = new EditHabitCommand(originalHabit, modifiedHabit);

View File

@@ -27,10 +27,14 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialogFragment;
import android.util.DisplayMetrics;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitHistoryView;
import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView;
import javax.inject.Inject;
public class HistoryEditorDialog extends AppCompatDialogFragment
implements DialogInterface.OnClickListener
@@ -41,16 +45,20 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
HabitHistoryView historyView;
@Inject
HabitList habitList;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
Context context = getActivity();
HabitsApplication.getComponent().inject(this);
historyView = new HabitHistoryView(context, null);
if (savedInstanceState != null)
{
long id = savedInstanceState.getLong("habit", -1);
if (id > 0) this.habit = Habit.get(id);
if (id > 0) this.habit = habitList.getById(id);
}
int padding =

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides dialogs for editing habits and related classes.
*/
package org.isoron.uhabits.ui.habits.edit;

View File

@@ -21,25 +21,33 @@ package org.isoron.uhabits.ui.habits.list;
import android.os.Bundle;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.BaseSystem;
import javax.inject.Inject;
/**
* Activity that allows the user to see and modify the list of habits.
*/
public class ListHabitsActivity extends BaseActivity
{
@Inject
HabitList habitList;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
HabitsApplication.getComponent().inject(this);
BaseSystem system = new BaseSystem(this);
ListHabitsScreen screen = new ListHabitsScreen(this);
ListHabitsController controller =
new ListHabitsController(screen, system);
new ListHabitsController(screen, system, habitList);
screen.setController(controller);
setScreen(screen);
controller.onStartup();
}

View File

@@ -26,6 +26,7 @@ import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.isoron.uhabits.tasks.ImportDataTask;
@@ -48,6 +49,9 @@ public class ListHabitsController
@NonNull
private final BaseSystem system;
@NonNull
private final HabitList habitList;
@Inject
Preferences prefs;
@@ -55,17 +59,19 @@ public class ListHabitsController
CommandRunner commandRunner;
public ListHabitsController(@NonNull ListHabitsScreen screen,
@NonNull BaseSystem system)
@NonNull BaseSystem system,
@NonNull HabitList habitList)
{
this.screen = screen;
this.system = system;
this.habitList = habitList;
HabitsApplication.getComponent().inject(this);
}
public void onExportCSV()
{
ExportCSVTask task =
new ExportCSVTask(Habit.getAll(true), screen.getProgressBar());
new ExportCSVTask(habitList.getAll(true), screen.getProgressBar());
task.setListener(filename -> {
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
@@ -92,7 +98,7 @@ public class ListHabitsController
@Override
public void onHabitReorder(@NonNull Habit from, @NonNull Habit to)
{
Habit.reorder(from, to);
habitList.reorder(from, to);
}
public void onImportData(File file)
@@ -133,7 +139,8 @@ public class ListHabitsController
try
{
system.dumpBugReportToFile();
} catch (IOException e)
}
catch (IOException e)
{
// ignored
}
@@ -146,7 +153,8 @@ public class ListHabitsController
String to = "dev@loophabits.org";
String subject = "Bug Report - Loop Habit Tracker";
screen.showSendEmailScreen(log, to, subject);
} catch (IOException e)
}
catch (IOException e)
{
e.printStackTrace();
screen.showMessage(R.string.bug_report_failed);

View File

@@ -115,7 +115,7 @@ public class ListHabitsScreen extends BaseScreen
public void showColorPicker(Habit habit, OnColorSelectedListener callback)
{
int color = ColorUtils.getColor(activity, habit.color);
int color = ColorUtils.getColor(activity, habit.getColor());
ColorPickerDialog picker =
ColorPickerDialog.newInstance(R.string.color_picker_default_title,

View File

@@ -18,6 +18,6 @@
*/
/**
* Contains controllers that are specific for ListHabitsActivity
* Provides controllers that are specific for {@link org.isoron.uhabits.ui.habits.list.ListHabitsActivity}.
*/
package org.isoron.uhabits.ui.habits.list.controllers;

View File

@@ -37,9 +37,10 @@ import java.util.List;
import javax.inject.Inject;
/**
* Provides data that backs a {@link HabitCardListView}. The data if fetched and
* cached by a {@link HabitCardListCache}. This adapter also holds a list of
* items that have been selected.
* Provides data that backs a {@link HabitCardListView}.
* <p>
* The data if fetched and cached by a {@link HabitCardListCache}. This adapter
* also holds a list of items that have been selected.
*/
public class HabitCardListAdapter extends BaseAdapter
implements HabitCardListCache.Listener
@@ -200,6 +201,12 @@ public class HabitCardListAdapter extends BaseAdapter
this.listView = listView;
}
public void setShowArchived(boolean showArchived)
{
cache.setIncludeArchived(showArchived);
cache.refreshAllHabits(true);
}
/**
* Selects or deselects the item at a given position.
*
@@ -213,10 +220,4 @@ public class HabitCardListAdapter extends BaseAdapter
else selected.remove(h);
notifyDataSetChanged();
}
public void setShowArchived(boolean showArchived)
{
cache.setIncludeArchived(showArchived);
cache.refreshAllHabits(true);
}
}

View File

@@ -26,6 +26,7 @@ import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.DateUtils;
@@ -63,6 +64,9 @@ public class HabitCardListCache implements CommandRunner.Listener
@Inject
CommandRunner commandRunner;
@Inject
HabitList allHabits;
public HabitCardListCache()
{
data = new CacheData();
@@ -148,7 +152,7 @@ public class HabitCardListCache implements CommandRunner.Listener
data.habitsList.remove(from);
data.habitsList.add(to, fromHabit);
Habit.reorder(fromHabit, toHabit);
allHabits.reorder(fromHabit, toHabit);
}
public void setCheckmarkCount(int checkmarkCount)
@@ -227,7 +231,7 @@ public class HabitCardListCache implements CommandRunner.Listener
public void fetchHabits()
{
habitsList = Habit.getAll(includeArchived);
habitsList = allHabits.getAll(includeArchived);
for (Habit h : habitsList)
habits.put(h.getId(), h);
}
@@ -272,9 +276,9 @@ public class HabitCardListCache implements CommandRunner.Listener
if (isCancelled()) return;
Long id = h.getId();
newData.scores.put(id, h.scores.getTodayValue());
newData.scores.put(id, h.getScores().getTodayValue());
newData.checkmarks.put(id,
h.checkmarks.getValues(dateFrom, dateTo));
h.getCheckmarks().getValues(dateFrom, dateTo));
publishProgress(current++, newData.habits.size());
}
@@ -316,12 +320,12 @@ public class HabitCardListCache implements CommandRunner.Listener
long dateFrom =
dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay;
Habit h = Habit.get(id);
Habit h = allHabits.getById(id);
if (h == null) return;
data.habits.put(id, h);
data.scores.put(id, h.scores.getTodayValue());
data.checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
data.scores.put(id, h.getScores().getTodayValue());
data.checkmarks.put(id, h.getCheckmarks().getValues(dateFrom, dateTo));
}
@Override

View File

@@ -18,6 +18,6 @@
*/
/**
* Contains model classes that are specific for ListHabitsActivity
* Provides models that are specific for {@link org.isoron.uhabits.ui.habits.list.ListHabitsActivity}.
*/
package org.isoron.uhabits.ui.habits.list.model;

View File

@@ -18,6 +18,6 @@
*/
/**
* Contains classes for ListHabitsActivity.
* Provides acitivity for listing habits and related classes.
*/
package org.isoron.uhabits.ui.habits.list;

View File

@@ -32,8 +32,8 @@ import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.ui.habits.show.views.RingView;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.views.RingView;
import java.util.Random;
@@ -98,7 +98,7 @@ public class HabitCardView extends FrameLayout
this.habit = habit;
int color = getActiveColor(habit);
label.setText(habit.name);
label.setText(habit.getName());
label.setTextColor(color);
scoreRing.setColor(color);
checkmarkPanel.setColor(color);
@@ -136,7 +136,7 @@ public class HabitCardView extends FrameLayout
{
int mediumContrastColor =
getStyledColor(context, R.attr.mediumContrastTextColor);
int activeColor = ColorUtils.getColor(context, habit.color);
int activeColor = ColorUtils.getColor(context, habit.getColor());
if (habit.isArchived()) activeColor = mediumContrastColor;
return activeColor;
@@ -173,7 +173,7 @@ public class HabitCardView extends FrameLayout
};
Random rand = new Random();
int color = ColorUtils.CSV_PALETTE[rand.nextInt(10)];
int color = ColorUtils.getAndroidTestColor(rand.nextInt(10));
int[] values = {
rand.nextInt(3),
rand.nextInt(3),

View File

@@ -24,10 +24,14 @@ import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.ui.BaseActivity;
import javax.inject.Inject;
/**
* Activity that allows the user to see more information about a single habit.
* Shows all the metadata for the habit, in addition to several charts.
@@ -36,13 +40,17 @@ public class ShowHabitActivity extends BaseActivity
{
private Habit habit;
@Inject
HabitList habitList;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
HabitsApplication.getComponent().inject(this);
Uri data = getIntent().getData();
habit = Habit.get(ContentUris.parseId(data));
habit = habitList.getById(ContentUris.parseId(data));
setContentView(R.layout.show_habit_activity);
// setupSupportActionBar(true);
@@ -56,7 +64,7 @@ public class ShowHabitActivity extends BaseActivity
ActionBar actionBar = getSupportActionBar();
if (actionBar == null) return;
actionBar.setTitle(habit.name);
actionBar.setTitle(habit.getName());
// setupActionBarColor(ColorUtils.getColor(this, habit.color));
}

View File

@@ -39,11 +39,11 @@ import org.isoron.uhabits.ui.habits.edit.EditHabitDialogFragment;
import org.isoron.uhabits.ui.habits.edit.HistoryEditorDialog;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.views.HabitFrequencyView;
import org.isoron.uhabits.views.HabitHistoryView;
import org.isoron.uhabits.views.HabitScoreView;
import org.isoron.uhabits.views.HabitStreakView;
import org.isoron.uhabits.ui.habits.show.views.HabitDataView;
import org.isoron.uhabits.ui.habits.show.views.HabitFrequencyView;
import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView;
import org.isoron.uhabits.ui.habits.show.views.HabitScoreView;
import org.isoron.uhabits.ui.habits.show.views.HabitStreakView;
import java.util.LinkedList;
import java.util.List;
@@ -212,13 +212,13 @@ public class ShowHabitFragment extends Fragment
public void onStart()
{
super.onStart();
habit.observable.addListener(this);
habit.getObservable().addListener(this);
}
@Override
public void onPause()
{
habit.observable.removeListener(this);
habit.getObservable().removeListener(this);
super.onPause();
}
@@ -234,9 +234,9 @@ public class ShowHabitFragment extends Fragment
long lastMonth = today - 30 * DateUtils.millisecondsInOneDay;
long lastYear = today - 365 * DateUtils.millisecondsInOneDay;
todayScore = (float) habit.scores.getTodayValue();
lastMonthScore = (float) habit.scores.getValue(lastMonth);
lastYearScore = (float) habit.scores.getValue(lastYear);
todayScore = (float) habit.getScores().getTodayValue();
lastMonthScore = (float) habit.getScores().getValue(lastMonth);
lastYearScore = (float) habit.getScores().getValue(lastYear);
}
@Override

View File

@@ -25,10 +25,10 @@ import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.ui.habits.show.views.RingView;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.views.RingView;
public class ShowHabitHelper
{
@@ -44,8 +44,8 @@ public class ShowHabitHelper
if (fragment.habit == null) return "";
Resources resources = fragment.getResources();
Integer freqNum = fragment.habit.freqNum;
Integer freqDen = fragment.habit.freqDen;
Integer freqNum = fragment.habit.getFreqNum();
Integer freqDen = fragment.habit.getFreqDen();
if (freqNum.equals(freqDen))
return resources.getString(R.string.every_day);
@@ -76,7 +76,8 @@ public class ShowHabitHelper
RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing);
int androidColor =
ColorUtils.getColor(fragment.getActivity(), fragment.habit.color);
ColorUtils.getColor(fragment.getActivity(),
fragment.habit.getColor());
scoreRing.setColor(androidColor);
scoreRing.setPercentage(todayPercentage);
@@ -109,13 +110,14 @@ public class ShowHabitHelper
TextView questionLabel =
(TextView) view.findViewById(R.id.questionLabel);
questionLabel.setTextColor(fragment.activeColor);
questionLabel.setText(fragment.habit.description);
questionLabel.setText(fragment.habit.getDescription());
TextView reminderLabel =
(TextView) view.findViewById(R.id.reminderLabel);
if (fragment.habit.hasReminder()) reminderLabel.setText(
DateUtils.formatTime(fragment.getActivity(),
fragment.habit.reminderHour, fragment.habit.reminderMin));
fragment.habit.getReminderHour(),
fragment.habit.getReminderMin()));
else reminderLabel.setText(
fragment.getResources().getString(R.string.reminder_off));
@@ -123,7 +125,7 @@ public class ShowHabitHelper
(TextView) view.findViewById(R.id.frequencyLabel);
frequencyLabel.setText(getFreqText());
if (fragment.habit.description.isEmpty())
if (fragment.habit.getDescription().isEmpty())
questionLabel.setVisibility(View.GONE);
}
@@ -143,14 +145,15 @@ public class ShowHabitHelper
TextView textView = (TextView) view.findViewById(viewId);
int androidColor =
ColorUtils.getColor(fragment.activity, fragment.habit.color);
ColorUtils.getColor(fragment.activity, fragment.habit.getColor());
textView.setTextColor(androidColor);
}
void updateColors()
{
fragment.activeColor =
ColorUtils.getColor(fragment.getContext(), fragment.habit.color);
ColorUtils.getColor(fragment.getContext(),
fragment.habit.getColor());
fragment.inactiveColor =
InterfaceUtils.getStyledColor(fragment.getContext(),
R.attr.mediumContrastTextColor);

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides activity that display detailed habit information and related
* classes.
*/
package org.isoron.uhabits.ui.habits.show;

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import org.isoron.uhabits.models.Habit;

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import android.content.Context;
import android.graphics.Canvas;
@@ -26,12 +26,12 @@ import android.graphics.RectF;
import android.util.AttributeSet;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.ModelObservable;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.models.Habit;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@@ -101,7 +101,8 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
{
if(habit != null)
{
this.primaryColor = ColorUtils.getColor(getContext(), habit.color);
this.primaryColor = ColorUtils.getColor(getContext(),
habit.getColor());
}
textColor = InterfaceUtils.getStyledColor(getContext(), R.attr.mediumContrastTextColor);
@@ -177,7 +178,7 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
if(isInEditMode()) generateRandomData();
else if(habit != null)
{
frequency = habit.repetitions.getWeekdayFrequency();
frequency = habit.getRepetitions().getWeekdayFrequency();
createColors();
}
@@ -314,15 +315,15 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
refreshData();
}
}.execute();
habit.observable.addListener(this);
habit.checkmarks.observable.addListener(this);
habit.getObservable().addListener(this);
habit.getCheckmarks().observable.addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
habit.checkmarks.observable.removeListener(this);
habit.observable.removeListener(this);
habit.getCheckmarks().observable.removeListener(this);
habit.getObservable().removeListener(this);
super.onDetachedFromWindow();
}

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import android.content.Context;
import android.graphics.Canvas;
@@ -167,7 +167,8 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
private void createColors()
{
if(habit != null)
this.primaryColor = ColorUtils.getColor(getContext(), habit.color);
this.primaryColor = ColorUtils.getColor(getContext(),
habit.getColor());
if(isBackgroundTransparent)
primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f);
@@ -216,7 +217,7 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
else
{
if(habit == null) return;
checkmarks = habit.checkmarks.getAllValues();
checkmarks = habit.getCheckmarks().getAllValues();
createColors();
}
@@ -424,15 +425,15 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
refreshData();
}
}.execute();
habit.observable.addListener(this);
habit.checkmarks.observable.addListener(this);
habit.getObservable().addListener(this);
habit.getCheckmarks().observable.addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
habit.checkmarks.observable.removeListener(this);
habit.observable.removeListener(this);
habit.getCheckmarks().observable.removeListener(this);
habit.getObservable().removeListener(this);
super.onDetachedFromWindow();
}

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import android.content.Context;
import android.graphics.Bitmap;
@@ -45,47 +45,69 @@ import java.util.GregorianCalendar;
import java.util.Random;
public class HabitScoreView extends ScrollableDataView
implements HabitDataView, ModelObservable.Listener
implements HabitDataView, ModelObservable.Listener
{
public static final PorterDuffXfermode XFERMODE_CLEAR =
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
public static final PorterDuffXfermode XFERMODE_SRC =
new PorterDuffXfermode(PorterDuff.Mode.SRC);
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
public static int DEFAULT_BUCKET_SIZES[] = { 1, 7, 31, 92, 365 };
public static final PorterDuffXfermode XFERMODE_SRC =
new PorterDuffXfermode(PorterDuff.Mode.SRC);
public static int DEFAULT_BUCKET_SIZES[] = {1, 7, 31, 92, 365};
private Paint pGrid;
private float em;
private Habit habit;
private SimpleDateFormat dfMonth;
private SimpleDateFormat dfDay;
private SimpleDateFormat dfYear;
private Paint pText, pGraph;
private RectF rect, prevRect;
private int baseSize;
private int paddingTop;
private float columnWidth;
private int columnHeight;
private int nColumns;
private int textColor;
private int gridColor;
@Nullable
private int[] scores;
private int primaryColor;
private int bucketSize = 7;
private int footerHeight;
private int backgroundColor;
private Bitmap drawingCache;
private Canvas cacheCanvas;
private boolean isTransparencyEnabled;
private int skipYear = 0;
private String previousYearText;
private String previousMonthText;
public HabitScoreView(Context context)
{
super(context);
@@ -99,33 +121,56 @@ public class HabitScoreView extends ScrollableDataView
init();
}
@Override
public void onModelChange()
{
refreshData();
}
@Override
public void refreshData()
{
if (isInEditMode()) generateRandomData();
else
{
if (habit == null) return;
scores = habit.getScores().getAllValues(bucketSize);
createColors();
}
postInvalidate();
}
public void setBucketSize(int bucketSize)
{
this.bucketSize = bucketSize;
}
@Override
public void setHabit(Habit habit)
{
this.habit = habit;
createColors();
}
private void init()
public void setIsTransparencyEnabled(boolean enabled)
{
createPaints();
this.isTransparencyEnabled = enabled;
createColors();
dfYear = DateUtils.getDateFormat("yyyy");
dfMonth = DateUtils.getDateFormat("MMM");
dfDay = DateUtils.getDateFormat("d");
rect = new RectF();
prevRect = new RectF();
requestLayout();
}
private void createColors()
{
if(habit != null)
this.primaryColor = ColorUtils.getColor(getContext(), habit.color);
if (habit != null) this.primaryColor =
ColorUtils.getColor(getContext(), habit.getColor());
textColor = InterfaceUtils.getStyledColor(getContext(), R.attr.mediumContrastTextColor);
gridColor = InterfaceUtils.getStyledColor(getContext(), R.attr.lowContrastTextColor);
backgroundColor = InterfaceUtils.getStyledColor(getContext(), R.attr.cardBackgroundColor);
textColor = InterfaceUtils.getStyledColor(getContext(),
R.attr.mediumContrastTextColor);
gridColor = InterfaceUtils.getStyledColor(getContext(),
R.attr.lowContrastTextColor);
backgroundColor = InterfaceUtils.getStyledColor(getContext(),
R.attr.cardBackgroundColor);
}
protected void createPaints()
@@ -141,72 +186,100 @@ public class HabitScoreView extends ScrollableDataView
pGrid.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
private void drawFooter(Canvas canvas, RectF rect, long currentDate)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
String yearText = dfYear.format(currentDate);
String monthText = dfMonth.format(currentDate);
String dayText = dfDay.format(currentDate);
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
{
if(height < 9) height = 200;
GregorianCalendar calendar = DateUtils.getCalendar(currentDate);
float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize);
float textSize = height * 0.06f;
pText.setTextSize(Math.min(textSize, maxTextSize));
em = pText.getFontSpacing();
String text;
int year = calendar.get(Calendar.YEAR);
footerHeight = (int)(3 * em);
paddingTop = (int) (em);
boolean shouldPrintYear = true;
if (yearText.equals(previousYearText)) shouldPrintYear = false;
if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
baseSize = (height - footerHeight - paddingTop) / 8;
setScrollerBucketSize(baseSize);
columnWidth = baseSize;
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
nColumns = (int) (width / columnWidth);
columnWidth = (float) width / nColumns;
columnHeight = 8 * baseSize;
float minStrokeWidth = InterfaceUtils.dpToPixels(getContext(), 1);
pGraph.setTextSize(baseSize * 0.5f);
pGraph.setStrokeWidth(baseSize * 0.1f);
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
if(isTransparencyEnabled)
initCache(width, height);
}
private void initCache(int width, int height)
{
if (drawingCache != null) drawingCache.recycle();
drawingCache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(drawingCache);
}
public void refreshData()
{
if(isInEditMode())
generateRandomData();
else
if (skipYear > 0)
{
if (habit == null) return;
scores = habit.scores.getAllValues(bucketSize);
createColors();
skipYear--;
shouldPrintYear = false;
}
postInvalidate();
if (shouldPrintYear)
{
previousYearText = yearText;
previousMonthText = "";
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f,
pText);
skipYear = 1;
}
if (bucketSize < 365)
{
if (!monthText.equals(previousMonthText))
{
previousMonthText = monthText;
text = monthText;
}
else
{
text = dayText;
}
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
pText);
}
}
public void setBucketSize(int bucketSize)
private void drawGrid(Canvas canvas, RectF rGrid)
{
this.bucketSize = bucketSize;
int nRows = 5;
float rowHeight = rGrid.height() / nRows;
pText.setTextAlign(Paint.Align.LEFT);
pText.setColor(textColor);
pGrid.setColor(gridColor);
for (int i = 0; i < nRows; i++)
{
canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)),
rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText);
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
pGrid);
rGrid.offset(0, rowHeight);
}
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
}
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
{
pGraph.setColor(primaryColor);
canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(),
rectTo.centerX(), rectTo.centerY(), pGraph);
}
private void drawMarker(Canvas canvas, RectF rect)
{
rect.inset(baseSize * 0.15f, baseSize * 0.15f);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawOval(rect, pGraph);
rect.inset(baseSize * 0.1f, baseSize * 0.1f);
setModeOrColor(pGraph, XFERMODE_SRC, primaryColor);
canvas.drawOval(rect, pGraph);
rect.inset(baseSize * 0.1f, baseSize * 0.1f);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawOval(rect, pGraph);
if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
}
private void generateRandomData()
@@ -215,7 +288,7 @@ public class HabitScoreView extends ScrollableDataView
scores = new int[100];
scores[0] = Score.MAX_VALUE / 2;
for(int i = 1; i < 100; i++)
for (int i = 1; i < 100; i++)
{
int step = Score.MAX_VALUE / 10;
scores[i] = scores[i - 1] + random.nextInt(step * 2) - step;
@@ -223,15 +296,90 @@ public class HabitScoreView extends ScrollableDataView
}
}
private float getMaxDayWidth()
{
float maxDayWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < 28; i++)
{
day.set(Calendar.DAY_OF_MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxDayWidth = Math.max(maxDayWidth, monthWidth);
}
return maxDayWidth;
}
private float getMaxMonthWidth()
{
float maxMonthWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < 12; i++)
{
day.set(Calendar.MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
}
return maxMonthWidth;
}
private void init()
{
createPaints();
createColors();
dfYear = DateUtils.getDateFormat("yyyy");
dfMonth = DateUtils.getDateFormat("MMM");
dfDay = DateUtils.getDateFormat("d");
rect = new RectF();
prevRect = new RectF();
}
private void initCache(int width, int height)
{
if (drawingCache != null) drawingCache.recycle();
drawingCache =
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(drawingCache);
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
new BaseTask()
{
@Override
protected void doInBackground()
{
refreshData();
}
}.execute();
habit.getObservable().addListener(this);
habit.getScores().getObservable().addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
habit.getScores().getObservable().removeListener(this);
habit.getObservable().removeListener(this);
super.onDetachedFromWindow();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Canvas activeCanvas;
if(isTransparencyEnabled)
if (isTransparencyEnabled)
{
if(drawingCache == null) initCache(getWidth(), getHeight());
if (drawingCache == null) initCache(getWidth(), getHeight());
activeCanvas = cacheCanvas;
drawingCache.eraseColor(Color.TRANSPARENT);
@@ -258,21 +406,21 @@ public class HabitScoreView extends ScrollableDataView
long currentDate = DateUtils.getStartOfToday();
for(int k = 0; k < nColumns + getDataOffset() - 1; k++)
for (int k = 0; k < nColumns + getDataOffset() - 1; k++)
currentDate -= bucketSize * DateUtils.millisecondsInOneDay;
for (int k = 0; k < nColumns; k++)
{
int score = 0;
int offset = nColumns - k - 1 + getDataOffset();
if(offset < scores.length) score = scores[offset];
if (offset < scores.length) score = scores[offset];
double relativeScore = ((double) score) / Score.MAX_VALUE;
int height = (int) (columnHeight * relativeScore);
rect.set(0, 0, baseSize, baseSize);
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
paddingTop + columnHeight - height - baseSize / 2);
paddingTop + columnHeight - height - baseSize / 2);
if (!prevRect.isEmpty())
{
@@ -291,181 +439,56 @@ public class HabitScoreView extends ScrollableDataView
currentDate += bucketSize * DateUtils.millisecondsInOneDay;
}
if(activeCanvas != canvas)
canvas.drawBitmap(drawingCache, 0, 0, null);
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
}
private int skipYear = 0;
private String previousYearText;
private String previousMonthText;
private void drawFooter(Canvas canvas, RectF rect, long currentDate)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
String yearText = dfYear.format(currentDate);
String monthText = dfMonth.format(currentDate);
String dayText = dfDay.format(currentDate);
GregorianCalendar calendar = DateUtils.getCalendar(currentDate);
String text;
int year = calendar.get(Calendar.YEAR);
boolean shouldPrintYear = true;
if(yearText.equals(previousYearText)) shouldPrintYear = false;
if(bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
if(skipYear > 0)
{
skipYear--;
shouldPrintYear = false;
}
if(shouldPrintYear)
{
previousYearText = yearText;
previousMonthText = "";
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText);
skipYear = 1;
}
if(bucketSize < 365)
{
if(!monthText.equals(previousMonthText))
{
previousMonthText = monthText;
text = monthText;
}
else
{
text = dayText;
}
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, pText);
}
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
private void drawGrid(Canvas canvas, RectF rGrid)
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
int nRows = 5;
float rowHeight = rGrid.height() / nRows;
if (height < 9) height = 200;
pText.setTextAlign(Paint.Align.LEFT);
pText.setColor(textColor);
pGrid.setColor(gridColor);
float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize);
float textSize = height * 0.06f;
pText.setTextSize(Math.min(textSize, maxTextSize));
em = pText.getFontSpacing();
for (int i = 0; i < nRows; i++)
{
canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), rGrid.left + 0.5f * em,
rGrid.top + 1f * em, pText);
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
rGrid.offset(0, rowHeight);
}
footerHeight = (int) (3 * em);
paddingTop = (int) (em);
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
}
baseSize = (height - footerHeight - paddingTop) / 8;
setScrollerBucketSize(baseSize);
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
{
pGraph.setColor(primaryColor);
canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), rectTo.centerX(), rectTo.centerY(),
pGraph);
}
columnWidth = baseSize;
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
private void drawMarker(Canvas canvas, RectF rect)
{
rect.inset(baseSize * 0.15f, baseSize * 0.15f);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawOval(rect, pGraph);
nColumns = (int) (width / columnWidth);
columnWidth = (float) width / nColumns;
rect.inset(baseSize * 0.1f, baseSize * 0.1f);
setModeOrColor(pGraph, XFERMODE_SRC, primaryColor);
canvas.drawOval(rect, pGraph);
columnHeight = 8 * baseSize;
rect.inset(baseSize * 0.1f, baseSize * 0.1f);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawOval(rect, pGraph);
float minStrokeWidth = InterfaceUtils.dpToPixels(getContext(), 1);
pGraph.setTextSize(baseSize * 0.5f);
pGraph.setStrokeWidth(baseSize * 0.1f);
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
if(isTransparencyEnabled)
pGraph.setXfermode(XFERMODE_SRC);
}
public void setIsTransparencyEnabled(boolean enabled)
{
this.isTransparencyEnabled = enabled;
createColors();
requestLayout();
if (isTransparencyEnabled) initCache(width, height);
}
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
{
if(isTransparencyEnabled)
p.setXfermode(mode);
else
p.setColor(color);
}
private float getMaxMonthWidth()
{
float maxMonthWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for(int i = 0; i < 12; i++)
{
day.set(Calendar.MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
}
return maxMonthWidth;
}
private float getMaxDayWidth()
{
float maxDayWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for(int i = 0; i < 28; i++)
{
day.set(Calendar.DAY_OF_MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxDayWidth = Math.max(maxDayWidth, monthWidth);
}
return maxDayWidth;
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
new BaseTask()
{
@Override
protected void doInBackground()
{
refreshData();
}
}.execute();
habit.observable.addListener(this);
habit.scores.observable.addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
habit.scores.observable.removeListener(this);
habit.observable.removeListener(this);
super.onDetachedFromWindow();
}
@Override
public void onModelChange()
{
refreshData();
if (isTransparencyEnabled) p.setXfermode(mode);
else p.setColor(color);
}
}

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import android.content.Context;
import android.graphics.Canvas;
@@ -28,12 +28,12 @@ import android.util.AttributeSet;
import android.view.View;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.ModelObservable;
import org.isoron.uhabits.models.Streak;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
import java.text.DateFormat;
import java.util.Collections;
@@ -41,29 +41,45 @@ import java.util.Date;
import java.util.List;
import java.util.TimeZone;
public class HabitStreakView extends View implements HabitDataView, ModelObservable.Listener
public class HabitStreakView extends View
implements HabitDataView, ModelObservable.Listener
{
private Habit habit;
private Paint paint;
private long minLength;
private long maxLength;
private int[] colors;
private RectF rect;
private int baseSize;
private int primaryColor;
private List<Streak> streaks;
private boolean isBackgroundTransparent;
private DateFormat dateFormat;
private int width;
private float em;
private float maxLabelWidth;
private float textMargin;
private boolean shouldShowLabels;
private int maxStreakCount;
private int textColor;
private int reverseTextColor;
public HabitStreakView(Context context)
@@ -79,12 +95,105 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva
init();
}
@Override
public void onModelChange()
{
refreshData();
}
@Override
public void refreshData()
{
if (habit == null) return;
streaks = habit.getStreaks().getBest(maxStreakCount);
createColors();
updateMaxMin();
postInvalidate();
}
@Override
public void setHabit(Habit habit)
{
this.habit = habit;
createColors();
}
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
{
this.isBackgroundTransparent = isBackgroundTransparent;
createColors();
}
private void createColors()
{
if (habit != null) this.primaryColor =
ColorUtils.getColor(getContext(), habit.getColor());
int red = Color.red(primaryColor);
int green = Color.green(primaryColor);
int blue = Color.blue(primaryColor);
colors = new int[4];
colors[3] = primaryColor;
colors[2] = Color.argb(192, red, green, blue);
colors[1] = Color.argb(96, red, green, blue);
colors[0] = InterfaceUtils.getStyledColor(getContext(),
R.attr.lowContrastTextColor);
textColor = InterfaceUtils.getStyledColor(getContext(),
R.attr.mediumContrastTextColor);
reverseTextColor = InterfaceUtils.getStyledColor(getContext(),
R.attr.highContrastReverseTextColor);
}
protected void createPaints()
{
paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setAntiAlias(true);
}
private void drawRow(Canvas canvas, Streak streak, RectF rect)
{
if (maxLength == 0) return;
float percentage = (float) streak.getLength() / maxLength;
float availableWidth = width - 2 * maxLabelWidth;
if (shouldShowLabels) availableWidth -= 2 * textMargin;
float barWidth = percentage * availableWidth;
float minBarWidth =
paint.measureText(Long.toString(streak.getLength())) + em;
barWidth = Math.max(barWidth, minBarWidth);
float gap = (width - barWidth) / 2;
float paddingTopBottom = baseSize * 0.05f;
paint.setColor(percentageToColor(percentage));
canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom,
rect.right - gap, rect.bottom - paddingTopBottom, paint);
float yOffset = rect.centerY() + 0.3f * em;
paint.setColor(reverseTextColor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(Long.toString(streak.getLength()), rect.centerX(),
yOffset, paint);
if (shouldShowLabels)
{
String startLabel = dateFormat.format(new Date(streak.getStart()));
String endLabel = dateFormat.format(new Date(streak.getEnd()));
paint.setColor(textColor);
paint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(startLabel, gap - textMargin, yOffset, paint);
paint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint);
}
}
private void init()
{
createPaints();
@@ -99,158 +208,6 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
{
maxStreakCount = height / baseSize;
this.width = width;
float minTextSize = getResources().getDimension(R.dimen.tinyTextSize);
float maxTextSize = getResources().getDimension(R.dimen.regularTextSize);
float textSize = baseSize * 0.5f;
paint.setTextSize(Math.max(Math.min(textSize, maxTextSize), minTextSize));
em = paint.getFontSpacing();
textMargin = 0.5f * em;
updateMaxMin();
}
private void createColors()
{
if(habit != null)
this.primaryColor = ColorUtils.getColor(getContext(), habit.color);
int red = Color.red(primaryColor);
int green = Color.green(primaryColor);
int blue = Color.blue(primaryColor);
colors = new int[4];
colors[3] = primaryColor;
colors[2] = Color.argb(192, red, green, blue);
colors[1] = Color.argb(96, red, green, blue);
colors[0] = InterfaceUtils.getStyledColor(getContext(), R.attr.lowContrastTextColor);
textColor = InterfaceUtils.getStyledColor(getContext(), R.attr.mediumContrastTextColor);
reverseTextColor = InterfaceUtils.getStyledColor(getContext(), R.attr.highContrastReverseTextColor);
}
protected void createPaints()
{
paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setAntiAlias(true);
}
public void refreshData()
{
if(habit == null) return;
streaks = habit.streaks.getAll(maxStreakCount);
createColors();
updateMaxMin();
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
if(streaks.size() == 0) return;
rect.set(0, 0, width, baseSize);
for(Streak s : streaks)
{
drawRow(canvas, s, rect);
rect.offset(0, baseSize);
}
}
private void updateMaxMin()
{
maxLength = 0;
minLength = Long.MAX_VALUE;
shouldShowLabels = true;
for (Streak s : streaks)
{
maxLength = Math.max(maxLength, s.length);
minLength = Math.min(minLength, s.length);
float lw1 = paint.measureText(dateFormat.format(new Date(s.start)));
float lw2 = paint.measureText(dateFormat.format(new Date(s.end)));
maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
}
if(width - 2 * maxLabelWidth < width * 0.25f)
{
maxLabelWidth = 0;
shouldShowLabels = false;
}
}
private void drawRow(Canvas canvas, Streak streak, RectF rect)
{
if(maxLength == 0) return;
float percentage = (float) streak.length / maxLength;
float availableWidth = width - 2 * maxLabelWidth;
if(shouldShowLabels) availableWidth -= 2 * textMargin;
float barWidth = percentage * availableWidth;
float minBarWidth = paint.measureText(streak.length.toString()) + em;
barWidth = Math.max(barWidth, minBarWidth);
float gap = (width - barWidth) / 2;
float paddingTopBottom = baseSize * 0.05f;
paint.setColor(percentageToColor(percentage));
canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, rect.right - gap,
rect.bottom - paddingTopBottom, paint);
float yOffset = rect.centerY() + 0.3f * em;
paint.setColor(reverseTextColor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(streak.length.toString(), rect.centerX(), yOffset, paint);
if(shouldShowLabels)
{
String startLabel = dateFormat.format(new Date(streak.start));
String endLabel = dateFormat.format(new Date(streak.end));
paint.setColor(textColor);
paint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(startLabel, gap - textMargin, yOffset, paint);
paint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint);
}
}
private int percentageToColor(float percentage)
{
if(percentage >= 1.0f) return colors[3];
if(percentage >= 0.8f) return colors[2];
if(percentage >= 0.5f) return colors[1];
return colors[0];
}
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
{
this.isBackgroundTransparent = isBackgroundTransparent;
createColors();
}
@Override
protected void onAttachedToWindow()
{
@@ -263,21 +220,93 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva
refreshData();
}
}.execute();
habit.observable.addListener(this);
habit.streaks.observable.addListener(this);
habit.getObservable().addListener(this);
habit.getStreaks().getObservable().addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
habit.streaks.observable.removeListener(this);
habit.observable.removeListener(this);
habit.getStreaks().getObservable().removeListener(this);
habit.getObservable().removeListener(this);
super.onDetachedFromWindow();
}
@Override
public void onModelChange()
protected void onDraw(Canvas canvas)
{
refreshData();
super.onDraw(canvas);
if (streaks.size() == 0) return;
rect.set(0, 0, width, baseSize);
for (Streak s : streaks)
{
drawRow(canvas, s, rect);
rect.offset(0, baseSize);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
maxStreakCount = height / baseSize;
this.width = width;
float minTextSize = getResources().getDimension(R.dimen.tinyTextSize);
float maxTextSize =
getResources().getDimension(R.dimen.regularTextSize);
float textSize = baseSize * 0.5f;
paint.setTextSize(
Math.max(Math.min(textSize, maxTextSize), minTextSize));
em = paint.getFontSpacing();
textMargin = 0.5f * em;
updateMaxMin();
}
private int percentageToColor(float percentage)
{
if (percentage >= 1.0f) return colors[3];
if (percentage >= 0.8f) return colors[2];
if (percentage >= 0.5f) return colors[1];
return colors[0];
}
private void updateMaxMin()
{
maxLength = 0;
minLength = Long.MAX_VALUE;
shouldShowLabels = true;
for (Streak s : streaks)
{
maxLength = Math.max(maxLength, s.getLength());
minLength = Math.min(minLength, s.getLength());
float lw1 =
paint.measureText(dateFormat.format(new Date(s.getStart())));
float lw2 =
paint.measureText(dateFormat.format(new Date(s.getEnd())));
maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
}
if (width - 2 * maxLabelWidth < width * 0.25f)
{
maxLabelWidth = 0;
shouldShowLabels = false;
}
}
}

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import android.content.Context;
import android.graphics.Color;
@@ -32,8 +32,8 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.util.Arrays;

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import android.content.Context;
import android.graphics.Bitmap;
@@ -70,7 +70,7 @@ public class RingView extends View
percentage = 0.0f;
precision = 0.01f;
color = ColorUtils.CSV_PALETTE[0];
color = ColorUtils.getAndroidTestColor(0);
thickness = InterfaceUtils.dpToPixels(getContext(), 2);
text = "";
textSize = context.getResources().getDimension(R.dimen.smallTextSize);

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
package org.isoron.uhabits.ui.habits.show.views;
import android.animation.ValueAnimator;
import android.content.Context;
@@ -28,15 +28,19 @@ import android.view.View;
import android.view.ViewParent;
import android.widget.Scroller;
public abstract class ScrollableDataView extends View implements GestureDetector.OnGestureListener,
ValueAnimator.AnimatorUpdateListener
public abstract class ScrollableDataView extends View
implements GestureDetector.OnGestureListener,
ValueAnimator.AnimatorUpdateListener
{
private int dataOffset;
private int scrollerBucketSize;
private GestureDetector detector;
private Scroller scroller;
private ValueAnimator scrollAnimator;
public ScrollableDataView(Context context)
@@ -51,75 +55,9 @@ public abstract class ScrollableDataView extends View implements GestureDetector
init(context);
}
private void init(Context context)
public int getDataOffset()
{
detector = new GestureDetector(context, this);
scroller = new Scroller(context, null, true);
scrollAnimator = ValueAnimator.ofFloat(0, 1);
scrollAnimator.addUpdateListener(this);
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
return detector.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent e)
{
return true;
}
@Override
public void onShowPress(MotionEvent e)
{
}
@Override
public boolean onSingleTapUp(MotionEvent e)
{
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy)
{
if(scrollerBucketSize == 0)
return false;
if(Math.abs(dx) > Math.abs(dy))
{
ViewParent parent = getParent();
if(parent != null) parent.requestDisallowInterceptTouchEvent(true);
}
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) -dx, (int) dy, 0);
scroller.computeScrollOffset();
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
postInvalidate();
return true;
}
@Override
public void onLongPress(MotionEvent e)
{
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
scroller.fling(scroller.getCurrX(), scroller.getCurrY(), (int) velocityX / 2, 0, 0, 100000,
0, 0);
invalidate();
scrollAnimator.setDuration(scroller.getDuration());
scrollAnimator.start();
return false;
return dataOffset;
}
@Override
@@ -137,13 +75,82 @@ public abstract class ScrollableDataView extends View implements GestureDetector
}
}
public int getDataOffset()
@Override
public boolean onDown(MotionEvent e)
{
return dataOffset;
return true;
}
@Override
public boolean onFling(MotionEvent e1,
MotionEvent e2,
float velocityX,
float velocityY)
{
scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
(int) velocityX / 2, 0, 0, 100000, 0, 0);
invalidate();
scrollAnimator.setDuration(scroller.getDuration());
scrollAnimator.start();
return false;
}
@Override
public void onLongPress(MotionEvent e)
{
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy)
{
if (scrollerBucketSize == 0) return false;
if (Math.abs(dx) > Math.abs(dy))
{
ViewParent parent = getParent();
if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
}
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(),
(int) -dx, (int) dy, 0);
scroller.computeScrollOffset();
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
postInvalidate();
return true;
}
@Override
public void onShowPress(MotionEvent e)
{
}
@Override
public boolean onSingleTapUp(MotionEvent e)
{
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
return detector.onTouchEvent(event);
}
public void setScrollerBucketSize(int scrollerBucketSize)
{
this.scrollerBucketSize = scrollerBucketSize;
}
private void init(Context context)
{
detector = new GestureDetector(context, this);
scroller = new Scroller(context, null, true);
scrollAnimator = ValueAnimator.ofFloat(0, 1);
scrollAnimator.addUpdateListener(this);
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides custom views that are used primarily on {@link
* org.isoron.uhabits.ui.habits.show.ShowHabitActivity}.
*/
package org.isoron.uhabits.ui.habits.show.views;

View File

@@ -18,6 +18,6 @@
*/
/**
* Contains classes for the IntroActivity.
* Provides activity that introduces app to the user and related classes.
*/
package org.isoron.uhabits.ui.intro;

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides classes for the Android user interface.
*/
package org.isoron.uhabits.ui;

View File

@@ -18,6 +18,6 @@
*/
/**
* Contains classes for the SettingsActivity.
* Provides activity for changing the settings.
*/
package org.isoron.uhabits.ui.settings;

View File

@@ -27,55 +27,79 @@ import org.isoron.uhabits.R;
public abstract class ColorUtils
{
public static int CSV_PALETTE[] =
{
Color.parseColor("#D32F2F"), // 0 red
Color.parseColor("#E64A19"), // 1 orange
Color.parseColor("#F9A825"), // 2 yellow
Color.parseColor("#AFB42B"), // 3 light green
Color.parseColor("#388E3C"), // 4 dark green
Color.parseColor("#00897B"), // 5 teal
Color.parseColor("#00ACC1"), // 6 cyan
Color.parseColor("#039BE5"), // 7 blue
Color.parseColor("#5E35B1"), // 8 deep purple
Color.parseColor("#8E24AA"), // 9 purple
Color.parseColor("#D81B60"), // 10 pink
Color.parseColor("#303030"), // 11 dark grey
Color.parseColor("#aaaaaa") // 12 light grey
public static String CSV_PALETTE[] = {
"#D32F2F", // 0 red
"#E64A19", // 1 orange
"#F9A825", // 2 yellow
"#AFB42B", // 3 light green
"#388E3C", // 4 dark green
"#00897B", // 5 teal
"#00ACC1", // 6 cyan
"#039BE5", // 7 blue
"#5E35B1", // 8 deep purple
"#8E24AA", // 9 purple
"#D81B60", // 10 pink
"#303030", // 11 dark grey
"#aaaaaa" // 12 light grey
};
public static int colorToPaletteIndex(Context context, int color)
{
int[] palette = getPalette(context);
for(int k = 0; k < palette.length; k++)
if(palette[k] == color) return k;
for (int k = 0; k < palette.length; k++)
if (palette[k] == color) return k;
return -1;
}
public static int[] getPalette(Context context)
public static int getAndroidTestColor(int index)
{
int resourceId = InterfaceUtils.getStyleResource(context, R.attr.palette);
if(resourceId < 0) return CSV_PALETTE;
int palette[] = {
Color.parseColor("#D32F2F"), // 0 red
Color.parseColor("#E64A19"), // 1 orange
Color.parseColor("#F9A825"), // 2 yellow
Color.parseColor("#AFB42B"), // 3 light green
Color.parseColor("#388E3C"), // 4 dark green
Color.parseColor("#00897B"), // 5 teal
Color.parseColor("#00ACC1"), // 6 cyan
Color.parseColor("#039BE5"), // 7 blue
Color.parseColor("#5E35B1"), // 8 deep purple
Color.parseColor("#8E24AA"), // 9 purple
Color.parseColor("#D81B60"), // 10 pink
Color.parseColor("#303030"), // 11 dark grey
Color.parseColor("#aaaaaa") // 12 light grey
};
return context.getResources().getIntArray(resourceId);
return palette[index];
}
public static int getColor(Context context, int paletteColor)
{
if(context == null) throw new IllegalArgumentException("Context is null");
if (context == null)
throw new IllegalArgumentException("Context is null");
int palette[] = getPalette(context);
if(paletteColor < 0 || paletteColor >= palette.length)
if (paletteColor < 0 || paletteColor >= palette.length)
{
Log.w("ColorHelper", String.format("Invalid color: %d. Returning default.", paletteColor));
Log.w("ColorHelper",
String.format("Invalid color: %d. Returning default.",
paletteColor));
paletteColor = 0;
}
return palette[paletteColor];
}
public static int[] getPalette(Context context)
{
int resourceId =
InterfaceUtils.getStyleResource(context, R.attr.palette);
if (resourceId < 0) throw new RuntimeException("resource not found");
return context.getResources().getIntArray(resourceId);
}
public static int mixColors(int color1, int color2, float amount)
{
final byte ALPHA_CHANNEL = 24;
@@ -86,36 +110,26 @@ public abstract class ColorUtils
final float inverseAmount = 1.0f - amount;
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff;
((float) (color2 >> ALPHA_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff;
((float) (color2 >> RED_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff;
((float) (color2 >> GREEN_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int b = ((int) (((float) (color1 & 0xff) * amount) +
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL;
}
public static int setHue(int color, float newHue)
{
return setHSVParameter(color, newHue, 0);
}
public static int setSaturation(int color, float newSaturation)
{
return setHSVParameter(color, newSaturation, 1);
}
public static int setValue(int color, float newValue)
{
return setHSVParameter(color, newValue, 2);
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL |
b << BLUE_CHANNEL;
}
public static int setAlpha(int color, float newAlpha)
{
int intAlpha = (int) (newAlpha * 255);
return Color.argb(intAlpha, Color.red(color), Color.green(color), Color.blue(color));
return Color.argb(intAlpha, Color.red(color), Color.green(color),
Color.blue(color));
}
public static int setMinValue(int color, float newValue)
@@ -126,16 +140,4 @@ public abstract class ColorUtils
return Color.HSVToColor(hsv);
}
private static int setHSVParameter(int color, float newValue, int index)
{
float hsv[] = new float[3];
Color.colorToHSV(color, hsv);
hsv[index] = newValue;
return Color.HSVToColor(hsv);
}
public static String toHTML(int color)
{
return String.format("#%06X", 0xFFFFFF & color);
}
}

View File

@@ -29,11 +29,11 @@ import com.activeandroid.Configuration;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.Streak;
import org.isoron.uhabits.models.sqlite.CheckmarkRecord;
import org.isoron.uhabits.models.sqlite.HabitRecord;
import org.isoron.uhabits.models.sqlite.RepetitionRecord;
import org.isoron.uhabits.models.sqlite.ScoreRecord;
import org.isoron.uhabits.models.sqlite.StreakRecord;
import java.io.File;
import java.io.IOException;
@@ -41,17 +41,12 @@ import java.text.SimpleDateFormat;
public abstract class DatabaseUtils
{
public interface Command
{
void execute();
}
public static void executeAsTransaction(Command command)
public static void executeAsTransaction(Callback callback)
{
ActiveAndroid.beginTransaction();
try
{
command.execute();
callback.execute();
ActiveAndroid.setTransactionSuccessful();
}
finally
@@ -60,30 +55,18 @@ public abstract class DatabaseUtils
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static String saveDatabaseCopy(File dir) throws IOException
{
File db = getDatabaseFile();
SimpleDateFormat dateFormat = DateUtils.getBackupDateFormat();
String date = dateFormat.format(DateUtils.getLocalTime());
File dbCopy = new File(String.format("%s/Loop Habits Backup %s.db", dir.getAbsolutePath(), date));
FileUtils.copy(db, dbCopy);
return dbCopy.getAbsolutePath();
}
@NonNull
public static File getDatabaseFile()
{
Context context = HabitsApplication.getContext();
if(context == null) throw new RuntimeException("No application context found");
if (context == null)
throw new RuntimeException("No application context found");
String databaseFilename = getDatabaseFilename();
return new File(String.format("%s/../databases/%s",
context.getApplicationContext().getFilesDir().getPath(), databaseFilename));
context.getApplicationContext().getFilesDir().getPath(),
databaseFilename));
}
@NonNull
@@ -91,8 +74,7 @@ public abstract class DatabaseUtils
{
String databaseFilename = BuildConfig.databaseFilename;
if (HabitsApplication.isTestMode())
databaseFilename = "test.db";
if (HabitsApplication.isTestMode()) databaseFilename = "test.db";
return databaseFilename;
}
@@ -101,14 +83,15 @@ public abstract class DatabaseUtils
public static void initializeActiveAndroid()
{
Context context = HabitsApplication.getContext();
if(context == null) throw new RuntimeException("application context should not be null");
if (context == null) throw new RuntimeException(
"application context should not be null");
Configuration dbConfig = new Configuration.Builder(context)
.setDatabaseName(getDatabaseFilename())
.setDatabaseVersion(BuildConfig.databaseVersion)
.addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class,
Streak.class)
.create();
.setDatabaseName(getDatabaseFilename())
.setDatabaseVersion(BuildConfig.databaseVersion)
.addModelClasses(CheckmarkRecord.class, HabitRecord.class,
RepetitionRecord.class, ScoreRecord.class, StreakRecord.class)
.create();
ActiveAndroid.initialize(dbConfig);
}
@@ -125,7 +108,28 @@ public abstract class DatabaseUtils
}
finally
{
if(c != null) c.close();
if (c != null) c.close();
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static String saveDatabaseCopy(File dir) throws IOException
{
File db = getDatabaseFile();
SimpleDateFormat dateFormat = DateUtils.getBackupDateFormat();
String date = dateFormat.format(DateUtils.getLocalTime());
File dbCopy = new File(
String.format("%s/Loop Habits Backup %s.db", dir.getAbsolutePath(),
date));
FileUtils.copy(db, dbCopy);
return dbCopy.getAbsolutePath();
}
public interface Callback
{
void execute();
}
}

View File

@@ -37,6 +37,7 @@ import android.util.Log;
import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.text.DateFormat;
import java.util.Calendar;
@@ -44,24 +45,20 @@ import java.util.Date;
public abstract class ReminderUtils
{
public static void createReminderAlarms(Context context)
public static void createReminderAlarm(Context context,
Habit habit,
@Nullable Long reminderTime)
{
for (Habit habit : Habit.getHabitsWithReminder())
createReminderAlarm(context, habit, null);
}
public static void createReminderAlarm(Context context, Habit habit, @Nullable Long reminderTime)
{
if(!habit.hasReminder()) return;
if (!habit.hasReminder()) return;
if (reminderTime == null)
{
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
//noinspection ConstantConditions
calendar.set(Calendar.HOUR_OF_DAY, habit.reminderHour);
calendar.set(Calendar.HOUR_OF_DAY, habit.getReminderHour());
//noinspection ConstantConditions
calendar.set(Calendar.MINUTE, habit.reminderMin);
calendar.set(Calendar.MINUTE, habit.getReminderMin());
calendar.set(Calendar.SECOND, 0);
reminderTime = calendar.getTimeInMillis();
@@ -70,7 +67,8 @@ public abstract class ReminderUtils
reminderTime += AlarmManager.INTERVAL_DAY;
}
long timestamp = DateUtils.getStartOfDay(DateUtils.toLocalTime(reminderTime));
long timestamp =
DateUtils.getStartOfDay(DateUtils.toLocalTime(reminderTime));
Uri uri = habit.getUri();
@@ -80,68 +78,32 @@ public abstract class ReminderUtils
alarmIntent.putExtra("timestamp", timestamp);
alarmIntent.putExtra("reminderTime", reminderTime);
PendingIntent pendingIntent =
PendingIntent.getBroadcast(context, ((int) (habit.getId() % Integer.MAX_VALUE)) + 1,
alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
((int) (habit.getId() % Integer.MAX_VALUE)) + 1, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
AlarmManager manager =
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (Build.VERSION.SDK_INT >= 23)
manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
reminderTime, pendingIntent);
else if (Build.VERSION.SDK_INT >= 19)
manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
else
manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime,
pendingIntent);
else manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
String name = habit.name.substring(0, Math.min(3, habit.name.length()));
String name = habit.getName().substring(0, Math.min(3, habit.getName().length()));
Log.d("ReminderHelper", String.format("Setting alarm (%s): %s",
DateFormat.getDateTimeInstance().format(new Date(reminderTime)), name));
DateFormat.getDateTimeInstance().format(new Date(reminderTime)),
name));
}
@Nullable
public static Uri getRingtoneUri(Context context)
public static void createReminderAlarms(Context context,
HabitList habitList)
{
Uri ringtoneUri = null;
Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String prefRingtoneUri = prefs.getString("pref_ringtone_uri", defaultRingtoneUri.toString());
if (prefRingtoneUri.length() > 0) ringtoneUri = Uri.parse(prefRingtoneUri);
return ringtoneUri;
}
public static void parseRingtoneData(Context context, @Nullable Intent data)
{
if(data == null) return;
Uri ringtoneUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
if (ringtoneUri != null)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString("pref_ringtone_uri", ringtoneUri.toString()).apply();
}
else
{
String off = context.getResources().getString(R.string.none);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString("pref_ringtone_uri", "").apply();
}
}
public static void startRingtonePickerActivity(Fragment fragment, int requestCode)
{
Uri existingRingtoneUri = ReminderUtils.getRingtoneUri(fragment.getContext());
Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultRingtoneUri);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existingRingtoneUri);
fragment.startActivityForResult(intent, requestCode);
for (Habit habit : habitList.getWithReminder())
createReminderAlarm(context, habit, null);
}
@Nullable
@@ -150,11 +112,13 @@ public abstract class ReminderUtils
try
{
Uri ringtoneUri = getRingtoneUri(context);
String ringtoneName = context.getResources().getString(R.string.none);
String ringtoneName =
context.getResources().getString(R.string.none);
if (ringtoneUri != null)
{
Ringtone ringtone = RingtoneManager.getRingtone(context, ringtoneUri);
Ringtone ringtone =
RingtoneManager.getRingtone(context, ringtoneUri);
if (ringtone != null)
{
ringtoneName = ringtone.getTitle(context);
@@ -170,4 +134,64 @@ public abstract class ReminderUtils
return null;
}
}
@Nullable
public static Uri getRingtoneUri(Context context)
{
Uri ringtoneUri = null;
Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
String prefRingtoneUri =
prefs.getString("pref_ringtone_uri", defaultRingtoneUri.toString());
if (prefRingtoneUri.length() > 0)
ringtoneUri = Uri.parse(prefRingtoneUri);
return ringtoneUri;
}
public static void parseRingtoneData(Context context, @Nullable Intent data)
{
if (data == null) return;
Uri ringtoneUri =
data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
if (ringtoneUri != null)
{
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
prefs
.edit()
.putString("pref_ringtone_uri", ringtoneUri.toString())
.apply();
}
else
{
String off = context.getResources().getString(R.string.none);
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString("pref_ringtone_uri", "").apply();
}
}
public static void startRingtonePickerActivity(Fragment fragment,
int requestCode)
{
Uri existingRingtoneUri =
ReminderUtils.getRingtoneUri(fragment.getContext());
Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
RingtoneManager.TYPE_NOTIFICATION);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
defaultRingtoneUri);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI,
existingRingtoneUri);
fragment.startActivityForResult(intent, requestCode);
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides various utilities classes, such as {@link org.isoron.uhabits.utils.ColorUtils}.
*/
package org.isoron.uhabits.utils;

View File

@@ -1,166 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
public class NumberView extends View
{
private int color;
private int number;
private float labelMarginTop;
private TextPaint pText;
private String label;
private RectF rect;
private StaticLayout labelLayout;
private int width;
private int height;
private float textSize;
private float labelTextSize;
private float numberTextSize;
private StaticLayout numberLayout;
public NumberView(Context context)
{
super(context);
this.textSize = getResources().getDimension(R.dimen.regularTextSize);
init();
}
public NumberView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.textSize = getResources().getDimension(R.dimen.regularTextSize);
this.label = InterfaceUtils.getAttribute(context, attrs, "label", "Number");
this.number = InterfaceUtils.getIntAttribute(context, attrs, "number", 0);
this.textSize = InterfaceUtils.getFloatAttribute(context, attrs, "textSize",
getResources().getDimension(R.dimen.regularTextSize));
this.color = ColorUtils.getColor(getContext(), 7);
init();
}
public void setColor(int color)
{
this.color = color;
pText.setColor(color);
postInvalidate();
}
public void setLabel(String label)
{
this.label = label;
requestLayout();
postInvalidate();
}
public void setNumber(int number)
{
this.number = number;
postInvalidate();
}
public void setTextSize(float textSize)
{
this.textSize = textSize;
requestLayout();
postInvalidate();
}
private void init()
{
pText = new TextPaint();
pText.setAntiAlias(true);
pText.setTextAlign(Paint.Align.CENTER);
rect = new RectF();
}
@Override
@SuppressLint("DrawAllocation")
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
labelTextSize = textSize;
labelMarginTop = textSize * 0.35f;
numberTextSize = textSize * 2.85f;
pText.setTextSize(numberTextSize);
numberLayout = new StaticLayout(Integer.toString(number), pText, width,
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, false);
int numberWidth = numberLayout.getWidth();
int numberHeight = numberLayout.getHeight();
pText.setTextSize(labelTextSize);
labelLayout = new StaticLayout(label, pText, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f,
false);
int labelWidth = labelLayout.getWidth();
int labelHeight = labelLayout.getHeight();
width = Math.max(numberWidth, labelWidth);
height = (int) (numberHeight + labelHeight + labelMarginTop);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
rect.set(0, 0, width, height);
canvas.save();
canvas.translate(rect.centerX(), 0);
pText.setColor(color);
pText.setTextSize(numberTextSize);
numberLayout.draw(canvas);
canvas.restore();
canvas.save();
pText.setColor(Color.GRAY);
pText.setTextSize(labelTextSize);
canvas.translate(rect.centerX(), numberLayout.getHeight() + labelMarginTop);
labelLayout.draw(canvas);
canvas.restore();
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.views;
import android.content.Context;
import android.util.AttributeSet;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.models.Habit;
import java.util.Calendar;
import java.util.GregorianCalendar;
public class RepetitionCountView extends NumberView implements HabitDataView
{
private int interval;
private Habit habit;
public RepetitionCountView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.interval = InterfaceUtils.getIntAttribute(context, attrs, "interval", 7);
int labelValue = InterfaceUtils.getIntAttribute(context, attrs, "labelValue", 7);
String labelFormat = InterfaceUtils.getAttribute(context, attrs, "labelFormat",
getResources().getString(R.string.last_x_days));
setLabel(String.format(labelFormat, labelValue));
}
@Override
public void refreshData()
{
if(isInEditMode())
{
setNumber(interval);
return;
}
long to = DateUtils.getStartOfToday();
long from;
if(interval == 0)
{
from = 0;
}
else
{
GregorianCalendar fromCalendar = DateUtils.getStartOfTodayCalendar();
fromCalendar.add(Calendar.DAY_OF_YEAR, -interval + 1);
from = fromCalendar.getTimeInMillis();
}
if(habit != null)
setNumber(habit.repetitions.count(from, to));
postInvalidate();
}
@Override
public void setHabit(Habit habit)
{
this.habit = habit;
setColor(ColorUtils.getColor(getContext(), habit.color));
}
}

View File

@@ -35,16 +35,23 @@ import android.widget.ImageView;
import android.widget.RemoteViews;
import android.widget.TextView;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.inject.Inject;
public abstract class BaseWidgetProvider extends AppWidgetProvider
{
@Inject
HabitList habitList;
private class WidgetDimensions
{
public int portraitWidth, portraitHeight;
@@ -100,6 +107,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
private void updateWidget(Context context, AppWidgetManager manager,
int widgetId, Bundle options)
{
HabitsApplication.getComponent().inject(this);
WidgetDimensions dim = getWidgetDimensions(context, options);
Context appContext = context.getApplicationContext();
@@ -108,7 +116,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1L);
if(habitId < 0) return;
Habit habit = Habit.get(habitId);
Habit habit = habitList.getById(habitId);
if(habit == null)
{
drawErrorWidget(context, manager, widgetId);
@@ -288,7 +296,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
widgetView.setDrawingCacheEnabled(true);
widgetView.buildDrawingCache(true);
Bitmap drawingCache = widgetView.getDrawingCache();
remoteViews.setTextViewText(R.id.label, habit.name);
remoteViews.setTextViewText(R.id.label, habit.getName());
remoteViews.setImageViewBitmap(R.id.imageView, drawingCache);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)

View File

@@ -25,8 +25,8 @@ import android.view.View;
import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.views.CheckmarkWidgetView;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.widgets.views.CheckmarkWidgetView;
import org.isoron.uhabits.ui.habits.show.views.HabitDataView;
public class CheckmarkWidgetProvider extends BaseWidgetProvider
{

View File

@@ -26,9 +26,9 @@ import android.view.View;
import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.views.GraphWidgetView;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.views.HabitFrequencyView;
import org.isoron.uhabits.widgets.views.GraphWidgetView;
import org.isoron.uhabits.ui.habits.show.views.HabitDataView;
import org.isoron.uhabits.ui.habits.show.views.HabitFrequencyView;
public class FrequencyWidgetProvider extends BaseWidgetProvider
{

Some files were not shown because too many files have changed in this diff Show More