mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-07 01:28:52 -06:00
Separate ActiveAndroid from models
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
23
app/src/main/java/org/isoron/uhabits/io/package-info.java
Normal file
23
app/src/main/java/org/isoron/uhabits/io/package-info.java
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
235
app/src/main/java/org/isoron/uhabits/models/HabitList.java
Normal file
235
app/src/main/java/org/isoron/uhabits/models/HabitList.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
23
app/src/main/java/org/isoron/uhabits/package-info.java
Normal file
23
app/src/main/java/org/isoron/uhabits/package-info.java
Normal 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;
|
||||
24
app/src/main/java/org/isoron/uhabits/tasks/package-info.java
Normal file
24
app/src/main/java/org/isoron/uhabits/tasks/package-info.java
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains classes for AboutActivity
|
||||
* Provides activity that shows information about the app.
|
||||
*/
|
||||
package org.isoron.uhabits.ui.about;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -18,6 +18,6 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains classes for ListHabitsActivity.
|
||||
* Provides acitivity for listing habits and related classes.
|
||||
*/
|
||||
package org.isoron.uhabits.ui.habits.list;
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
23
app/src/main/java/org/isoron/uhabits/ui/package-info.java
Normal file
23
app/src/main/java/org/isoron/uhabits/ui/package-info.java
Normal 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;
|
||||
@@ -18,6 +18,6 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains classes for the SettingsActivity.
|
||||
* Provides activity for changing the settings.
|
||||
*/
|
||||
package org.isoron.uhabits.ui.settings;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
23
app/src/main/java/org/isoron/uhabits/utils/package-info.java
Normal file
23
app/src/main/java/org/isoron/uhabits/utils/package-info.java
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user