Move existing source code to subfolder

This commit is contained in:
2019-01-22 13:19:54 -06:00
parent db1ba822fe
commit 4ccda9d6f7
882 changed files with 0 additions and 0 deletions

1
legacy/uhabits-core/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,51 @@
plugins {
id "net.ltgt.apt" version "0.7"
}
apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'jacoco'
dependencies {
apt 'com.google.auto.factory:auto-factory:1.0-beta3'
apt 'com.google.dagger:dagger:2.11-rc2'
compileOnly 'javax.annotation:jsr250-api:1.0'
compileOnly 'org.jetbrains:annotations-java5:15.0'
compileOnly 'com.google.auto.factory:auto-factory:1.0-beta3'
compileOnly 'com.google.dagger:dagger:2.11-rc2'
implementation "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'org.apache.commons:commons-lang3:3.5'
implementation 'com.google.code.gson:gson:2.7'
testImplementation 'junit:junit:4+'
testImplementation 'org.hamcrest:hamcrest-library:1.4-atlassian-1'
testImplementation 'org.apache.commons:commons-io:1.3.2'
testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.json:json:20160810'
testImplementation 'org.xerial:sqlite-jdbc:3.18.0'
testImplementation 'nl.jqno.equalsverifier:equalsverifier:2.3.1'
implementation('com.opencsv:opencsv:3.9') {
exclude group: 'commons-logging', module: 'commons-logging'
}
}
jacocoTestReport {
reports {
xml.enabled = true
html.enabled = true
}
afterEvaluate {
classDirectories = files(classDirectories.files.collect {
fileTree(dir: it, exclude: '**/*Factory.*')
})
}
}
check.dependsOn jacocoTestReport
sourceCompatibility = "1.8"
targetCompatibility = "1.8"

View File

@@ -0,0 +1,25 @@
/*
* 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.core;
import javax.inject.*;
@Scope
public @interface AppScope {}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2017 Á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.core;
public class Config
{
public static final String DATABASE_FILENAME = "uhabits.db";
public static int DATABASE_VERSION = 22;
}

View File

@@ -0,0 +1,99 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
/**
* Command to archive a list of habits.
*/
public class ArchiveHabitsCommand extends Command
{
final List<Habit> selected;
final HabitList habitList;
public ArchiveHabitsCommand(@NonNull HabitList habitList,
@NonNull List<Habit> selected)
{
super();
this.habitList = habitList;
this.selected = new LinkedList<>(selected);
}
@Override
public void execute()
{
for (Habit h : selected) h.setArchived(true);
habitList.update(selected);
}
@NonNull
@Override
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
for (Habit h : selected) h.setArchived(false);
habitList.update(selected);
}
public static class Record
{
@NonNull
public final String id;
@NonNull
public final String event = "Archive";
@NonNull
public final List<Long> habits;
public Record(@NonNull ArchiveHabitsCommand command)
{
id = command.getId();
habits = new LinkedList<>();
for (Habit h : command.selected)
{
habits.add(h.getId());
}
}
@NonNull
public ArchiveHabitsCommand toCommand(@NonNull HabitList habitList)
{
List<Habit> selected = new LinkedList<>();
for (Long id : this.habits) selected.add(habitList.getById(id));
ArchiveHabitsCommand command;
command = new ArchiveHabitsCommand(habitList, selected);
command.setId(id);
return command;
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.commands;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
/**
* Command to change the color of a list of habits.
*/
public class ChangeHabitColorCommand extends Command
{
@NonNull
final HabitList habitList;
@NonNull
final List<Habit> selected;
@NonNull
final List<Integer> originalColors;
@NonNull
final Integer newColor;
public ChangeHabitColorCommand(@NonNull HabitList habitList,
@NonNull List<Habit> selected,
@NonNull Integer newColor)
{
this.habitList = habitList;
this.selected = selected;
this.newColor = newColor;
this.originalColors = new ArrayList<>(selected.size());
for (Habit h : selected) originalColors.add(h.getColor());
}
@Override
public void execute()
{
for (Habit h : selected) h.setColor(newColor);
habitList.update(selected);
}
@NonNull
@Override
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
int k = 0;
for (Habit h : selected) h.setColor(originalColors.get(k++));
habitList.update(selected);
}
public static class Record
{
@NonNull
public String id;
@NonNull
public String event = "ChangeColor";
@NonNull
public List<Long> habits;
@NonNull
public Integer color;
public Record(ChangeHabitColorCommand command)
{
id = command.getId();
color = command.newColor;
habits = new LinkedList<>();
for (Habit h : command.selected)
{
if (!h.hasId()) throw new RuntimeException("Habit not saved");
habits.add(h.getId());
}
}
public ChangeHabitColorCommand toCommand(@NonNull HabitList habitList)
{
List<Habit> selected = new LinkedList<>();
for (Long id : this.habits) selected.add(habitList.getById(id));
ChangeHabitColorCommand command;
command = new ChangeHabitColorCommand(habitList, selected, color);
command.setId(id);
return command;
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import com.google.gson.*;
import org.isoron.uhabits.core.utils.*;
/**
* 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
{
private String id;
private boolean isRemote;
public Command()
{
id = StringUtils.getRandomId();
isRemote = false;
}
public Command(String id)
{
this.id = id;
isRemote = false;
}
public abstract void execute();
public String getId()
{
return id;
}
public void setId(String id)
{
this.id = id;
}
public boolean isRemote()
{
return isRemote;
}
public void setRemote(boolean remote)
{
isRemote = remote;
}
@NonNull
public String toJson()
{
return new GsonBuilder().create().toJson(toRecord());
}
@NonNull
public abstract Object toRecord();
public abstract void undo();
}

View File

@@ -0,0 +1,85 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import com.google.gson.*;
import org.isoron.uhabits.core.models.*;
import javax.inject.*;
public class CommandParser
{
private HabitList habitList;
private ModelFactory modelFactory;
@Inject
public CommandParser(@NonNull HabitList habitList,
@NonNull ModelFactory modelFactory)
{
this.habitList = habitList;
this.modelFactory = modelFactory;
}
@NonNull
public Command parse(@NonNull String json)
{
JsonObject parsed = new JsonParser().parse(json).getAsJsonObject();
String event = parsed.get("event").getAsString();
Gson gson = new GsonBuilder().create();
if (event.equals("Archive")) return gson
.fromJson(json, ArchiveHabitsCommand.Record.class)
.toCommand(habitList);
if (event.equals("ChangeColor")) return gson
.fromJson(json, ChangeHabitColorCommand.Record.class)
.toCommand(habitList);
if (event.equals("CreateHabit")) return gson
.fromJson(json, CreateHabitCommand.Record.class)
.toCommand(modelFactory, habitList);
if (event.equals("CreateRep")) return gson
.fromJson(json, CreateRepetitionCommand.Record.class)
.toCommand(habitList);
if (event.equals("DeleteHabit")) return gson
.fromJson(json, DeleteHabitsCommand.Record.class)
.toCommand(habitList);
if (event.equals("EditHabit")) return gson
.fromJson(json, EditHabitCommand.Record.class)
.toCommand(modelFactory, habitList);
if (event.equals("Toggle")) return gson
.fromJson(json, ToggleRepetitionCommand.Record.class)
.toCommand(habitList);
if (event.equals("Unarchive")) return gson
.fromJson(json, UnarchiveHabitsCommand.Record.class)
.toCommand(habitList);
throw new IllegalStateException("Unknown command");
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.tasks.*;
import java.util.*;
import javax.inject.*;
/**
* A CommandRunner executes and undoes commands.
* <p>
* CommandRunners also allows objects to subscribe to it, and receive events
* whenever a command is performed.
*/
@AppScope
public class CommandRunner
{
private TaskRunner taskRunner;
private LinkedList<Listener> listeners;
@Inject
public CommandRunner(@NonNull TaskRunner taskRunner)
{
this.taskRunner = taskRunner;
listeners = new LinkedList<>();
}
public void addListener(Listener l)
{
listeners.add(l);
}
public void execute(final Command command, final Long refreshKey)
{
taskRunner.execute(new Task()
{
@Override
public void doInBackground()
{
command.execute();
}
@Override
public void onPostExecute()
{
for (Listener l : listeners)
l.onCommandExecuted(command, refreshKey);
}
});
}
public void removeListener(Listener l)
{
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,
@Nullable Long refreshKey);
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.commands;
import android.support.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.models.*;
/**
* Command to create a habit.
*/
@AutoFactory
public class CreateHabitCommand extends Command
{
ModelFactory modelFactory;
HabitList habitList;
@NonNull
Habit model;
@Nullable
Long savedId;
public CreateHabitCommand(@Provided @NonNull ModelFactory modelFactory,
@NonNull HabitList habitList,
@NonNull Habit model)
{
this.modelFactory = modelFactory;
this.habitList = habitList;
this.model = model;
}
@Override
public void execute()
{
Habit savedHabit = modelFactory.buildHabit();
savedHabit.copyFrom(model);
savedHabit.setId(savedId);
habitList.add(savedHabit);
savedId = savedHabit.getId();
}
@NonNull
@Override
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
if (savedId == null) throw new IllegalStateException();
Habit habit = habitList.getById(savedId);
if (habit == null) throw new HabitNotFoundException();
habitList.remove(habit);
}
public static class Record
{
@NonNull
public String id;
@NonNull
public String event = "CreateHabit";
@NonNull
public Habit.HabitData habit;
@Nullable
public Long savedId;
public Record(CreateHabitCommand command)
{
id = command.getId();
habit = command.model.getData();
savedId = command.savedId;
}
public CreateHabitCommand toCommand(@NonNull ModelFactory modelFactory,
@NonNull HabitList habitList)
{
Habit h = modelFactory.buildHabit(habit);
CreateHabitCommand command;
command = new CreateHabitCommand(modelFactory, habitList, h);
command.savedId = savedId;
command.setId(id);
return command;
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
/**
* Command to toggle a repetition.
*/
public class CreateRepetitionCommand extends Command
{
@NonNull
final Habit habit;
final Timestamp timestamp;
final int value;
@Nullable
Repetition previousRep;
@Nullable
Repetition newRep;
public CreateRepetitionCommand(@NonNull Habit habit,
Timestamp timestamp,
int value)
{
this.timestamp = timestamp;
this.habit = habit;
this.value = value;
}
@Override
public void execute()
{
RepetitionList reps = habit.getRepetitions();
previousRep = reps.getByTimestamp(timestamp);
if (previousRep != null) reps.remove(previousRep);
newRep = new Repetition(timestamp, value);
reps.add(newRep);
habit.invalidateNewerThan(timestamp);
}
@NonNull
public Habit getHabit()
{
return habit;
}
@Override
@NonNull
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
if(newRep == null) throw new IllegalStateException();
habit.getRepetitions().remove(newRep);
if (previousRep != null) habit.getRepetitions().add(previousRep);
habit.invalidateNewerThan(timestamp);
}
public static class Record
{
@NonNull
public String id;
@NonNull
public String event = "CreateRep";
public long habit;
public long repTimestamp;
public int value;
public Record(CreateRepetitionCommand command)
{
id = command.getId();
Long habitId = command.habit.getId();
if(habitId == null) throw new RuntimeException("Habit not saved");
this.habit = habitId;
this.repTimestamp = command.timestamp.getUnixTime();
this.value = command.value;
}
public CreateRepetitionCommand toCommand(@NonNull HabitList habitList)
{
Habit h = habitList.getById(habit);
if(h == null) throw new HabitNotFoundException();
CreateRepetitionCommand command;
command = new CreateRepetitionCommand(
h, new Timestamp(repTimestamp), value);
command.setId(id);
return command;
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
/**
* Command to delete a list of habits.
*/
public class DeleteHabitsCommand extends Command
{
@NonNull
final HabitList habitList;
@NonNull
final List<Habit> selected;
public DeleteHabitsCommand(@NonNull HabitList habitList,
@NonNull List<Habit> selected)
{
this.selected = new LinkedList<>(selected);
this.habitList = habitList;
}
@Override
public void execute()
{
for (Habit h : selected)
habitList.remove(h);
}
public List<Habit> getSelected()
{
return Collections.unmodifiableList(selected);
}
@Override
@NonNull
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
throw new UnsupportedOperationException();
}
public static class Record
{
@NonNull
public String id;
@NonNull
public String event = "DeleteHabit";
@NonNull
public List<Long> habits;
public Record(DeleteHabitsCommand command)
{
id = command.getId();
habits = new LinkedList<>();
for (Habit h : command.selected)
{
if (!h.hasId()) throw new RuntimeException("Habit not saved");
habits.add(h.getId());
}
}
public DeleteHabitsCommand toCommand(@NonNull HabitList habitList)
{
List<Habit> selected = new LinkedList<>();
for (Long id : this.habits) selected.add(habitList.getById(id));
DeleteHabitsCommand command;
command = new DeleteHabitsCommand(habitList, selected);
command.setId(id);
return command;
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.models.*;
/**
* Command to modify a habit.
*/
@AutoFactory
public class EditHabitCommand extends Command
{
@NonNull
final HabitList habitList;
@NonNull
final Habit original;
@NonNull
final Habit modified;
final long savedId;
final boolean hasFrequencyChanged;
final boolean hasTargetChanged;
public EditHabitCommand(@Provided @NonNull ModelFactory modelFactory,
@NonNull HabitList habitList,
@NonNull Habit original,
@NonNull Habit modified)
{
Long habitId = original.getId();
if (habitId == null) throw new RuntimeException("Habit not saved");
this.savedId = habitId;
this.habitList = habitList;
this.modified = modelFactory.buildHabit();
this.original = modelFactory.buildHabit();
this.modified.copyFrom(modified);
this.original.copyFrom(original);
Frequency originalFreq = this.original.getFrequency();
Frequency modifiedFreq = this.modified.getFrequency();
hasFrequencyChanged = (!originalFreq.equals(modifiedFreq));
hasTargetChanged =
(original.getTargetType() != modified.getTargetType() ||
original.getTargetValue() != modified.getTargetValue());
}
@Override
public void execute()
{
copyAttributes(this.modified);
}
@NonNull
@Override
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
copyAttributes(this.original);
}
private void copyAttributes(Habit model)
{
Habit habit = habitList.getById(savedId);
if (habit == null) throw new RuntimeException("Habit not found");
habit.copyFrom(model);
habitList.update(habit);
if (hasFrequencyChanged || hasTargetChanged)
habit.invalidateNewerThan(Timestamp.ZERO);
}
public static class Record
{
@NonNull
public String id;
@NonNull
public String event = "EditHabit";
@NonNull
public Habit.HabitData habit;
public long habitId;
public Record(EditHabitCommand command)
{
id = command.getId();
this.habitId = command.savedId;
this.habit = command.modified.getData();
}
@NonNull
public EditHabitCommand toCommand(@NonNull ModelFactory modelFactory,
@NonNull HabitList habitList)
{
Habit original = habitList.getById(habitId);
if(original == null) throw new HabitNotFoundException();
Habit modified = modelFactory.buildHabit(habit);
EditHabitCommand command;
command = new EditHabitCommand(modelFactory, habitList, original,
modified);
command.setId(id);
return command;
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
/**
* Command to toggle a repetition.
*/
public class ToggleRepetitionCommand extends Command
{
@NonNull
private HabitList list;
final Timestamp timestamp;
@NonNull
final Habit habit;
public ToggleRepetitionCommand(@NonNull HabitList list,
@NonNull Habit habit,
Timestamp timestamp)
{
super();
this.list = list;
this.timestamp = timestamp;
this.habit = habit;
}
@Override
public void execute()
{
habit.getRepetitions().toggle(timestamp);
list.update(habit);
}
@NonNull
public Habit getHabit()
{
return habit;
}
@Override
@NonNull
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
execute();
}
public static class Record
{
@NonNull
public String id;
@NonNull
public String event = "Toggle";
public long habit;
public long repTimestamp;
public Record(@NonNull ToggleRepetitionCommand command)
{
id = command.getId();
Long habitId = command.habit.getId();
if (habitId == null) throw new RuntimeException("Habit not saved");
this.repTimestamp = command.timestamp.getUnixTime();
this.habit = habitId;
}
public ToggleRepetitionCommand toCommand(@NonNull HabitList habitList)
{
Habit h = habitList.getById(habit);
if (h == null) throw new HabitNotFoundException();
ToggleRepetitionCommand command;
command = new ToggleRepetitionCommand(
habitList, h, new Timestamp(repTimestamp));
command.setId(id);
return command;
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.core.commands;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
/**
* Command to unarchive a list of habits.
*/
public class UnarchiveHabitsCommand extends Command
{
@NonNull
final HabitList habitList;
@NonNull
final List<Habit> selected;
public UnarchiveHabitsCommand(@NonNull HabitList habitList,
@NonNull List<Habit> selected)
{
this.selected = new LinkedList<>(selected);
this.habitList = habitList;
}
@Override
public void execute()
{
for (Habit h : selected) h.setArchived(false);
habitList.update(selected);
}
@Override
@NonNull
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
for (Habit h : selected) h.setArchived(true);
habitList.update(selected);
}
public static class Record
{
@NonNull
public final String id;
@NonNull
public final String event = "Unarchive";
@NonNull
public final List<Long> habits;
public Record(@NonNull UnarchiveHabitsCommand command)
{
id = command.getId();
habits = new LinkedList<>();
for (Habit h : command.selected)
{
if (!h.hasId()) throw new RuntimeException("Habit not saved");
habits.add(h.getId());
}
}
@NonNull
public UnarchiveHabitsCommand toCommand(@NonNull HabitList habitList)
{
List<Habit> selected = new LinkedList<>();
for (Long id : this.habits) selected.add(habitList.getById(id));
UnarchiveHabitsCommand command;
command = new UnarchiveHabitsCommand(habitList, selected);
command.setId(id);
return command;
}
}
}

View File

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

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2017 Á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.core.database;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
public @interface Column
{
String name() default "";
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2017 Á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.core.database;
import android.support.annotation.*;
public interface Cursor extends AutoCloseable
{
@Override
void close();
/**
* Moves the cursor forward one row from its current position. Returns
* true if the current position is valid, or false if the cursor is already
* past the last row. The cursor start at position -1, so this method must
* be called first.
*/
boolean moveToNext();
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as an Integer. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
Integer getInt(int index);
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a Long. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
Long getLong(int index);
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a Double. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
Double getDouble(int index);
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a String. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
String getString(int index);
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2017 Á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.core.database;
import java.util.*;
public interface Database
{
Cursor query(String query, String... params);
default void query(String query, ProcessCallback callback)
{
try (Cursor c = query(query)) {
c.moveToNext();
callback.process(c);
}
}
int update(String tableName,
Map<String, Object> values,
String where,
String... params);
Long insert(String tableName, Map<String, Object> values);
void delete(String tableName, String where, String... params);
void execute(String query, Object... params);
void beginTransaction();
void setTransactionSuccessful();
void endTransaction();
void close();
int getVersion();
interface ProcessCallback
{
void process(Cursor cursor);
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2017 Á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.core.database;
import android.support.annotation.*;
import java.io.*;
public interface DatabaseOpener
{
Database open(@NonNull File file);
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2017 Á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.core.database;
import java.sql.*;
public class JdbcCursor implements Cursor
{
private ResultSet resultSet;
public JdbcCursor(ResultSet resultSet)
{
this.resultSet = resultSet;
}
@Override
public void close()
{
try
{
resultSet.close();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public boolean moveToNext()
{
try
{
return resultSet.next();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Integer getInt(int index)
{
try
{
Integer value = resultSet.getInt(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Long getLong(int index)
{
try
{
Long value = resultSet.getLong(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Double getDouble(int index)
{
try
{
Double value = resultSet.getDouble(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public String getString(int index)
{
try
{
String value = resultSet.getString(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2017 Á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.core.database;
import org.apache.commons.lang3.*;
import java.sql.*;
import java.util.*;
public class JdbcDatabase implements Database
{
private Connection connection;
private boolean transactionSuccessful;
public JdbcDatabase(Connection connection)
{
this.connection = connection;
}
@Override
public Cursor query(String query, String... params)
{
try
{
PreparedStatement st = buildStatement(query, params);
return new JdbcCursor(st.executeQuery());
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public int update(String tableName,
Map<String, Object> map,
String where,
String... params)
{
try
{
ArrayList<String> fields = new ArrayList<>();
ArrayList<String> values = new ArrayList<>();
for (Map.Entry<String, Object> entry : map.entrySet())
{
if (entry.getValue() == null) continue;
fields.add(entry.getKey() + "=?");
values.add(entry.getValue().toString());
}
values.addAll(Arrays.asList(params));
String query = String.format("update %s set %s where %s", tableName,
StringUtils.join(fields, ", "), where);
PreparedStatement st = buildStatement(query, values.toArray());
return st.executeUpdate();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Long insert(String tableName, Map<String, Object> map)
{
try
{
ArrayList<String> fields = new ArrayList<>();
ArrayList<Object> params = new ArrayList<>();
ArrayList<String> questionMarks = new ArrayList<>();
for (Map.Entry<String, Object> entry : map.entrySet())
{
if (entry.getValue() == null) continue;
fields.add(entry.getKey());
params.add(entry.getValue());
questionMarks.add("?");
}
String query =
String.format("insert into %s(%s) values(%s)", tableName,
StringUtils.join(fields, ", "),
StringUtils.join(questionMarks, ", "));
PreparedStatement st = buildStatement(query, params.toArray());
st.execute();
Long id = null;
ResultSet keys = st.getGeneratedKeys();
if (keys.next()) id = keys.getLong(1);
return id;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public void delete(String tableName, String where, String... params)
{
String query =
String.format("delete from %s where %s", tableName, where);
execute(query, (Object[]) params);
}
@Override
public void execute(String query, Object... params)
{
try
{
buildStatement(query, params).execute();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
private PreparedStatement buildStatement(String query, Object[] params)
throws SQLException
{
PreparedStatement st = connection.prepareStatement(query);
int index = 1;
for (Object param : params) st.setString(index++, param.toString());
return st;
}
@Override
public synchronized void beginTransaction()
{
try
{
connection.setAutoCommit(false);
transactionSuccessful = false;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public synchronized void setTransactionSuccessful()
{
transactionSuccessful = true;
}
@Override
public synchronized void endTransaction()
{
try
{
if (transactionSuccessful) connection.commit();
else connection.rollback();
connection.setAutoCommit(true);
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public void close()
{
try
{
connection.close();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public int getVersion()
{
try (Cursor c = query("PRAGMA user_version"))
{
c.moveToNext();
return c.getInt(0);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2017 Á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.core.database;
import android.support.annotation.*;
import java.io.*;
import java.util.*;
import java.util.logging.*;
public class MigrationHelper
{
private static final Logger LOGGER =
Logger.getLogger(MigrationHelper.class.getName());
private final Database db;
public MigrationHelper(@NonNull Database db)
{
this.db = db;
}
public void migrateTo(int newVersion)
{
try
{
for (int v = db.getVersion() + 1; v <= newVersion; v++)
{
String fname = String.format(Locale.US, "/migrations/%02d.sql", v);
for (String command : SQLParser.parse(open(fname)))
db.execute(command);
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@NonNull
private InputStream open(String fname) throws IOException
{
InputStream resource = getClass().getResourceAsStream(fname);
if(resource != null) return resource;
// Workaround for bug in Android Studio / IntelliJ. Removing this
// causes unit tests to fail when run from within the IDE, although
// everything works fine from the command line.
File file = new File("uhabits-core/src/main/resources/" + fname);
if(file.exists()) return new FileInputStream(file);
throw new RuntimeException("resource not found: " + fname);
}
}

View File

@@ -0,0 +1,355 @@
/*
* Copyright (C) 2017 Á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.core.database;
import android.support.annotation.*;
import org.apache.commons.lang3.*;
import org.apache.commons.lang3.tuple.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class Repository<T>
{
@NonNull
private final Class klass;
@NonNull
private final Database db;
public Repository(@NonNull Class<T> klass, @NonNull Database db)
{
this.klass = klass;
this.db = db;
}
/**
* Returns the record that has the id provided.
* If no record is found, returns null.
*/
@Nullable
public T find(@NonNull Long id)
{
return findFirst(String.format("where %s=?", getIdName()),
id.toString());
}
/**
* Returns all records matching the given SQL query.
* <p>
* The query should only contain the "where" part of the SQL query, and
* optinally the "order by" part. "Group by" is not allowed. If no matching
* records are found, returns an empty list.
*/
@NonNull
public List<T> findAll(@NonNull String query, @NonNull String... params)
{
try (Cursor c = db.query(buildSelectQuery() + query, params))
{
return cursorToMultipleRecords(c);
}
}
/**
* Returns the first record matching the given SQL query.
* See findAll for more details about the parameters.
*/
@Nullable
public T findFirst(String query, String... params)
{
try (Cursor c = db.query(buildSelectQuery() + query, params))
{
if (!c.moveToNext()) return null;
return cursorToSingleRecord(c);
}
}
/**
* Executes the given SQL query on the repository.
* <p>
* The query can be of any kind. For example, complex deletes and updates
* are allowed. The repository does not perform any checks to guarantee
* that the query is valid, however the underlying database might.
*/
public void execSQL(String query, Object... params)
{
db.execute(query, params);
}
/**
* Executes the given callback inside a database transaction.
* <p>
* If the callback terminates without throwing any exceptions, the
* transaction is considered successful. If any exceptions are thrown,
* the transaction is aborted. Nesting transactions is not allowed.
*/
public void executeAsTransaction(Runnable callback)
{
db.beginTransaction();
try
{
callback.run();
db.setTransactionSuccessful();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
db.endTransaction();
}
}
/**
* Saves the record on the database.
* <p>
* If the id of the given record is null, it is assumed that the record has
* not been inserted in the repository yet. The record will be inserted, a
* new id will be automatically generated, and the id of the given record
* will be updated.
* <p>
* If the given record has a non-null id, then an update will be performed
* instead. That is, the previous record will be overwritten by the one
* provided.
*/
public void save(T record)
{
try
{
Field fields[] = getFields();
String columns[] = getColumnNames();
Map<String, Object> values = new HashMap<>();
for (int i = 0; i < fields.length; i++)
values.put(columns[i], fields[i].get(record));
Long id = (Long) getIdField().get(record);
int affectedRows = 0;
if (id != null) affectedRows =
db.update(getTableName(), values, getIdName() + "=?",
id.toString());
if (id == null || affectedRows == 0)
{
id = db.insert(getTableName(), values);
getIdField().set(record, id);
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Removes the given record from the repository.
* The id of the given record is also set to null.
*/
public void remove(T record)
{
try
{
Long id = (Long) getIdField().get(record);
if (id == null) return;
db.delete(getTableName(), getIdName() + "=?", id.toString());
getIdField().set(record, null);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@NonNull
private List<T> cursorToMultipleRecords(Cursor c)
{
List<T> records = new LinkedList<>();
while (c.moveToNext()) records.add(cursorToSingleRecord(c));
return records;
}
@NonNull
private T cursorToSingleRecord(Cursor cursor)
{
try
{
Constructor constructor = klass.getDeclaredConstructors()[0];
constructor.setAccessible(true);
T record = (T) constructor.newInstance();
int index = 0;
for (Field field : getFields())
copyFieldFromCursor(record, field, cursor, index++);
return record;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
private void copyFieldFromCursor(T record, Field field, Cursor c, int index)
throws IllegalAccessException
{
if (field.getType().isAssignableFrom(Integer.class))
field.set(record, c.getInt(index));
else if (field.getType().isAssignableFrom(Long.class))
field.set(record, c.getLong(index));
else if (field.getType().isAssignableFrom(Double.class))
field.set(record, c.getDouble(index));
else if (field.getType().isAssignableFrom(String.class))
field.set(record, c.getString(index));
else throw new RuntimeException(
"Type not supported: " + field.getType().getName() + " " +
field.getName());
}
private String buildSelectQuery()
{
return String.format("select %s from %s ",
StringUtils.join(getColumnNames(), ", "), getTableName());
}
private List<Pair<Field, Column>> getFieldColumnPairs()
{
List<Pair<Field, Column>> fields = new ArrayList<>();
for (Field field : klass.getDeclaredFields())
for (Annotation annotation : field.getAnnotations())
{
if (!(annotation instanceof Column)) continue;
Column column = (Column) annotation;
fields.add(new ImmutablePair<>(field, column));
}
return fields;
}
private Field[] cacheFields = null;
@NonNull
private Field[] getFields()
{
if (cacheFields == null)
{
List<Field> fields = new ArrayList<>();
List<Pair<Field, Column>> columns = getFieldColumnPairs();
for (Pair<Field, Column> pair : columns) fields.add(pair.getLeft());
cacheFields = fields.toArray(new Field[]{});
}
return cacheFields;
}
private String[] cacheColumnNames = null;
@NonNull
private String[] getColumnNames()
{
if (cacheColumnNames == null)
{
List<String> names = new ArrayList<>();
List<Pair<Field, Column>> columns = getFieldColumnPairs();
for (Pair<Field, Column> pair : columns)
{
String cname = pair.getRight().name();
if (cname.isEmpty()) cname = pair.getLeft().getName();
if (names.contains(cname))
throw new RuntimeException("duplicated column : " + cname);
names.add(cname);
}
cacheColumnNames = names.toArray(new String[]{});
}
return cacheColumnNames;
}
private String cacheTableName = null;
@NonNull
private String getTableName()
{
if (cacheTableName == null)
{
String name = getTableAnnotation().name();
if (name.isEmpty()) throw new RuntimeException("Table name is empty");
cacheTableName = name;
}
return cacheTableName;
}
private String cacheIdName = null;
@NonNull
private String getIdName()
{
if (cacheIdName == null)
{
String id = getTableAnnotation().id();
if (id.isEmpty()) throw new RuntimeException("Table id is empty");
cacheIdName = id;
}
return cacheIdName;
}
private Field cacheIdField = null;
@NonNull
private Field getIdField()
{
if (cacheIdField == null)
{
Field fields[] = getFields();
String idName = getIdName();
for (Field f : fields)
if (f.getName().equals(idName))
{
cacheIdField = f;
break;
}
if (cacheIdField == null)
throw new RuntimeException("Field not found: " + idName);
}
return cacheIdField;
}
@NonNull
private Table getTableAnnotation()
{
Table t = null;
for (Annotation annotation : klass.getAnnotations())
{
if (!(annotation instanceof Table)) continue;
t = (Table) annotation;
break;
}
if (t == null) throw new RuntimeException("Table annotation not found");
return t;
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2014 Markus Pfeiffer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.isoron.uhabits.core.database;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
class Tokenizer {
private final InputStream mStream;
private boolean mIsNext;
private int mCurrent;
public Tokenizer(final InputStream in) {
this.mStream = in;
}
public boolean hasNext() throws IOException {
if (!this.mIsNext) {
this.mIsNext = true;
this.mCurrent = this.mStream.read();
}
return this.mCurrent != -1;
}
public int next() throws IOException {
if (!this.mIsNext) {
this.mCurrent = this.mStream.read();
}
this.mIsNext = false;
return this.mCurrent;
}
public boolean skip(final String s) throws IOException {
if (s == null || s.length() == 0) {
return false;
}
if (s.charAt(0) != this.mCurrent) {
return false;
}
final int len = s.length();
this.mStream.mark(len - 1);
for (int n = 1; n < len; n++) {
final int value = this.mStream.read();
if (value != s.charAt(n)) {
this.mStream.reset();
return false;
}
}
return true;
}
}
public class SQLParser {
public final static int STATE_NONE = 0;
public final static int STATE_STRING = 1;
public final static int STATE_COMMENT = 2;
public final static int STATE_COMMENT_BLOCK = 3;
public static List<String> parse(final InputStream stream) throws IOException {
final BufferedInputStream buffer = new BufferedInputStream(stream);
final List<String> commands = new ArrayList<String>();
final StringBuffer sb = new StringBuffer();
try {
final Tokenizer tokenizer = new Tokenizer(buffer);
int state = STATE_NONE;
while (tokenizer.hasNext()) {
final char c = (char) tokenizer.next();
if (state == STATE_COMMENT_BLOCK) {
if (tokenizer.skip("*/")) {
state = STATE_NONE;
}
continue;
} else if (state == STATE_COMMENT) {
if (isNewLine(c)) {
state = STATE_NONE;
}
continue;
} else if (state == STATE_NONE && tokenizer.skip("/*")) {
state = STATE_COMMENT_BLOCK;
continue;
} else if (state == STATE_NONE && tokenizer.skip("--")) {
state = STATE_COMMENT;
continue;
} else if (state == STATE_NONE && c == ';') {
final String command = sb.toString().trim();
commands.add(command);
sb.setLength(0);
continue;
} else if (state == STATE_NONE && c == '\'') {
state = STATE_STRING;
} else if (state == STATE_STRING && c == '\'') {
state = STATE_NONE;
}
if (state == STATE_NONE || state == STATE_STRING) {
if (state == STATE_NONE && isWhitespace(c)) {
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
sb.append(' ');
}
} else {
sb.append(c);
}
}
}
} finally {
buffer.close();
}
if (sb.length() > 0) {
commands.add(sb.toString().trim());
}
return commands;
}
private static boolean isNewLine(final char c) {
return c == '\r' || c == '\n';
}
private static boolean isWhitespace(final char c) {
return c == '\r' || c == '\n' || c == '\t' || c == ' ';
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2017 Á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.core.database;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table
{
String name();
String id() default "id";
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2017 Á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.core.database;
public class UnsupportedDatabaseVersionException extends RuntimeException
{
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2017 Á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.core.io;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.io.*;
import java.util.*;
/**
* AbstractImporter is the base class for all classes that import data from
* files into the app.
*/
public abstract class AbstractImporter
{
protected final HabitList habitList;
public AbstractImporter(HabitList habitList)
{
this.habitList = habitList;
}
public abstract boolean canHandle(@NonNull File file) throws IOException;
public abstract void importHabitsFromFile(@NonNull File file) throws IOException;
public static boolean isSQLite3File(@NonNull File file) throws IOException
{
FileInputStream fis = new FileInputStream(file);
byte[] sqliteHeader = "SQLite format 3".getBytes();
byte[] buffer = new byte[sqliteHeader.length];
int count = fis.read(buffer);
if(count < sqliteHeader.length) return false;
return Arrays.equals(buffer, sqliteHeader);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2017 Á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.core.io;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
/**
* 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;
@Inject
public GenericImporter(@NonNull HabitList habits,
@NonNull LoopDBImporter loopDBImporter,
@NonNull RewireDBImporter rewireDBImporter,
@NonNull TickmateDBImporter tickmateDBImporter,
@NonNull HabitBullCSVImporter habitBullCSVImporter)
{
super(habits);
importers = new LinkedList<>();
importers.add(loopDBImporter);
importers.add(rewireDBImporter);
importers.add(tickmateDBImporter);
importers.add(habitBullCSVImporter);
}
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
for (AbstractImporter importer : importers)
if (importer.canHandle(file)) return true;
return false;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
for (AbstractImporter importer : importers)
if (importer.canHandle(file)) importer.importHabitsFromFile(file);
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2017 Á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.core.io;
import android.support.annotation.*;
import com.opencsv.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
/**
* Class that imports data from HabitBull CSV files.
*/
public class HabitBullCSVImporter extends AbstractImporter
{
private ModelFactory modelFactory;
@Inject
public HabitBullCSVImporter(@NonNull HabitList habits,
@NonNull ModelFactory modelFactory)
{
super(habits);
this.modelFactory = modelFactory;
}
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
BufferedReader reader = new BufferedReader(new FileReader(file));
String line = reader.readLine();
return line.startsWith("HabitName,HabitDescription,HabitCategory");
}
@Override
public void importHabitsFromFile(@NonNull final File file)
throws IOException
{
CSVReader reader = new CSVReader(new FileReader(file));
HashMap<String, Habit> map = new HashMap<>();
for (String line[] : reader)
{
String name = line[0];
if (name.equals("HabitName")) continue;
String description = line[1];
String dateString[] = line[3].split("-");
int year = Integer.parseInt(dateString[0]);
int month = Integer.parseInt(dateString[1]);
int day = Integer.parseInt(dateString[2]);
Calendar date = DateUtils.getStartOfTodayCalendar();
date.set(year, month - 1, day);
Timestamp timestamp = new Timestamp(date.getTimeInMillis());
int value = Integer.parseInt(line[4]);
if (value != 1) continue;
Habit h = map.get(name);
if (h == null)
{
h = modelFactory.buildHabit();
h.setName(name);
h.setDescription(description);
h.setFrequency(Frequency.DAILY);
habitList.add(h);
map.put(name, h);
}
if (!h.getRepetitions().containsTimestamp(timestamp))
h.getRepetitions().toggle(timestamp);
}
}
}

View File

@@ -0,0 +1,275 @@
/*
* 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.core.io;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.text.*;
import java.util.*;
import java.util.zip.*;
/**
* Class that exports the application data to CSV files.
*/
public class HabitsCSVExporter
{
private List<Habit> selectedHabits;
private List<String> generateDirs;
private List<String> generateFilenames;
private String exportDirName;
/**
* Delimiter used in a CSV file.
*/
private final String DELIMITER = ",";
@NonNull
private final HabitList allHabits;
public HabitsCSVExporter(@NonNull HabitList allHabits,
@NonNull List<Habit> selectedHabits,
@NonNull File dir)
{
this.allHabits = allHabits;
this.selectedHabits = selectedHabits;
this.exportDirName = dir.getAbsolutePath() + "/";
generateDirs = new LinkedList<>();
generateFilenames = new LinkedList<>();
}
public String writeArchive() throws IOException
{
String zipFilename;
writeHabits();
zipFilename = writeZipFile();
cleanup();
return zipFilename;
}
private void addFileToZip(ZipOutputStream zos, String filename)
throws IOException
{
FileInputStream fis =
new FileInputStream(new File(exportDirName + filename));
ZipEntry ze = new ZipEntry(filename);
zos.putNextEntry(ze);
int length;
byte bytes[] = new byte[1024];
while ((length = fis.read(bytes)) >= 0) zos.write(bytes, 0, length);
zos.closeEntry();
fis.close();
}
private void cleanup()
{
for (String filename : generateFilenames)
new File(exportDirName + filename).delete();
for (String filename : generateDirs)
new File(exportDirName + filename).delete();
new File(exportDirName).delete();
}
@NonNull
private String sanitizeFilename(String name)
{
String s = name.replaceAll("[^ a-zA-Z0-9\\._-]+", "");
return s.substring(0, Math.min(s.length(), 100));
}
private void writeHabits() throws IOException
{
String filename = "Habits.csv";
new File(exportDirName).mkdirs();
FileWriter out = new FileWriter(exportDirName + filename);
generateFilenames.add(filename);
allHabits.writeCSV(out);
out.close();
for (Habit h : selectedHabits)
{
String sane = sanitizeFilename(h.getName());
String habitDirName =
String.format("%03d %s", allHabits.indexOf(h) + 1, sane);
habitDirName = habitDirName.trim() + "/";
new File(exportDirName + habitDirName).mkdirs();
generateDirs.add(habitDirName);
writeScores(habitDirName, h.getScores());
writeCheckmarks(habitDirName, h.getCheckmarks());
}
writeMultipleHabits();
}
private void writeScores(String habitDirName, ScoreList scores)
throws IOException
{
String path = habitDirName + "Scores.csv";
FileWriter out = new FileWriter(exportDirName + path);
generateFilenames.add(path);
scores.writeCSV(out);
out.close();
}
private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks)
throws IOException
{
String filename = habitDirName + "Checkmarks.csv";
FileWriter out = new FileWriter(exportDirName + filename);
generateFilenames.add(filename);
checkmarks.writeCSV(out);
out.close();
}
/**
* Writes a scores file and a checkmarks file containing scores and checkmarks of every habit.
* The first column corresponds to the date. Subsequent columns correspond to a habit.
* Habits are taken from the list of selected habits.
* Dates are determined from the oldest repetition date to the newest repetition date found in
* the list of habits.
*
* @throws IOException if there was problem writing the files
*/
private void writeMultipleHabits() throws IOException
{
String scoresFileName = "Scores.csv";
String checksFileName = "Checkmarks.csv";
generateFilenames.add(scoresFileName);
generateFilenames.add(checksFileName);
FileWriter scoresWriter = new FileWriter(exportDirName + scoresFileName);
FileWriter checksWriter = new FileWriter(exportDirName + checksFileName);
writeMultipleHabitsHeader(scoresWriter);
writeMultipleHabitsHeader(checksWriter);
Timestamp[] timeframe = getTimeframe();
Timestamp oldest = timeframe[0];
Timestamp newest = DateUtils.getToday();
List<int[]> checkmarks = new ArrayList<>();
List<double[]> scores = new ArrayList<>();
for (Habit h : selectedHabits)
{
checkmarks.add(h.getCheckmarks().getValues(oldest, newest));
scores.add(h.getScores().getValues(oldest, newest));
}
int days = oldest.daysUntil(newest);
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
for (int i = 0; i <= days; i++)
{
Date day = newest.minus(i).toJavaDate();
String date = dateFormat.format(day);
StringBuilder sb = new StringBuilder();
sb.append(date).append(DELIMITER);
checksWriter.write(sb.toString());
scoresWriter.write(sb.toString());
for(int j = 0; j < selectedHabits.size(); j++)
{
checksWriter.write(String.valueOf(checkmarks.get(j)[i]));
checksWriter.write(DELIMITER);
String score =
String.format("%.4f", ((float) scores.get(j)[i]));
scoresWriter.write(score);
scoresWriter.write(DELIMITER);
}
checksWriter.write("\n");
scoresWriter.write("\n");
}
scoresWriter.close();
checksWriter.close();
}
/**
* Writes the first row, containing header information, using the given writer.
* This consists of the date title and the names of the selected habits.
*
* @param out the writer to use
* @throws IOException if there was a problem writing
*/
private void writeMultipleHabitsHeader(Writer out) throws IOException
{
out.write("Date" + DELIMITER);
for (Habit h : selectedHabits) {
out.write(h.getName());
out.write(DELIMITER);
}
out.write("\n");
}
/**
* Gets the overall timeframe of the selected habits.
* The timeframe is an array containing the oldest timestamp among the habits and the
* newest timestamp among the habits.
* Both timestamps are in milliseconds.
*
* @return the timeframe containing the oldest timestamp and the newest timestamp
*/
private Timestamp[] getTimeframe()
{
Timestamp oldest = Timestamp.ZERO.plus(1000000);
Timestamp newest = Timestamp.ZERO;
for (Habit h : selectedHabits)
{
if(h.getRepetitions().getOldest() == null || h.getRepetitions().getNewest() == null)
continue;
Timestamp currOld = h.getRepetitions().getOldest().getTimestamp();
Timestamp currNew = h.getRepetitions().getNewest().getTimestamp();
oldest = currOld.isOlderThan(oldest) ? oldest : currOld;
newest = currNew.isNewerThan(newest) ? newest : currNew;
}
return new Timestamp[]{oldest, newest};
}
private String writeZipFile() throws IOException
{
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
String date = dateFormat.format(DateUtils.getStartOfToday());
String zipFilename =
String.format("%s/Loop Habits CSV %s.zip", exportDirName, date);
FileOutputStream fos = new FileOutputStream(zipFilename);
ZipOutputStream zos = new ZipOutputStream(fos);
for (String filename : generateFilenames)
addFileToZip(zos, filename);
zos.close();
fos.close();
return zipFilename;
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2017 Á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.core.io;
import android.support.annotation.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.sqlite.records.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
import static org.isoron.uhabits.core.Config.*;
/**
* Class that imports data from database files exported by Loop Habit Tracker.
*/
public class LoopDBImporter extends AbstractImporter
{
@NonNull
private final ModelFactory modelFactory;
@NonNull
private final DatabaseOpener opener;
@Inject
public LoopDBImporter(@NonNull HabitList habitList,
@NonNull ModelFactory modelFactory,
@NonNull DatabaseOpener opener)
{
super(habitList);
this.modelFactory = modelFactory;
this.opener = opener;
}
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if (!isSQLite3File(file)) return false;
Database db = opener.open(file);
boolean canHandle = true;
Cursor c = db.query("select count(*) from SQLITE_MASTER " +
"where name='Habits' or name='Repetitions'");
if (!c.moveToNext() || c.getInt(0) != 2)
{
// Log.w("LoopDBImporter", "Cannot handle file: tables not found");
canHandle = false;
}
if (db.getVersion() > DATABASE_VERSION)
{
// Log.w("LoopDBImporter", String.format(
// "Cannot handle file: incompatible version: %d > %d",
// db.getVersion(), DATABASE_VERSION));
canHandle = false;
}
c.close();
db.close();
return canHandle;
}
@Override
public synchronized void importHabitsFromFile(@NonNull File file)
throws IOException
{
Database db = opener.open(file);
MigrationHelper helper = new MigrationHelper(db);
helper.migrateTo(DATABASE_VERSION);
Repository<HabitRecord> habitsRepository;
Repository<RepetitionRecord> repsRepository;
habitsRepository = new Repository<>(HabitRecord.class, db);
repsRepository = new Repository<>(RepetitionRecord.class, db);
for (HabitRecord habitRecord : habitsRepository.findAll(
"order by position"))
{
Habit h = modelFactory.buildHabit();
habitRecord.copyTo(h);
h.setId(null);
habitList.add(h);
List<RepetitionRecord> reps =
repsRepository.findAll("where habit = ?",
habitRecord.id.toString());
for (RepetitionRecord r : reps)
h.getRepetitions().toggle(new Timestamp(r.timestamp), r.value);
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2017 Á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.core.io;
import android.support.annotation.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
/**
* Class that imports database files exported by Rewire.
*/
public class RewireDBImporter extends AbstractImporter
{
private ModelFactory modelFactory;
@NonNull
private final DatabaseOpener opener;
@Inject
public RewireDBImporter(@NonNull HabitList habits,
@NonNull ModelFactory modelFactory,
@NonNull DatabaseOpener opener)
{
super(habits);
this.modelFactory = modelFactory;
this.opener = opener;
}
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if (!isSQLite3File(file)) return false;
Database db = opener.open(file);
Cursor c = db.query("select count(*) from SQLITE_MASTER " +
"where name='CHECKINS' or name='UNIT'");
boolean result = (c.moveToNext() && c.getInt(0) == 2);
c.close();
db.close();
return result;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
Database db = opener.open(file);
db.beginTransaction();
createHabits(db);
db.setTransactionSuccessful();
db.endTransaction();
db.close();
}
private void createHabits(Database db)
{
Cursor c = null;
try
{
c = db.query("select _id, name, description, schedule, " +
"active_days, repeating_count, days, period " +
"from habits");
if (!c.moveToNext()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
int schedule = c.getInt(3);
String activeDays = c.getString(4);
int repeatingCount = c.getInt(5);
int days = c.getInt(6);
int periodIndex = c.getInt(7);
Habit habit = modelFactory.buildHabit();
habit.setName(name);
habit.setDescription(description);
int periods[] = { 7, 31, 365 };
int numerator, denominator;
switch (schedule)
{
case 0:
numerator = activeDays.split(",").length;
denominator = 7;
break;
case 1:
numerator = days;
denominator = (periods[periodIndex]);
break;
case 2:
numerator = 1;
denominator = repeatingCount;
break;
default:
throw new IllegalStateException();
}
habit.setFrequency(new Frequency(numerator, denominator));
habitList.add(habit);
createReminder(db, habit, id);
createCheckmarks(db, habit, id);
} while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createCheckmarks(@NonNull Database db,
@NonNull Habit habit,
int rewireHabitId)
{
Cursor c = null;
try
{
String[] params = { Integer.toString(rewireHabitId) };
c = db.query(
"select distinct date from checkins where habit_id=? and type=2",
params);
if (!c.moveToNext()) return;
do
{
String date = c.getString(0);
int year = Integer.parseInt(date.substring(0, 4));
int month = Integer.parseInt(date.substring(4, 6));
int day = Integer.parseInt(date.substring(6, 8));
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, month - 1, day);
habit.getRepetitions().toggle(new Timestamp(cal));
} while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createReminder(Database db, Habit habit, int rewireHabitId)
{
String[] params = { Integer.toString(rewireHabitId) };
Cursor c = null;
try
{
c = db.query(
"select time, active_days from reminders where habit_id=? limit 1",
params);
if (!c.moveToNext()) return;
int rewireReminder = Integer.parseInt(c.getString(0));
if (rewireReminder <= 0 || rewireReminder >= 1440) return;
boolean reminderDays[] = new boolean[7];
String activeDays[] = c.getString(1).split(",");
for (String d : activeDays)
{
int idx = (Integer.parseInt(d) + 1) % 7;
reminderDays[idx] = true;
}
int hour = rewireReminder / 60;
int minute = rewireReminder % 60;
WeekdayList days = new WeekdayList(reminderDays);
Reminder reminder = new Reminder(hour, minute, days);
habit.setReminder(reminder);
habitList.update(habit);
}
finally
{
if (c != null) c.close();
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2017 Á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.core.io;
import android.support.annotation.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
/**
* Class that imports data from database files exported by Tickmate.
*/
public class TickmateDBImporter extends AbstractImporter
{
private ModelFactory modelFactory;
@NonNull
private final DatabaseOpener opener;
@Inject
public TickmateDBImporter(@NonNull HabitList habits,
@NonNull ModelFactory modelFactory,
@NonNull DatabaseOpener opener)
{
super(habits);
this.modelFactory = modelFactory;
this.opener = opener;
}
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if (!isSQLite3File(file)) return false;
Database db = opener.open(file);
Cursor c = db.query("select count(*) from SQLITE_MASTER " +
"where name='tracks' or name='track2groups'");
boolean result = (c.moveToNext() && c.getInt(0) == 2);
c.close();
db.close();
return result;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
final Database db = opener.open(file);
db.beginTransaction();
createHabits(db);
db.setTransactionSuccessful();
db.endTransaction();
db.close();
}
private void createCheckmarks(@NonNull Database db,
@NonNull Habit habit,
int tickmateTrackId)
{
Cursor c = null;
try
{
String[] params = {Integer.toString(tickmateTrackId)};
c = db.query(
"select distinct year, month, day from ticks where _track_id=?",
params);
if (!c.moveToNext()) return;
do
{
int year = c.getInt(0);
int month = c.getInt(1);
int day = c.getInt(2);
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, month, day);
habit.getRepetitions().toggle(new Timestamp(cal));
} while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createHabits(Database db)
{
Cursor c = null;
try
{
c = db.query("select _id, name, description from tracks",
new String[0]);
if (!c.moveToNext()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
Habit habit = modelFactory.buildHabit();
habit.setName(name);
habit.setDescription(description);
habit.setFrequency(Frequency.DAILY);
habitList.add(habit);
createCheckmarks(db, habit, id);
} while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.core.models;
import org.apache.commons.lang3.builder.*;
import javax.annotation.concurrent.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* 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.
*/
@ThreadSafe
public final class Checkmark
{
/**
* Indicates that there was a repetition at the timestamp.
*/
public static final int CHECKED_EXPLICITLY = 2;
/**
* 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 no repetition at the timestamp, even though a
* repetition was expected.
*/
public static final int UNCHECKED = 0;
private final Timestamp timestamp;
/**
* The value of the checkmark.
* <p>
* For boolean habits, this equals either UNCHECKED, CHECKED_EXPLICITLY,
* or CHECKED_IMPLICITLY.
* <p>
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is stored as 1500.
*/
private final int value;
public Checkmark(Timestamp timestamp, int value)
{
this.timestamp = timestamp;
this.value = value;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Checkmark checkmark = (Checkmark) o;
return new EqualsBuilder()
.append(timestamp, checkmark.timestamp)
.append(value, checkmark.value)
.isEquals();
}
public Timestamp getTimestamp()
{
return timestamp;
}
public int getValue()
{
return value;
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(timestamp)
.append(value)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
}

View File

@@ -0,0 +1,444 @@
/*
* 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.core.models;
import android.support.annotation.*;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.annotation.concurrent.*;
import static org.isoron.uhabits.core.models.Checkmark.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* The collection of {@link Checkmark}s belonging to a habit.
*/
@ThreadSafe
public abstract class CheckmarkList
{
protected final Habit habit;
public final ModelObservable observable;
public CheckmarkList(Habit habit)
{
this.habit = habit;
this.observable = new ModelObservable();
}
@NonNull
static List<Checkmark> buildCheckmarksFromIntervals(Repetition[] reps,
ArrayList<Interval> intervals)
{
if (reps.length == 0) throw new IllegalArgumentException();
Timestamp today = DateUtils.getToday();
Timestamp begin = reps[0].getTimestamp();
if (intervals.size() > 0) begin = Timestamp.oldest(begin, intervals.get(0).begin);
int nDays = begin.daysUntil(today) + 1;
List<Checkmark> checkmarks = new ArrayList<>(nDays);
for (int i = 0; i < nDays; i++)
checkmarks.add(new Checkmark(today.minus(i), UNCHECKED));
for (Interval interval : intervals)
{
for (int i = 0; i < interval.length(); i++)
{
Timestamp date = interval.begin.plus(i);
int offset = date.daysUntil(today);
if (offset < 0) continue;
checkmarks.set(offset, new Checkmark(date, CHECKED_IMPLICITLY));
}
}
for (Repetition rep : reps)
{
Timestamp date = rep.getTimestamp();
int offset = date.daysUntil(today);
checkmarks.set(offset, new Checkmark(date, CHECKED_EXPLICITLY));
}
return checkmarks;
}
/**
* For non-daily habits, some groups of repetitions generate many
* checkmarks. For example, for weekly habits, each repetition generates
* seven checkmarks. For twice-a-week habits, two repetitions that are close
* enough together also generate seven checkmarks.
* <p>
* This group of generated checkmarks, for a given set of repetition, is
* represented by an interval. This function computes the list of intervals
* for a given list of repetitions. It tries to build the intervals as far
* away in the future as possible.
*/
@NonNull
static ArrayList<Interval> buildIntervals(@NonNull Frequency freq,
@NonNull Repetition[] reps)
{
int num = freq.getNumerator();
int den = freq.getDenominator();
ArrayList<Interval> intervals = new ArrayList<>();
for (int i = 0; i < reps.length - num + 1; i++)
{
Repetition first = reps[i];
Repetition last = reps[i + num - 1];
long distance = first.getTimestamp().daysUntil(last.getTimestamp());
if (distance >= den) continue;
Timestamp begin = first.getTimestamp();
Timestamp center = last.getTimestamp();
Timestamp end = begin.plus(den - 1);
intervals.add(new Interval(begin, center, end));
}
return intervals;
}
/**
* Starting from the oldest interval, this function tries to slide the
* intervals backwards into the past, so that gaps are eliminated and
* streaks are maximized. When it detects that sliding an interval
* would not help fixing any gap, it leaves the interval unchanged.
*/
static void snapIntervalsTogether(@NonNull ArrayList<Interval> intervals)
{
for (int i = 1; i < intervals.size(); i++)
{
Interval curr = intervals.get(i);
Interval prev = intervals.get(i - 1);
int gap = prev.end.daysUntil(curr.begin) - 1;
if (gap <= 0 || curr.end.minus(gap).isOlderThan(curr.center)) continue;
intervals.set(i, new Interval(curr.begin.minus(gap), curr.center,
curr.end.minus(gap)));
}
}
/**
* Adds all the given checkmarks to the list.
* <p>
* This should never be called by the application, since the checkmarks are
* computed automatically from the list of repetitions.
*
* @param checkmarks the checkmarks to be added.
*/
public abstract void add(List<Checkmark> checkmarks);
/**
* Returns the values for all the checkmarks, since the oldest repetition of
* the habit until today.
* <p>
* 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.
*
* @return values for the checkmarks in the interval
*/
@NonNull
public synchronized final int[] getAllValues()
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0];
Timestamp fromTimestamp = oldestRep.getTimestamp();
Timestamp toTimestamp = DateUtils.getToday();
return getValues(fromTimestamp, toTimestamp);
}
/**
* Returns the list of checkmarks that fall within the given interval.
* <p>
* There is exactly one checkmark per day in the interval. The endpoints of
* the interval are included. The list is ordered by timestamp (decreasing).
* That is, the first checkmark corresponds to the newest timestamp, and the
* last checkmark corresponds to the oldest timestamp.
*
* @param fromTimestamp timestamp of the beginning of the interval.
* @param toTimestamp timestamp of the end of the interval.
* @return the list of checkmarks within the interval.
*/
@NonNull
public abstract List<Checkmark> getByInterval(Timestamp fromTimestamp,
Timestamp toTimestamp);
/**
* Returns the checkmark for today.
*
* @return checkmark for today
*/
@Nullable
public synchronized final Checkmark getToday()
{
compute();
Timestamp today = DateUtils.getToday();
return getByInterval(today, today).get(0);
}
/**
* Returns the value of today's checkmark.
*
* @return value of today's checkmark
*/
public synchronized int getTodayValue()
{
Checkmark today = getToday();
if (today != null) return today.getValue();
else return UNCHECKED;
}
/**
* 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 final int[] getValues(Timestamp from, Timestamp to)
{
if (from.isNewerThan(to)) return new int[0];
List<Checkmark> checkmarks = getByInterval(from, to);
int values[] = new int[checkmarks.size()];
int i = 0;
for (Checkmark c : checkmarks)
values[i++] = c.getValue();
return values;
}
/**
* 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(Timestamp timestamp);
/**
* Writes the entire list of checkmarks to the given writer, in CSV format.
*
* @param out the writer where the CSV will be output
* @throws IOException in case write operations fail
*/
public final void writeCSV(Writer out) throws IOException
{
int values[];
synchronized (this)
{
compute();
values = getAllValues();
}
Timestamp timestamp = DateUtils.getToday();
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
for (int value : values)
{
String date = dateFormat.format(timestamp.toJavaDate());
out.write(String.format("%s,%d\n", date, value));
timestamp = timestamp.minus(1);
}
}
/**
* Computes and stores one checkmark for each day, from the first habit
* repetition to today. If this list is already computed, does nothing.
*/
protected final synchronized void compute()
{
final Timestamp today = DateUtils.getToday();
Checkmark newest = getNewestComputed();
if (newest != null && newest.getTimestamp().equals(today)) return;
invalidateNewerThan(Timestamp.ZERO);
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return;
final Timestamp from = oldestRep.getTimestamp();
Repetition reps[] = habit
.getRepetitions()
.getByInterval(from, today)
.toArray(new Repetition[0]);
if (habit.isNumerical()) computeNumerical(reps);
else computeYesNo(reps);
}
/**
* Returns newest checkmark that has already been computed.
*
* @return newest checkmark already computed
*/
@Nullable
protected abstract Checkmark getNewestComputed();
/**
* Returns oldest checkmark that has already been computed.
*
* @return oldest checkmark already computed
*/
@Nullable
protected abstract Checkmark getOldestComputed();
private void computeNumerical(Repetition[] reps)
{
if (reps.length == 0) throw new IllegalArgumentException();
Timestamp today = DateUtils.getToday();
Timestamp begin = reps[0].getTimestamp();
int nDays = begin.daysUntil(today) + 1;
List<Checkmark> checkmarks = new ArrayList<>(nDays);
for (int i = 0; i < nDays; i++)
checkmarks.add(new Checkmark(today.minus(i), 0));
for (Repetition rep : reps)
{
int offset = rep.getTimestamp().daysUntil(today);
checkmarks.set(offset, new Checkmark(rep.getTimestamp(), rep.getValue()));
}
add(checkmarks);
}
private void computeYesNo(Repetition[] reps)
{
ArrayList<Interval> intervals;
intervals = buildIntervals(habit.getFrequency(), reps);
snapIntervalsTogether(intervals);
add(buildCheckmarksFromIntervals(reps, intervals));
}
public List<Checkmark> getAll() {
Repetition oldest = habit.getRepetitions().getOldest();
if(oldest == null) return new ArrayList<>();
return getByInterval(oldest.getTimestamp(), DateUtils.getToday());
}
static final class Interval
{
final Timestamp begin;
final Timestamp center;
final Timestamp end;
Interval(Timestamp begin, Timestamp center, Timestamp end)
{
this.begin = begin;
this.center = center;
this.end = end;
}
public int length() {
return begin.daysUntil(end) + 1;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Interval interval = (Interval) o;
return new EqualsBuilder()
.append(begin, interval.begin)
.append(center, interval.center)
.append(end, interval.end)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(begin)
.append(center)
.append(end)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("begin", begin)
.append("center", center)
.append("end", end)
.toString();
}
}
@NonNull
public List<Checkmark> groupBy(DateUtils.TruncateField field)
{
List<Checkmark> checks = getAll();
int count = 0;
Timestamp truncatedTimestamps[] = new Timestamp[checks.size()];
int values[] = new int[checks.size()];
for (Checkmark rep : checks)
{
Timestamp tt = rep.getTimestamp().truncate(field);
if (count == 0 || !truncatedTimestamps[count - 1].equals(tt))
truncatedTimestamps[count++] = tt;
if(habit.isNumerical())
values[count - 1] += rep.getValue();
else if(rep.getValue() == Checkmark.CHECKED_EXPLICITLY)
values[count - 1] += 1000;
}
ArrayList<Checkmark> groupedCheckmarks = new ArrayList<>();
for (int i = 0; i < count; i++)
{
Checkmark rep = new Checkmark(truncatedTimestamps[i], values[i]);
groupedCheckmarks.add(rep);
}
return groupedCheckmarks;
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.models;
import org.apache.commons.lang3.builder.*;
import javax.annotation.concurrent.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* Represents how often is the habit repeated.
*/
@ThreadSafe
public class Frequency
{
public static final Frequency DAILY = new Frequency(1, 1);
public static final Frequency FIVE_TIMES_PER_WEEK = new Frequency(5, 7);
public static final Frequency THREE_TIMES_PER_WEEK = new Frequency(3, 7);
public static final Frequency TWO_TIMES_PER_WEEK = new Frequency(2, 7);
public static final Frequency WEEKLY = new Frequency(1, 7);
private final int numerator;
private final int denominator;
public Frequency(int numerator, int denominator)
{
if (numerator == denominator) numerator = denominator = 1;
this.numerator = numerator;
this.denominator = denominator;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Frequency frequency = (Frequency) o;
return new EqualsBuilder()
.append(numerator, frequency.numerator)
.append(denominator, frequency.denominator)
.isEquals();
}
public int getDenominator()
{
return denominator;
}
public int getNumerator()
{
return numerator;
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(numerator)
.append(denominator)
.toHashCode();
}
public double toDouble()
{
return (double) numerator / denominator;
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("numerator", numerator)
.append("denominator", denominator)
.toString();
}
}

View File

@@ -0,0 +1,484 @@
/*
* 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.core.models;
import android.support.annotation.*;
import org.apache.commons.lang3.builder.*;
import java.util.*;
import javax.annotation.concurrent.*;
import javax.inject.*;
import static org.isoron.uhabits.core.models.Checkmark.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* The thing that the user wants to track.
*/
@ThreadSafe
public class Habit
{
public static final int AT_LEAST = 0;
public static final int AT_MOST = 1;
public static final String HABIT_URI_FORMAT =
"content://org.isoron.uhabits/habit/%d";
public static final int NUMBER_HABIT = 1;
public static final int YES_NO_HABIT = 0;
@Nullable
public Long id;
@NonNull
private HabitData data;
@NonNull
private StreakList streaks;
@NonNull
private ScoreList scores;
@NonNull
private RepetitionList repetitions;
@NonNull
private CheckmarkList checkmarks;
private ModelObservable observable = new ModelObservable();
/**
* Constructs a habit with default data.
* <p>
* The habit is not archived, not highlighted, has no reminders and is
* placed in the last position of the list of habits.
*/
@Inject
Habit(@NonNull ModelFactory factory)
{
this.data = new HabitData();
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buildRepetitionList(this);
}
Habit(@NonNull ModelFactory factory, @NonNull HabitData data)
{
this.data = new HabitData(data);
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buildRepetitionList(this);
observable = new ModelObservable();
}
/**
* Clears the reminder for a habit.
*/
public synchronized void clearReminder()
{
data.reminder = null;
observable.notifyListeners();
}
/**
* Copies all the attributes of the specified habit into this habit
*
* @param model the model whose attributes should be copied from
*/
public synchronized void copyFrom(@NonNull Habit model)
{
this.data = new HabitData(model.data);
observable.notifyListeners();
}
/**
* List of checkmarks belonging to this habit.
*/
@NonNull
public synchronized CheckmarkList getCheckmarks()
{
return checkmarks;
}
/**
* 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).
*/
@NonNull
public synchronized Integer getColor()
{
return data.color;
}
public synchronized void setColor(@NonNull Integer color)
{
data.color = color;
}
@NonNull
public synchronized String getDescription()
{
return data.description;
}
public synchronized void setDescription(@NonNull String description)
{
data.description = description;
}
@NonNull
public synchronized Frequency getFrequency()
{
return data.frequency;
}
public synchronized void setFrequency(@NonNull Frequency frequency)
{
data.frequency = frequency;
}
@Nullable
public synchronized Long getId()
{
return id;
}
public synchronized void setId(@Nullable Long id)
{
this.id = id;
}
@NonNull
public synchronized String getName()
{
return data.name;
}
public synchronized void setName(@NonNull String name)
{
data.name = name;
}
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns the reminder for this habit.
* <p>
* Before calling this method, you should call {@link #hasReminder()} to
* verify that a reminder does exist, otherwise an exception will be
* thrown.
*
* @return the reminder for this habit
* @throws IllegalStateException if habit has no reminder
*/
@NonNull
public synchronized Reminder getReminder()
{
if (data.reminder == null) throw new IllegalStateException();
return data.reminder;
}
public synchronized void setReminder(@Nullable Reminder reminder)
{
data.reminder = reminder;
}
@NonNull
public RepetitionList getRepetitions()
{
return repetitions;
}
@NonNull
public ScoreList getScores()
{
return scores;
}
@NonNull
public StreakList getStreaks()
{
return streaks;
}
public synchronized int getTargetType()
{
return data.targetType;
}
public synchronized void setTargetType(int targetType)
{
if (targetType != AT_LEAST && targetType != AT_MOST)
throw new IllegalArgumentException(
String.format("invalid targetType: %d", targetType));
data.targetType = targetType;
}
public synchronized double getTargetValue()
{
return data.targetValue;
}
public synchronized void setTargetValue(double targetValue)
{
if (targetValue < 0) throw new IllegalArgumentException();
data.targetValue = targetValue;
}
public synchronized int getType()
{
return data.type;
}
public synchronized void setType(int type)
{
if (type != YES_NO_HABIT && type != NUMBER_HABIT)
throw new IllegalArgumentException();
data.type = type;
}
@NonNull
public synchronized String getUnit()
{
return data.unit;
}
public synchronized void setUnit(@NonNull String unit)
{
data.unit = unit;
}
/**
* Returns the public URI that identifies this habit
*
* @return the URI
*/
public String getUriString()
{
return String.format(Locale.US, HABIT_URI_FORMAT, getId());
}
public synchronized boolean hasId()
{
return getId() != null;
}
/**
* Returns whether the habit has a reminder.
*
* @return true if habit has reminder, false otherwise
*/
public synchronized boolean hasReminder()
{
return data.reminder != null;
}
public void invalidateNewerThan(Timestamp timestamp)
{
getScores().invalidateNewerThan(timestamp);
getCheckmarks().invalidateNewerThan(timestamp);
getStreaks().invalidateNewerThan(timestamp);
}
public synchronized boolean isArchived()
{
return data.archived;
}
public synchronized void setArchived(boolean archived)
{
data.archived = archived;
}
public synchronized boolean isCompletedToday()
{
int todayCheckmark = getCheckmarks().getTodayValue();
if (isNumerical())
{
if(getTargetType() == AT_LEAST)
return todayCheckmark >= data.targetValue;
else
return todayCheckmark <= data.targetValue;
}
else return (todayCheckmark != UNCHECKED);
}
public synchronized boolean isNumerical()
{
return data.type == NUMBER_HABIT;
}
public HabitData getData()
{
return new HabitData(data);
}
public Integer getPosition()
{
return data.position;
}
public void setPosition(int newPosition)
{
data.position = newPosition;
}
public static final class HabitData
{
@NonNull
public String name;
@NonNull
public String description;
@NonNull
public Frequency frequency;
public int color;
public boolean archived;
public int targetType;
public double targetValue;
public int type;
@NonNull
public String unit;
@Nullable
public Reminder reminder;
public int position;
public HabitData()
{
this.color = 8;
this.archived = false;
this.frequency = new Frequency(3, 7);
this.type = YES_NO_HABIT;
this.name = "";
this.description = "";
this.targetType = AT_LEAST;
this.targetValue = 100;
this.unit = "";
this.position = 0;
}
public HabitData(@NonNull HabitData model)
{
this.name = model.name;
this.description = model.description;
this.frequency = model.frequency;
this.color = model.color;
this.archived = model.archived;
this.targetType = model.targetType;
this.targetValue = model.targetValue;
this.type = model.type;
this.unit = model.unit;
this.reminder = model.reminder;
this.position = model.position;
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("name", name)
.append("description", description)
.append("frequency", frequency)
.append("color", color)
.append("archived", archived)
.append("targetType", targetType)
.append("targetValue", targetValue)
.append("type", type)
.append("unit", unit)
.append("reminder", reminder)
.append("position", position)
.toString();
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HabitData habitData = (HabitData) o;
return new EqualsBuilder()
.append(color, habitData.color)
.append(archived, habitData.archived)
.append(targetType, habitData.targetType)
.append(targetValue, habitData.targetValue)
.append(type, habitData.type)
.append(name, habitData.name)
.append(description, habitData.description)
.append(frequency, habitData.frequency)
.append(unit, habitData.unit)
.append(reminder, habitData.reminder)
.append(position, habitData.position)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(name)
.append(description)
.append(frequency)
.append(color)
.append(archived)
.append(targetType)
.append(targetValue)
.append(type)
.append(unit)
.append(reminder)
.append(position)
.toHashCode();
}
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("id", id)
.append("data", data)
.toString();
}
}

View File

@@ -0,0 +1,250 @@
/*
* 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.core.models;
import android.support.annotation.*;
import com.opencsv.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.util.*;
import javax.annotation.concurrent.*;
/**
* An ordered collection of {@link Habit}s.
*/
@ThreadSafe
public abstract class HabitList implements Iterable<Habit>
{
private final ModelObservable observable;
@NonNull
protected final HabitMatcher filter;
/**
* Creates a new HabitList.
* <p>
* Depending on the implementation, this list can either be empty or be
* populated by some pre-existing habits, for example, from a certain
* database.
*/
public HabitList()
{
observable = new ModelObservable();
filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
}
protected HabitList(@NonNull HabitMatcher filter)
{
observable = new ModelObservable();
this.filter = filter;
}
/**
* Inserts a new habit in the list.
* <p>
* If the id of the habit is null, the list will assign it a new id, which
* is guaranteed to be unique in the scope of the list. If id is not null,
* the caller should make sure that the list does not already contain
* another habit with same id, otherwise a RuntimeException will be thrown.
*
* @param habit the habit to be inserted
* @throws IllegalArgumentException if the habit is already on the list.
*/
public abstract void add(@NonNull Habit habit)
throws IllegalArgumentException;
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
*/
@Nullable
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
* @throws IndexOutOfBoundsException when the position is invalid
*/
@NonNull
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 abstract HabitList getFiltered(HabitMatcher matcher);
public ModelObservable getObservable()
{
return observable;
}
public abstract Order getOrder();
/**
* Changes the order of the elements on the list.
*
* @param order the new order criterea
*/
public abstract void setOrder(@NonNull Order order);
/**
* 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(@NonNull Habit h);
public boolean isEmpty()
{
return size() == 0;
}
/**
* 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);
/**
* Removes all the habits from the list.
*/
public void removeAll()
{
List<Habit> copy = new LinkedList<>();
for (Habit h : this) copy.add(h);
for (Habit h : copy) remove(h);
observable.notifyListeners();
}
/**
* 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(@NonNull Habit from, @NonNull Habit to);
public void repair()
{
for (Habit h : this)
{
h.getCheckmarks().invalidateNewerThan(Timestamp.ZERO);
h.getStreaks().invalidateNewerThan(Timestamp.ZERO);
h.getScores().invalidateNewerThan(Timestamp.ZERO);
}
}
/**
* Returns the number of habits in this list.
*
* @return number of habits
*/
public abstract int size();
/**
* 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(@NonNull 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(@NonNull Writer out) throws IOException
{
String header[] = {
"Position",
"Name",
"Description",
"NumRepetitions",
"Interval",
"Color"
};
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
for (Habit habit : this)
{
Frequency freq = habit.getFrequency();
String[] cols = {
String.format("%03d", indexOf(habit) + 1),
habit.getName(),
habit.getDescription(),
Integer.toString(freq.getNumerator()),
Integer.toString(freq.getDenominator()),
ColorConstants.CSV_PALETTE[habit.getColor()]
};
csv.writeNext(cols, false);
}
csv.close();
}
public enum Order
{
BY_NAME,
BY_COLOR,
BY_SCORE,
BY_POSITION
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.models;
public class HabitMatcher
{
public static final HabitMatcher WITH_ALARM = new HabitMatcherBuilder()
.setArchivedAllowed(true)
.setReminderRequired(true)
.build();
private final boolean archivedAllowed;
private final boolean reminderRequired;
private final boolean completedAllowed;
public HabitMatcher(boolean allowArchived,
boolean reminderRequired,
boolean completedAllowed)
{
this.archivedAllowed = allowArchived;
this.reminderRequired = reminderRequired;
this.completedAllowed = completedAllowed;
}
public boolean isArchivedAllowed()
{
return archivedAllowed;
}
public boolean isCompletedAllowed()
{
return completedAllowed;
}
public boolean isReminderRequired()
{
return reminderRequired;
}
public boolean matches(Habit habit)
{
if (!isArchivedAllowed() && habit.isArchived()) return false;
if (isReminderRequired() && !habit.hasReminder()) return false;
if (!isCompletedAllowed() && habit.isCompletedToday()) return false;
return true;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.core.models;
public class HabitMatcherBuilder
{
private boolean archivedAllowed = false;
private boolean reminderRequired = false;
private boolean completedAllowed = true;
public HabitMatcher build()
{
return new HabitMatcher(archivedAllowed, reminderRequired,
completedAllowed);
}
public HabitMatcherBuilder setArchivedAllowed(boolean archivedAllowed)
{
this.archivedAllowed = archivedAllowed;
return this;
}
public HabitMatcherBuilder setCompletedAllowed(boolean completedAllowed)
{
this.completedAllowed = completedAllowed;
return this;
}
public HabitMatcherBuilder setReminderRequired(boolean reminderRequired)
{
this.reminderRequired = reminderRequired;
return this;
}
}

View File

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

View File

@@ -0,0 +1,54 @@
/*
* 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.core.models;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.sqlite.records.*;
/**
* Interface implemented by factories that provide concrete implementations of
* the core model classes.
*/
public interface ModelFactory
{
CheckmarkList buildCheckmarkList(Habit habit);
default Habit buildHabit()
{
return new Habit(this);
}
default Habit buildHabit(Habit.HabitData data)
{
return new Habit(this, data);
}
HabitList buildHabitList();
RepetitionList buildRepetitionList(Habit habit);
ScoreList buildScoreList(Habit habit);
StreakList buildStreakList(Habit habit);
Repository<HabitRecord> buildHabitListRepository();
Repository<RepetitionRecord> buildRepetitionListRepository();
}

View File

@@ -0,0 +1,89 @@
/*
* 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.core.models;
import java.util.*;
import javax.annotation.concurrent.*;
/**
* A ModelObservable allows objects to subscribe themselves to it and receive
* notifications whenever the model is changed.
*/
@ThreadSafe
public class ModelObservable
{
private List<Listener> listeners;
/**
* Creates a new ModelObservable with no listeners.
*/
public ModelObservable()
{
super();
listeners = new LinkedList<>();
}
/**
* Adds the given listener to the observable.
*
* @param l the listener to be added.
*/
public synchronized void addListener(Listener l)
{
listeners.add(l);
}
/**
* Notifies every listener that the model has changed.
* <p>
* Only models should call this method.
*/
public synchronized 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 subscribed to this observable, does nothing.
*
* @param l the listener to be removed
*/
public synchronized void removeListener(Listener l)
{
listeners.remove(l);
}
/**
* Interface implemented by objects that want to be notified when the model
* changes.
*/
public interface Listener
{
/**
* Called whenever the model associated to this observable has been
* modified.
*/
void onModelChange();
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.core.models;
import android.support.annotation.*;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*;
import static org.isoron.uhabits.core.utils.StringUtils.*;
public final class Reminder
{
private final int hour;
private final int minute;
private final WeekdayList days;
public Reminder(int hour, int minute, @NonNull WeekdayList days)
{
this.hour = hour;
this.minute = minute;
this.days = days;
}
@NonNull
public WeekdayList getDays()
{
return days;
}
public int getHour()
{
return hour;
}
public int getMinute()
{
return minute;
}
public long getTimeInMillis()
{
return DateUtils.getUpcomingTimeInMillis(hour, minute);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reminder reminder = (Reminder) o;
return new EqualsBuilder()
.append(hour, reminder.hour)
.append(minute, reminder.minute)
.append(days, reminder.days)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(hour)
.append(minute)
.append(days)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("hour", hour)
.append("minute", minute)
.append("days", days)
.toString();
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.core.models;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.DateFormats;
import org.isoron.uhabits.core.utils.DateUtils;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* Represents a record that the user has performed a certain habit at a certain
* date.
*/
public final class Repetition
{
private final Timestamp timestamp;
/**
* The value of the repetition.
*
* For boolean habits, this always equals Checkmark.CHECKED_EXPLICITLY.
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is here stored as 1500.
*/
private final int value;
/**
* 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 timestamp the time this repetition occurred.
*/
public Repetition(Timestamp timestamp, int value)
{
this.timestamp = timestamp;
this.value = value;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Repetition that = (Repetition) o;
return new EqualsBuilder()
.append(timestamp, that.timestamp)
.append(value, that.value)
.isEquals();
}
public Timestamp getTimestamp()
{
return timestamp;
}
public int getValue()
{
return value;
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(timestamp)
.append(value)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
}

View File

@@ -0,0 +1,222 @@
/*
* 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.core.models;
import android.support.annotation.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
/**
* The collection of {@link Repetition}s belonging to a habit.
*/
public abstract class RepetitionList
{
@NonNull
protected final Habit habit;
@NonNull
protected final ModelObservable observable;
public RepetitionList(@NonNull Habit habit)
{
this.habit = habit;
this.observable = new ModelObservable();
}
/**
* Adds a repetition to the list.
* <p>
* Any implementation of this method must call observable.notifyListeners()
* after the repetition has been added.
*
* @param repetition the repetition to be added.
*/
public abstract void add(Repetition repetition);
/**
* Returns true if the list contains a repetition that has the given
* timestamp.
*
* @param timestamp the timestamp to find.
* @return true if list contains repetition with given timestamp, false
* otherwise.
*/
public boolean containsTimestamp(Timestamp timestamp)
{
return (getByTimestamp(timestamp) != null);
}
/**
* Returns the list of repetitions that happened within the given time
* interval.
* <p>
* The list is sorted by timestamp in increasing order. That is, the first
* element corresponds to oldest timestamp, while the last element
* corresponds to the newest. 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 abstract List<Repetition> getByInterval(Timestamp fromTimestamp,
Timestamp toTimestamp);
/**
* Returns the repetition that has the given timestamp, or null if none
* exists.
*
* @param timestamp the repetition timestamp.
* @return the repetition that has the given timestamp.
*/
@Nullable
public abstract Repetition getByTimestamp(Timestamp timestamp);
@NonNull
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns the oldest repetition in the list.
* <p>
* If the list is empty, returns null. Repetitions in the future are
* discarded.
*
* @return oldest repetition in the list, or null if list is empty.
*/
@Nullable
public abstract Repetition getOldest();
@Nullable
/**
* Returns the newest repetition in the list.
* <p>
* If the list is empty, returns null. Repetitions in the past are
* discarded.
*
* @return newest repetition in the list, or null if list is empty.
*/
public abstract Repetition getNewest();
/**
* 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<Timestamp, Integer[]> getWeekdayFrequency()
{
List<Repetition> reps =
getByInterval(Timestamp.ZERO, DateUtils.getToday());
HashMap<Timestamp, Integer[]> map = new HashMap<>();
for (Repetition r : reps)
{
Calendar date = r.getTimestamp().toCalendar();
int weekday = r.getTimestamp().getWeekday();
date.set(Calendar.DAY_OF_MONTH, 1);
Timestamp timestamp = new Timestamp(date.getTimeInMillis());
Integer[] list = map.get(timestamp);
if (list == null)
{
list = new Integer[7];
Arrays.fill(list, 0);
map.put(timestamp, list);
}
list[weekday]++;
}
return map;
}
/**
* 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 repetition the repetition to be removed
*/
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 synchronized Repetition toggle(Timestamp timestamp)
{
if (habit.isNumerical())
throw new IllegalStateException("habit must NOT be numerical");
Repetition rep = getByTimestamp(timestamp);
if (rep != null) remove(rep);
else
{
rep = new Repetition(timestamp, Checkmark.CHECKED_EXPLICITLY);
add(rep);
}
habit.invalidateNewerThan(timestamp);
return rep;
}
/**
* Returns the number of all repetitions
*
* @return number of all repetitions
*/
@NonNull
public abstract long getTotalCount();
public void toggle(Timestamp timestamp, int value)
{
Repetition rep = getByTimestamp(timestamp);
if (rep != null) remove(rep);
add(new Repetition(timestamp, value));
habit.invalidateNewerThan(timestamp);
}
public abstract void removeAll();
}

View File

@@ -0,0 +1,121 @@
/*
* 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.core.models;
import org.apache.commons.lang3.builder.*;
import static java.lang.Math.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* Represents how strong a habit is at a certain date.
*/
public final class Score
{
/**
* Timestamp of the day to which this score applies. Time of day should be
* midnight (UTC).
*/
private final Timestamp timestamp;
/**
* Value of the score.
*/
private final double value;
public Score(Timestamp timestamp, double value)
{
this.timestamp = timestamp;
this.value = value;
}
/**
* 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.
*
* @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 double compute(double frequency,
double previousScore,
double checkmarkValue)
{
double multiplier = pow(0.5, frequency / 13.0);
double score = previousScore * multiplier;
score += checkmarkValue * (1 - multiplier);
return score;
}
public int compareNewer(Score other)
{
return getTimestamp().compare(other.getTimestamp());
}
public Timestamp getTimestamp()
{
return timestamp;
}
public double getValue()
{
return value;
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Score score = (Score) o;
return new EqualsBuilder()
.append(value, score.value)
.append(timestamp, score.timestamp)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(timestamp)
.append(value)
.toHashCode();
}
}

View File

@@ -0,0 +1,332 @@
/*
* 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.core.models;
import android.support.annotation.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.text.*;
import java.util.*;
public abstract class ScoreList implements Iterable<Score>
{
protected final Habit habit;
protected ModelObservable observable;
/**
* 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 to which the scores belong.
*/
public ScoreList(Habit habit)
{
this.habit = habit;
observable = new ModelObservable();
}
/**
* Adds the given scores to the list.
* <p>
* This method should not be called by the application, since the scores are
* computed automatically from the list of repetitions.
*
* @param scores the scores to add.
*/
public abstract void add(List<Score> scores);
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns the value of the score for today.
*
* @return value of today's score
*/
public double getTodayValue()
{
return getValue(DateUtils.getToday());
}
/**
* Returns the value of the score for a given day.
* <p>
* If the timestamp given happens before the first repetition of the habit
* then returns zero.
*
* @param timestamp the timestamp of a day
* @return score value for that day
*/
public final synchronized double getValue(Timestamp timestamp)
{
compute(timestamp, timestamp);
Score s = getComputedByTimestamp(timestamp);
if (s == null) throw new IllegalStateException();
return s.getValue();
}
/**
* Returns the list of scores that fall within the given interval.
* <p>
* There is exactly one score per day in the interval. The endpoints of
* the interval are included. The list is ordered by timestamp (decreasing).
* That is, the first score corresponds to the newest timestamp, and the
* last score corresponds to the oldest timestamp.
*
* @param fromTimestamp timestamp of the beginning of the interval.
* @param toTimestamp timestamp of the end of the interval.
* @return the list of scores within the interval.
*/
@NonNull
public abstract List<Score> getByInterval(@NonNull Timestamp fromTimestamp,
@NonNull Timestamp toTimestamp);
/**
* Returns the values of the scores 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 score
* @param to timestamp for the newest score
* @return values for the scores inside the given interval
*/
public final double[] getValues(Timestamp from, Timestamp to)
{
List<Score> scores = getByInterval(from, to);
double[] values = new double[scores.size()];
for (int i = 0; i < values.length; i++)
values[i] = scores.get(i).getValue();
return values;
}
public List<Score> groupBy(DateUtils.TruncateField field)
{
computeAll();
HashMap<Timestamp, ArrayList<Double>> groups = getGroupedValues(field);
List<Score> scores = groupsToAvgScores(groups);
Collections.sort(scores, (s1, s2) -> s2.compareNewer(s1));
return scores;
}
/**
* 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(Timestamp timestamp);
@Override
public Iterator<Score> iterator()
{
return toList().iterator();
}
/**
* Returns a Java list of scores, containing one score for each day, from
* the first repetition of the habit until today.
* <p>
* The scores are sorted by decreasing timestamp. The first score
* corresponds to today.
*
* @return list of scores
*/
public abstract List<Score> toList();
public void writeCSV(Writer out) throws IOException
{
computeAll();
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
for (Score s : this)
{
String timestamp = dateFormat.format(s.getTimestamp().getUnixTime());
String score = String.format((Locale)null, "%.4f", s.getValue());
out.write(String.format("%s,%s\n", timestamp, score));
}
}
/**
* Computes and stores one score for each day inside the given 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 method assumes the list of computed scores has no holes. That is, if
* there is a score computed at time t1 and another at time t2, then every
* score between t1 and t2 is also computed.
*
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the time interval
*/
protected synchronized void compute(@NonNull Timestamp from,
@NonNull Timestamp to)
{
Score newest = getNewestComputed();
Score oldest = getOldestComputed();
if (newest == null)
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep != null) from =
Timestamp.oldest(from, oldestRep.getTimestamp());
forceRecompute(from, to, 0);
}
else
{
if (oldest == null) throw new IllegalStateException();
forceRecompute(from, oldest.getTimestamp().minus(1), 0);
forceRecompute(newest.getTimestamp().plus(1), to,
newest.getValue());
}
}
/**
* 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;
Timestamp today = DateUtils.getToday();
compute(oldestRep.getTimestamp(), today);
}
/**
* Returns the score that has the given timestamp, if it has already been
* computed. If that score has not been computed yet, returns null.
*
* @param timestamp the timestamp of the score
* @return the score with given timestamp, or null not yet computed.
*/
@Nullable
protected abstract Score getComputedByTimestamp(Timestamp timestamp);
/**
* Returns the most recent score that has already been computed. If no score
* has been computed yet, returns null.
*/
@Nullable
protected abstract Score getNewestComputed();
/**
* Returns oldest score already computed. If no score has been computed yet,
* returns null.
*/
@Nullable
protected abstract Score getOldestComputed();
/**
* Computes and stores one score for each day inside the given interval.
* <p>
* This function does not check if the scores have already been computed. If
* they have, then it stores duplicate scores, which is a bad thing.
*
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the interval
* @param previousValue value of the score on the day immediately before the
* interval begins
*/
private void forceRecompute(@NonNull Timestamp from,
@NonNull Timestamp to,
double previousValue)
{
if (from.isNewerThan(to)) return;
final double freq = habit.getFrequency().toDouble();
final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to);
List<Score> scores = new LinkedList<>();
for (int i = 0; i < checkmarkValues.length; i++)
{
double value = checkmarkValues[checkmarkValues.length - i - 1];
if (habit.isNumerical())
{
value /= 1000;
value /= habit.getTargetValue();
value = Math.min(1, value);
}
if (!habit.isNumerical() && value > 0) value = 1;
previousValue = Score.compute(freq, previousValue, value);
scores.add(new Score(from.plus(i), previousValue));
}
add(scores);
}
@NonNull
private HashMap<Timestamp, ArrayList<Double>> getGroupedValues(DateUtils.TruncateField field)
{
HashMap<Timestamp, ArrayList<Double>> groups = new HashMap<>();
for (Score s : this)
{
Timestamp groupTimestamp = new Timestamp(
DateUtils.truncate(field, s.getTimestamp().getUnixTime()));
if (!groups.containsKey(groupTimestamp))
groups.put(groupTimestamp, new ArrayList<>());
groups.get(groupTimestamp).add(s.getValue());
}
return groups;
}
@NonNull
private List<Score> groupsToAvgScores(HashMap<Timestamp, ArrayList<Double>> groups)
{
List<Score> scores = new LinkedList<>();
for (Timestamp timestamp : groups.keySet())
{
double meanValue = 0.0;
ArrayList<Double> groupValues = groups.get(timestamp);
for (Double v : groupValues) meanValue += v;
meanValue /= groupValues.size();
scores.add(new Score(timestamp, meanValue));
}
return scores;
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.core.models;
import org.apache.commons.lang3.builder.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
public final class Streak
{
private final Timestamp start;
private final Timestamp end;
public Streak(Timestamp start, Timestamp end)
{
this.start = start;
this.end = end;
}
public int compareLonger(Streak other)
{
if (this.getLength() != other.getLength())
return Long.signum(this.getLength() - other.getLength());
return compareNewer(other);
}
public int compareNewer(Streak other)
{
return end.compare(other.end);
}
public Timestamp getEnd()
{
return end;
}
public int getLength()
{
return start.daysUntil(end) + 1;
}
public Timestamp getStart()
{
return start;
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("start", start)
.append("end", end)
.toString();
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Streak streak = (Streak) o;
return new EqualsBuilder()
.append(start, streak.start)
.append(end, streak.end)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(start)
.append(end)
.toHashCode();
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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.core.models;
import android.support.annotation.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
/**
* 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
{
protected final Habit habit;
protected ModelObservable observable;
protected StreakList(Habit habit)
{
this.habit = habit;
observable = new ModelObservable();
}
public abstract List<Streak> getAll();
@NonNull
public List<Streak> getBest(int limit)
{
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;
}
@Nullable
public abstract Streak getNewestComputed();
@NonNull
public ModelObservable getObservable()
{
return observable;
}
public abstract void invalidateNewerThan(Timestamp timestamp);
public synchronized void rebuild()
{
Timestamp today = DateUtils.getToday();
Timestamp beginning = findBeginning();
if (beginning == null || beginning.isNewerThan(today)) return;
int checks[] = habit.getCheckmarks().getValues(beginning, today);
List<Streak> streaks = checkmarksToStreaks(beginning, checks);
removeNewestComputed();
add(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(Timestamp beginning, int[] checks)
{
ArrayList<Timestamp> transitions = getTransitions(beginning, checks);
List<Streak> streaks = new LinkedList<>();
for (int i = 0; i < transitions.size(); i += 2)
{
Timestamp start = transitions.get(i);
Timestamp end = transitions.get(i + 1);
streaks.add(new Streak(start, end));
}
return streaks;
}
/**
* Finds the place where we should start when recomputing the streaks.
*
* @return
*/
@Nullable
protected Timestamp 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<Timestamp> getTransitions(Timestamp beginning, int[] checks)
{
ArrayList<Timestamp> list = new ArrayList<>();
Timestamp current = beginning;
list.add(current);
for (int i = 1; i < checks.length; i++)
{
current = current.plus(1);
int j = checks.length - i - 1;
if ((checks[j + 1] == 0 && checks[j] > 0)) list.add(current);
if ((checks[j + 1] > 0 && checks[j] == 0)) list.add(current.minus(1));
}
if (list.size() % 2 == 1) list.add(current);
return list;
}
protected abstract void add(@NonNull List<Streak> streaks);
protected abstract void removeNewestComputed();
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2015-2017 Á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.core.models;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.DateFormats;
import org.isoron.uhabits.core.utils.DateUtils;
import java.util.*;
import static java.util.Calendar.*;
import static org.isoron.uhabits.core.utils.StringUtils.*;
public final class Timestamp
{
public static final long DAY_LENGTH = 86400000;
public static final Timestamp ZERO = new Timestamp(0);
private final long unixTime;
public Timestamp(long unixTime)
{
if (unixTime < 0 || unixTime % DAY_LENGTH != 0)
throw new IllegalArgumentException(
"Invalid unix time: " + unixTime);
this.unixTime = unixTime;
}
public Timestamp(GregorianCalendar cal)
{
this(cal.getTimeInMillis());
}
public long getUnixTime()
{
return unixTime;
}
/**
* Returns -1 if this timestamp is older than the given timestamp, 1 if this
* timestamp is newer, or zero if they are equal.
*/
public int compare(Timestamp other)
{
return Long.signum(this.unixTime - other.unixTime);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Timestamp timestamp = (Timestamp) o;
return new EqualsBuilder()
.append(unixTime, timestamp.unixTime)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37).append(unixTime).toHashCode();
}
/**
* Given two timestamps, returns whichever timestamp is the oldest one.
*/
public static Timestamp oldest(Timestamp first, Timestamp second)
{
return first.unixTime < second.unixTime ? first : second;
}
public Timestamp minus(int days)
{
return plus(-days);
}
public Timestamp plus(int days)
{
return new Timestamp(unixTime + DAY_LENGTH * days);
}
/**
* Returns the number of days between this timestamp and the given one. If
* the other timestamp equals this one, returns zero. If the other timestamp
* is older than this one, returns a negative number.
*/
public int daysUntil(Timestamp other)
{
return (int) ((other.unixTime - this.unixTime) / DAY_LENGTH);
}
public boolean isNewerThan(Timestamp other)
{
return compare(other) > 0;
}
public boolean isOlderThan(Timestamp other)
{
return compare(other) < 0;
}
public Date toJavaDate()
{
return new Date(unixTime);
}
public GregorianCalendar toCalendar()
{
GregorianCalendar day =
new GregorianCalendar(TimeZone.getTimeZone("GMT"));
day.setTimeInMillis(unixTime);
return day;
}
@Override
public String toString()
{
return DateFormats.getCSVDateFormat().format(new Date(unixTime));
}
public int getWeekday()
{
return toCalendar().get(DAY_OF_WEEK) % 7;
}
public Timestamp truncate(DateUtils.TruncateField field)
{
return new Timestamp(DateUtils.truncate(field, unixTime));
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.core.models;
import org.apache.commons.lang3.builder.*;
import java.util.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
public final class WeekdayList
{
public static final WeekdayList EVERY_DAY = new WeekdayList(127);
private final boolean[] weekdays;
public WeekdayList(int packedList)
{
weekdays = new boolean[7];
int current = 1;
for (int i = 0; i < 7; i++)
{
if ((packedList & current) != 0) weekdays[i] = true;
current = current << 1;
}
}
public WeekdayList(boolean weekdays[])
{
this.weekdays = Arrays.copyOf(weekdays, 7);
}
public boolean isEmpty()
{
for (boolean d : weekdays) if (d) return false;
return true;
}
public boolean[] toArray()
{
return Arrays.copyOf(weekdays, 7);
}
public int toInteger()
{
int packedList = 0;
int current = 1;
for (int i = 0; i < 7; i++)
{
if (weekdays[i]) packedList |= current;
current = current << 1;
}
return packedList;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WeekdayList that = (WeekdayList) o;
return new EqualsBuilder().append(weekdays, that.weekdays).isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37).append(weekdays).toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("weekdays", weekdays)
.toString();
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
/**
* In-memory implementation of {@link CheckmarkList}.
*/
public class MemoryCheckmarkList extends CheckmarkList
{
ArrayList<Checkmark> list;
public MemoryCheckmarkList(Habit habit)
{
super(habit);
list = new ArrayList<>();
}
@Override
public void add(List<Checkmark> checkmarks)
{
list.addAll(checkmarks);
Collections.sort(list,
(c1, c2) -> c2.getTimestamp().compare(c1.getTimestamp()));
}
@NonNull
@Override
public synchronized List<Checkmark> getByInterval(Timestamp from,
Timestamp to)
{
compute();
Timestamp newestComputed = new Timestamp(0);
Timestamp oldestComputed = new Timestamp(0).plus(1000000);
Checkmark newest = getNewestComputed();
Checkmark oldest = getOldestComputed();
if(newest != null) newestComputed = newest.getTimestamp();
if(oldest != null) oldestComputed = oldest.getTimestamp();
List<Checkmark> filtered = new ArrayList<>(
Math.max(0, oldestComputed.daysUntil(newestComputed) + 1));
for(int i = 0; i <= from.daysUntil(to); i++)
{
Timestamp t = to.minus(i);
if(t.isNewerThan(newestComputed) || t.isOlderThan(oldestComputed))
filtered.add(new Checkmark(t, Checkmark.UNCHECKED));
else
filtered.add(list.get(t.daysUntil(newestComputed)));
}
return filtered;
}
@Override
public void invalidateNewerThan(Timestamp timestamp)
{
list.clear();
observable.notifyListeners();
}
@Override
@Nullable
protected Checkmark getOldestComputed()
{
if(list.isEmpty()) return null;
return list.get(list.size()-1);
}
@Override
@Nullable
protected Checkmark getNewestComputed()
{
if(list.isEmpty()) return null;
return list.get(0);
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
import static org.isoron.uhabits.core.models.HabitList.Order.*;
/**
* In-memory implementation of {@link HabitList}.
*/
public class MemoryHabitList extends HabitList
{
@NonNull
private LinkedList<Habit> list = new LinkedList<>();
private Comparator<Habit> comparator = null;
@NonNull
private Order order = Order.BY_POSITION;
@Nullable
private MemoryHabitList parent = null;
public MemoryHabitList()
{
super();
}
protected MemoryHabitList(@NonNull HabitMatcher matcher,
Comparator<Habit> comparator,
@NonNull MemoryHabitList parent)
{
super(matcher);
this.parent = parent;
this.comparator = comparator;
parent.getObservable().addListener(this::loadFromParent);
loadFromParent();
}
@Override
public synchronized void add(@NonNull Habit habit)
throws IllegalArgumentException
{
throwIfHasParent();
if (list.contains(habit))
throw new IllegalArgumentException("habit already added");
Long id = habit.getId();
if (id != null && getById(id) != null)
throw new RuntimeException("duplicate id");
if (id == null) habit.setId((long) list.size());
list.addLast(habit);
resort();
getObservable().notifyListeners();
}
@Override
public synchronized Habit getById(long id)
{
for (Habit h : list)
{
if (h.getId() == null) throw new IllegalStateException();
if (h.getId() == id) return h;
}
return null;
}
@NonNull
@Override
public synchronized Habit getByPosition(int position)
{
return list.get(position);
}
@NonNull
@Override
public synchronized HabitList getFiltered(HabitMatcher matcher)
{
return new MemoryHabitList(matcher, comparator, this);
}
@Override
public synchronized Order getOrder()
{
return order;
}
@Override
public synchronized void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
getObservable().notifyListeners();
}
private Comparator<Habit> getComparatorByOrder(Order order)
{
Comparator<Habit> nameComparator =
(h1, h2) -> h1.getName().compareTo(h2.getName());
Comparator<Habit> colorComparator = (h1, h2) ->
{
Integer c1 = h1.getColor();
Integer c2 = h2.getColor();
if (c1.equals(c2)) return nameComparator.compare(h1, h2);
else return c1.compareTo(c2);
};
Comparator<Habit> scoreComparator = (h1, h2) ->
{
Double s1 = h1.getScores().getTodayValue();
Double s2 = h2.getScores().getTodayValue();
if (s1.equals(s2)) return nameComparator.compare(h1, h2);
else return s2.compareTo(s1);
};
Comparator<Habit> positionComparator = (h1, h2) ->
{
Integer p1 = h1.getPosition();
Integer p2 = h2.getPosition();
if (p1.equals(p2)) return nameComparator.compare(h1, h2);
else return p1.compareTo(p2);
};
if (order == BY_POSITION) return positionComparator;
if (order == BY_NAME) return nameComparator;
if (order == BY_COLOR) return colorComparator;
if (order == BY_SCORE) return scoreComparator;
throw new IllegalStateException();
}
@Override
public synchronized int indexOf(@NonNull Habit h)
{
return list.indexOf(h);
}
@NonNull
@Override
public synchronized Iterator<Habit> iterator()
{
return new ArrayList<>(list).iterator();
}
@Override
public synchronized void remove(@NonNull Habit habit)
{
throwIfHasParent();
list.remove(habit);
getObservable().notifyListeners();
}
@Override
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
{
throwIfHasParent();
if (order != BY_POSITION) throw new IllegalStateException(
"cannot reorder automatically sorted list");
if (indexOf(from) < 0) throw new IllegalArgumentException(
"list does not contain (from) habit");
int toPos = indexOf(to);
if (toPos < 0) throw new IllegalArgumentException(
"list does not contain (to) habit");
list.remove(from);
list.add(toPos, from);
int position = 0;
for(Habit h : list)
h.setPosition(position++);
getObservable().notifyListeners();
}
@Override
public synchronized int size()
{
return list.size();
}
@Override
public synchronized void update(List<Habit> habits)
{
resort();
getObservable().notifyListeners();
}
private void throwIfHasParent()
{
if (parent != null) throw new IllegalStateException(
"Filtered lists cannot be modified directly. " +
"You should modify the parent list instead.");
}
private synchronized void loadFromParent()
{
if (parent == null) throw new IllegalStateException();
list.clear();
for (Habit h : parent) if (filter.matches(h)) list.add(h);
resort();
}
private synchronized void resort()
{
if (comparator != null) Collections.sort(list, comparator);
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.core.models.memory;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.sqlite.records.*;
public class MemoryModelFactory implements ModelFactory
{
@Override
public CheckmarkList buildCheckmarkList(Habit habit)
{
return new MemoryCheckmarkList(habit);
}
@Override
public HabitList buildHabitList()
{
return new MemoryHabitList();
}
@Override
public RepetitionList buildRepetitionList(Habit habit)
{
return new MemoryRepetitionList(habit);
}
@Override
public ScoreList buildScoreList(Habit habit)
{
return new MemoryScoreList(habit);
}
@Override
public StreakList buildStreakList(Habit habit)
{
return new MemoryStreakList(habit);
}
@Override
public Repository<HabitRecord> buildHabitListRepository()
{
throw new IllegalStateException();
}
@Override
public Repository<RepetitionRecord> buildRepetitionListRepository()
{
throw new IllegalStateException();
}
}

View File

@@ -0,0 +1,133 @@
/*
* 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.core.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
/**
* In-memory implementation of {@link RepetitionList}.
*/
public class MemoryRepetitionList extends RepetitionList
{
ArrayList<Repetition> list;
public MemoryRepetitionList(Habit habit)
{
super(habit);
list = new ArrayList<>();
}
@Override
public void add(Repetition repetition)
{
list.add(repetition);
observable.notifyListeners();
}
@Override
public List<Repetition> getByInterval(Timestamp fromTimestamp, Timestamp toTimestamp)
{
ArrayList<Repetition> filtered = new ArrayList<>();
for (Repetition r : list)
{
Timestamp t = r.getTimestamp();
if (t.isOlderThan(fromTimestamp) || t.isNewerThan(toTimestamp)) continue;
filtered.add(r);
}
Collections.sort(filtered,
(r1, r2) -> r1.getTimestamp().compare(r2.getTimestamp()));
return filtered;
}
@Nullable
@Override
public Repetition getByTimestamp(Timestamp timestamp)
{
for (Repetition r : list)
if (r.getTimestamp().equals(timestamp)) return r;
return null;
}
@Nullable
@Override
public Repetition getOldest()
{
Timestamp oldestTimestamp = Timestamp.ZERO.plus(1000000);
Repetition oldestRep = null;
for (Repetition rep : list)
{
if (rep.getTimestamp().isOlderThan(oldestTimestamp))
{
oldestRep = rep;
oldestTimestamp = rep.getTimestamp();
}
}
return oldestRep;
}
@Nullable
@Override
public Repetition getNewest()
{
Timestamp newestTimestamp = Timestamp.ZERO;
Repetition newestRep = null;
for (Repetition rep : list)
{
if (rep.getTimestamp().isNewerThan(newestTimestamp))
{
newestRep = rep;
newestTimestamp = rep.getTimestamp();
}
}
return newestRep;
}
@Override
public void remove(@NonNull Repetition repetition)
{
list.remove(repetition);
observable.notifyListeners();
}
@Override
public long getTotalCount()
{
return list.size();
}
@Override
public void removeAll()
{
list.clear();
getObservable().notifyListeners();
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
public class MemoryScoreList extends ScoreList
{
ArrayList<Score> list;
public MemoryScoreList(Habit habit)
{
super(habit);
list = new ArrayList<>();
}
@Override
public void add(List<Score> scores)
{
list.addAll(scores);
Collections.sort(list,
(s1, s2) -> s2.getTimestamp().compare(s1.getTimestamp()));
getObservable().notifyListeners();
}
@NonNull
@Override
public List<Score> getByInterval(@NonNull Timestamp fromTimestamp,
@NonNull Timestamp toTimestamp)
{
compute(fromTimestamp, toTimestamp);
List<Score> filtered = new LinkedList<>();
for (Score s : list)
{
if (s.getTimestamp().isNewerThan(toTimestamp) ||
s.getTimestamp().isOlderThan(fromTimestamp)) continue;
filtered.add(s);
}
return filtered;
}
@Nullable
@Override
public Score getComputedByTimestamp(Timestamp timestamp)
{
for (Score s : list)
if (s.getTimestamp().equals(timestamp)) return s;
return null;
}
@Override
public void invalidateNewerThan(Timestamp timestamp)
{
list.clear();
getObservable().notifyListeners();
}
@Override
@NonNull
public List<Score> toList()
{
computeAll();
return new LinkedList<>(list);
}
@Nullable
@Override
protected Score getNewestComputed()
{
if (list.isEmpty()) return null;
return list.get(0);
}
@Nullable
@Override
protected Score getOldestComputed()
{
if (list.isEmpty()) return null;
return list.get(list.size() - 1);
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.core.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
public class MemoryStreakList extends StreakList
{
ArrayList<Streak> list;
public MemoryStreakList(Habit habit)
{
super(habit);
list = new ArrayList<>();
}
@Override
public Streak getNewestComputed()
{
Streak newest = null;
for (Streak s : list)
if (newest == null || s.getEnd().isNewerThan(newest.getEnd()))
newest = s;
return newest;
}
@Override
public void invalidateNewerThan(Timestamp timestamp)
{
list.clear();
observable.notifyListeners();
}
@Override
protected void add(@NonNull List<Streak> streaks)
{
list.addAll(streaks);
Collections.sort(list, (s1, s2) -> s2.compareNewer(s1));
observable.notifyListeners();
}
@Override
protected void removeNewestComputed()
{
Streak newest = getNewestComputed();
if (newest != null) list.remove(newest);
}
@Override
public List<Streak> getAll()
{
rebuild();
return new LinkedList<>(list);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.memory.*;
import org.isoron.uhabits.core.models.sqlite.records.*;
import javax.inject.*;
/**
* Factory that provides models backed by an SQLite database.
*/
public class SQLModelFactory implements ModelFactory
{
public final Database db;
@Inject
public SQLModelFactory(Database db)
{
this.db = db;
}
@Override
public CheckmarkList buildCheckmarkList(Habit habit)
{
return new MemoryCheckmarkList(habit);
}
@Override
public HabitList buildHabitList()
{
return new SQLiteHabitList(this);
}
@Override
public RepetitionList buildRepetitionList(Habit habit)
{
return new SQLiteRepetitionList(habit, this);
}
@Override
public ScoreList buildScoreList(Habit habit)
{
return new MemoryScoreList(habit);
}
@Override
public StreakList buildStreakList(Habit habit)
{
return new MemoryStreakList(habit);
}
@Override
public Repository<HabitRecord> buildHabitListRepository()
{
return new Repository<>(HabitRecord.class, db);
}
@Override
public Repository<RepetitionRecord> buildRepetitionListRepository()
{
return new Repository<>(RepetitionRecord.class, db);
}
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite;
import android.support.annotation.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.memory.*;
import org.isoron.uhabits.core.models.sqlite.records.*;
import java.util.*;
import javax.inject.*;
/**
* Implementation of a {@link HabitList} that is backed by SQLite.
*/
public class SQLiteHabitList extends HabitList
{
private static SQLiteHabitList instance;
@NonNull
private final Repository<HabitRecord> repository;
@NonNull
private final ModelFactory modelFactory;
@NonNull
private final MemoryHabitList list;
private boolean loaded = false;
@Inject
public SQLiteHabitList(@NonNull ModelFactory modelFactory)
{
super();
this.modelFactory = modelFactory;
this.list = new MemoryHabitList();
this.repository = modelFactory.buildHabitListRepository();
}
private void loadRecords()
{
if (loaded) return;
loaded = true;
list.removeAll();
List<HabitRecord> records = repository.findAll("order by position");
for (HabitRecord rec : records)
{
Habit h = modelFactory.buildHabit();
rec.copyTo(h);
list.add(h);
}
}
@Override
public synchronized void add(@NonNull Habit habit)
{
loadRecords();
habit.setPosition(size());
list.add(habit);
HabitRecord record = new HabitRecord();
record.copyFrom(habit);
repository.save(record);
rebuildOrder();
getObservable().notifyListeners();
}
@Override
@Nullable
public synchronized Habit getById(long id)
{
loadRecords();
return list.getById(id);
}
@Override
@NonNull
public synchronized Habit getByPosition(int position)
{
loadRecords();
return list.getByPosition(position);
}
@NonNull
@Override
public synchronized HabitList getFiltered(HabitMatcher filter)
{
loadRecords();
return list.getFiltered(filter);
}
@Override
@NonNull
public Order getOrder()
{
return list.getOrder();
}
@Override
public synchronized void setOrder(@NonNull Order order)
{
list.setOrder(order);
getObservable().notifyListeners();
}
@Override
public synchronized int indexOf(@NonNull Habit h)
{
loadRecords();
return list.indexOf(h);
}
@Override
public synchronized Iterator<Habit> iterator()
{
loadRecords();
return list.iterator();
}
private synchronized void rebuildOrder()
{
List<HabitRecord> records = repository.findAll("order by position");
repository.executeAsTransaction(() -> {
int pos = 0;
for (HabitRecord r : records) {
r.position = pos++;
repository.save(r);
}
});
}
@Override
public synchronized void remove(@NonNull Habit habit)
{
loadRecords();
list.remove(habit);
HabitRecord record = repository.find(habit.getId());
if (record == null) throw new RuntimeException("habit not in database");
repository.executeAsTransaction(() ->
{
((SQLiteRepetitionList) habit.getRepetitions()).removeAll();
repository.remove(record);
});
rebuildOrder();
getObservable().notifyListeners();
}
@Override
public synchronized void removeAll()
{
list.removeAll();
repository.execSQL("delete from habits");
repository.execSQL("delete from repetitions");
getObservable().notifyListeners();
}
@Override
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
{
loadRecords();
list.reorder(from, to);
HabitRecord fromRecord = repository.find(from.getId());
HabitRecord toRecord = repository.find(to.getId());
if (fromRecord == null)
throw new RuntimeException("habit not in database");
if (toRecord == null)
throw new RuntimeException("habit not in database");
if (toRecord.position < fromRecord.position)
{
repository.execSQL("update habits set position = position + 1 " +
"where position >= ? and position < ?",
toRecord.position, fromRecord.position);
}
else
{
repository.execSQL("update habits set position = position - 1 " +
"where position > ? and position <= ?",
fromRecord.position, toRecord.position);
}
fromRecord.position = toRecord.position;
repository.save(fromRecord);
getObservable().notifyListeners();
}
@Override
public synchronized void repair()
{
loadRecords();
rebuildOrder();
getObservable().notifyListeners();
}
@Override
public synchronized int size()
{
loadRecords();
return list.size();
}
@Override
public synchronized void update(List<Habit> habits)
{
loadRecords();
list.update(habits);
for (Habit h : habits)
{
HabitRecord record = repository.find(h.getId());
if (record == null) continue;
record.copyFrom(h);
repository.save(record);
}
getObservable().notifyListeners();
}
public synchronized void reload()
{
loaded = false;
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite;
import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.memory.*;
import org.isoron.uhabits.core.models.sqlite.records.*;
import org.jetbrains.annotations.*;
import java.util.*;
/**
* Implementation of a {@link RepetitionList} that is backed by SQLite.
*/
public class SQLiteRepetitionList extends RepetitionList
{
private final Repository<RepetitionRecord> repository;
private final MemoryRepetitionList list;
private boolean loaded = false;
public SQLiteRepetitionList(@NonNull Habit habit,
@NonNull ModelFactory modelFactory)
{
super(habit);
repository = modelFactory.buildRepetitionListRepository();
list = new MemoryRepetitionList(habit);
}
private void loadRecords()
{
if (loaded) return;
loaded = true;
check(habit.getId());
List<RepetitionRecord> records =
repository.findAll("where habit = ? order by timestamp",
habit.getId().toString());
for (RepetitionRecord rec : records)
list.add(rec.toRepetition());
}
@Override
public void add(Repetition rep)
{
loadRecords();
list.add(rep);
check(habit.getId());
RepetitionRecord record = new RepetitionRecord();
record.habit_id = habit.getId();
record.copyFrom(rep);
repository.save(record);
observable.notifyListeners();
}
@Override
public List<Repetition> getByInterval(Timestamp timeFrom, Timestamp timeTo)
{
loadRecords();
return list.getByInterval(timeFrom, timeTo);
}
@Override
@Nullable
public Repetition getByTimestamp(Timestamp timestamp)
{
loadRecords();
return list.getByTimestamp(timestamp);
}
@Override
public Repetition getOldest()
{
loadRecords();
return list.getOldest();
}
@Override
public Repetition getNewest()
{
loadRecords();
return list.getNewest();
}
@Override
public void remove(@NonNull Repetition repetition)
{
loadRecords();
list.remove(repetition);
check(habit.getId());
repository.execSQL(
"delete from repetitions where habit = ? and timestamp = ?",
habit.getId(), repetition.getTimestamp().getUnixTime());
observable.notifyListeners();
}
public void removeAll()
{
loadRecords();
list.removeAll();
check(habit.getId());
repository.execSQL("delete from repetitions where habit = ?",
habit.getId());
}
@Override
public long getTotalCount()
{
loadRecords();
return list.getTotalCount();
}
public void reload()
{
loaded = false;
}
@Contract("null -> fail")
private void check(Long value)
{
if (value == null) throw new RuntimeException("null check failed");
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite;

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite.records;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
/**
* The SQLite database record corresponding to a {@link Habit}.
*/
@Table(name = "habits")
public class HabitRecord
{
@Column
public String description;
@Column
public String name;
@Column(name = "freq_num")
public Integer freqNum;
@Column(name = "freq_den")
public Integer freqDen;
@Column
public Integer color;
@Column
public Integer position;
@Column(name = "reminder_hour")
public Integer reminderHour;
@Column(name = "reminder_min")
public Integer reminderMin;
@Column(name = "reminder_days")
public Integer reminderDays;
@Column
public Integer highlight;
@Column
public Integer archived;
@Column
public Integer type;
@Column(name = "target_value")
public Double targetValue;
@Column(name = "target_type")
public Integer targetType;
@Column
public String unit;
@Column
public Long id;
public void copyFrom(Habit model)
{
this.id = model.getId();
this.name = model.getName();
this.description = model.getDescription();
this.highlight = 0;
this.color = model.getColor();
this.archived = model.isArchived() ? 1 : 0;
this.type = model.getType();
this.targetType = model.getTargetType();
this.targetValue = model.getTargetValue();
this.unit = model.getUnit();
this.position = model.getPosition();
Frequency freq = model.getFrequency();
this.freqNum = freq.getNumerator();
this.freqDen = freq.getDenominator();
this.reminderDays = 0;
this.reminderMin = null;
this.reminderHour = null;
if (model.hasReminder())
{
Reminder reminder = model.getReminder();
this.reminderHour = reminder.getHour();
this.reminderMin = reminder.getMinute();
this.reminderDays = reminder.getDays().toInteger();
}
}
public void copyTo(Habit habit)
{
habit.setId(this.id);
habit.setName(this.name);
habit.setDescription(this.description);
habit.setFrequency(new Frequency(this.freqNum, this.freqDen));
habit.setColor(this.color);
habit.setArchived(this.archived != 0);
habit.setType(this.type);
habit.setTargetType(this.targetType);
habit.setTargetValue(this.targetValue);
habit.setUnit(this.unit);
habit.setPosition(this.position);
if (reminderHour != null && reminderMin != null)
{
habit.setReminder(new Reminder(reminderHour, reminderMin,
new WeekdayList(reminderDays)));
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite.records;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
/**
* The SQLite database record corresponding to a {@link Repetition}.
*/
@Table(name = "Repetitions")
public class RepetitionRecord
{
public HabitRecord habit;
@Column(name = "habit")
public Long habit_id;
@Column
public Long timestamp;
@Column
public Integer value;
@Column
public Long id;
public void copyFrom(Repetition repetition)
{
timestamp = repetition.getTimestamp().getUnixTime();
value = repetition.getValue();
}
public Repetition toRepetition()
{
return new Repetition(new Timestamp(timestamp), value);
}
}

View File

@@ -0,0 +1,385 @@
/*
* Copyright (C) 2017 Á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.core.preferences;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
public class Preferences
{
public static final String DEFAULT_SYNC_SERVER =
"https://sync.loophabits.org";
@NonNull
private final Storage storage;
@NonNull
private List<Listener> listeners;
@Nullable
private Boolean shouldReverseCheckmarks = null;
public Preferences(@NonNull Storage storage)
{
this.storage = storage;
listeners = new LinkedList<>();
storage.onAttached(this);
}
public void addListener(Listener listener)
{
listeners.add(listener);
}
public Integer getDefaultHabitColor(int fallbackColor)
{
return storage.getInt("pref_default_habit_palette_color",
fallbackColor);
}
public HabitList.Order getDefaultOrder()
{
String name = storage.getString("pref_default_order", "BY_POSITION");
try
{
return HabitList.Order.valueOf(name);
}
catch (IllegalArgumentException e)
{
setDefaultOrder(HabitList.Order.BY_POSITION);
return HabitList.Order.BY_POSITION;
}
}
public void setDefaultOrder(HabitList.Order order)
{
storage.putString("pref_default_order", order.name());
}
public int getDefaultScoreSpinnerPosition()
{
int defaultScoreInterval =
storage.getInt("pref_score_view_interval", 1);
if (defaultScoreInterval > 5 || defaultScoreInterval < 0)
{
defaultScoreInterval = 1;
storage.putInt("pref_score_view_interval", 1);
}
return defaultScoreInterval;
}
public void setDefaultScoreSpinnerPosition(int position)
{
storage.putInt("pref_score_view_interval", position);
}
public int getLastHintNumber()
{
return storage.getInt("last_hint_number", -1);
}
public Timestamp getLastHintTimestamp()
{
long unixTime = storage.getLong("last_hint_timestamp", -1);
if (unixTime < 0) return null;
else return new Timestamp(unixTime);
}
public long getLastSync()
{
return storage.getLong("last_sync", 0);
}
public void setLastSync(long timestamp)
{
storage.putLong("last_sync", timestamp);
}
public boolean getShowArchived()
{
return storage.getBoolean("pref_show_archived", false);
}
public void setShowArchived(boolean showArchived)
{
storage.putBoolean("pref_show_archived", showArchived);
}
public boolean getShowCompleted()
{
return storage.getBoolean("pref_show_completed", true);
}
public void setShowCompleted(boolean showCompleted)
{
storage.putBoolean("pref_show_completed", showCompleted);
}
public long getSnoozeInterval()
{
return Long.parseLong(storage.getString("pref_snooze_interval", "15"));
}
public void setSnoozeInterval(int interval)
{
storage.putString("pref_snooze_interval", String.valueOf(interval));
}
public String getSyncAddress()
{
return storage.getString("pref_sync_address", DEFAULT_SYNC_SERVER);
}
public void setSyncAddress(String address)
{
storage.putString("pref_sync_address", address);
for (Listener l : listeners) l.onSyncFeatureChanged();
}
public String getSyncClientId()
{
String id = storage.getString("pref_sync_client_id", "");
if (!id.isEmpty()) return id;
id = UUID.randomUUID().toString();
storage.putString("pref_sync_client_id", id);
return id;
}
public String getSyncKey()
{
return storage.getString("pref_sync_key", "");
}
public void setSyncKey(String key)
{
storage.putString("pref_sync_key", key);
for (Listener l : listeners) l.onSyncFeatureChanged();
}
public int getTheme()
{
return storage.getInt("pref_theme", ThemeSwitcher.THEME_LIGHT);
}
public void setTheme(int theme)
{
storage.putInt("pref_theme", theme);
}
public void incrementLaunchCount()
{
storage.putInt("launch_count", getLaunchCount() + 1);
}
public int getLaunchCount()
{
return storage.getInt("launch_count", 0);
}
public boolean isDeveloper()
{
return storage.getBoolean("pref_developer", false);
}
public void setDeveloper(boolean isDeveloper)
{
storage.putBoolean("pref_developer", isDeveloper);
}
public boolean isFirstRun()
{
return storage.getBoolean("pref_first_run", true);
}
public void setFirstRun(boolean isFirstRun)
{
storage.putBoolean("pref_first_run", isFirstRun);
}
public boolean isNumericalHabitsFeatureEnabled()
{
return storage.getBoolean("pref_feature_numerical_habits", false);
}
public void setNumericalHabitsFeatureEnabled(boolean enabled)
{
storage.putBoolean("pref_feature_numerical_habits", enabled);
}
public boolean isPureBlackEnabled()
{
return storage.getBoolean("pref_pure_black", false);
}
public void setPureBlackEnabled(boolean enabled)
{
storage.putBoolean("pref_pure_black", enabled);
}
public boolean isShortToggleEnabled()
{
return storage.getBoolean("pref_short_toggle", false);
}
public void setShortToggleEnabled(boolean enabled)
{
storage.putBoolean("pref_short_toggle", enabled);
}
public boolean isSyncEnabled()
{
return storage.getBoolean("pref_feature_sync", false);
}
public void setSyncEnabled(boolean isEnabled)
{
storage.putBoolean("pref_feature_sync", isEnabled);
for (Listener l : listeners) l.onSyncFeatureChanged();
}
public void removeListener(Listener listener)
{
listeners.remove(listener);
}
public void clear()
{
storage.clear();
}
public void setDefaultHabitColor(int color)
{
storage.putInt("pref_default_habit_palette_color", color);
}
public void setNotificationsSticky(boolean sticky)
{
storage.putBoolean("pref_sticky_notifications", sticky);
for (Listener l : listeners) l.onNotificationsChanged();
}
public void setNotificationsLed(boolean enabled)
{
storage.putBoolean("pref_led_notifications", enabled);
for (Listener l : listeners) l.onNotificationsChanged();
}
public boolean shouldMakeNotificationsSticky()
{
return storage.getBoolean("pref_sticky_notifications", false);
}
public boolean shouldMakeNotificationsLed()
{
return storage.getBoolean("pref_led_notifications", false);
}
public boolean isCheckmarkSequenceReversed()
{
if (shouldReverseCheckmarks == null) shouldReverseCheckmarks =
storage.getBoolean("pref_checkmark_reverse_order", false);
return shouldReverseCheckmarks;
}
public void setCheckmarkSequenceReversed(boolean reverse)
{
shouldReverseCheckmarks = reverse;
storage.putBoolean("pref_checkmark_reverse_order", reverse);
for (Listener l : listeners) l.onCheckmarkSequenceChanged();
}
public void updateLastHint(int number, Timestamp timestamp)
{
storage.putInt("last_hint_number", number);
storage.putLong("last_hint_timestamp", timestamp.getUnixTime());
}
public int getLastAppVersion()
{
return storage.getInt("last_version", 0);
}
public void setLastAppVersion(int version)
{
storage.putInt("last_version", version);
}
public interface Listener
{
default void onCheckmarkSequenceChanged()
{
}
default void onNotificationsChanged()
{
}
default void onSyncFeatureChanged()
{
}
}
public interface Storage
{
void clear();
boolean getBoolean(String key, boolean defValue);
int getInt(String key, int defValue);
long getLong(String key, long defValue);
String getString(String key, String defValue);
void onAttached(Preferences preferences);
void putBoolean(String key, boolean value);
void putInt(String key, int value);
void putLong(String key, long value);
void putString(String key, String value);
void remove(String key);
default void putLongArray(String key, long[] values)
{
putString(key, StringUtils.joinLongs(values));
}
default long[] getLongArray(String key)
{
return StringUtils.splitLongs(getString(key, ""));
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2015-2017 Á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.core.preferences;
import android.support.annotation.*;
import java.io.*;
import java.util.*;
public class PropertiesStorage implements Preferences.Storage
{
@NonNull
private final Properties props;
@NonNull
private File file;
public PropertiesStorage(@NonNull File file)
{
try
{
this.file = file;
props = new Properties();
props.load(new FileInputStream(file));
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
@Override
public void clear()
{
for(String key : props.stringPropertyNames()) props.remove(key);
flush();
}
@Override
public boolean getBoolean(String key, boolean defValue)
{
String value = props.getProperty(key, Boolean.toString(defValue));
return Boolean.parseBoolean(value);
}
@Override
public int getInt(String key, int defValue)
{
String value = props.getProperty(key, Integer.toString(defValue));
return Integer.parseInt(value);
}
@Override
public long getLong(String key, long defValue)
{
String value = props.getProperty(key, Long.toString(defValue));
return Long.parseLong(value);
}
@Override
public String getString(String key, String defValue)
{
return props.getProperty(key, defValue);
}
@Override
public void onAttached(Preferences preferences)
{
// nop
}
@Override
public void putBoolean(String key, boolean value)
{
props.setProperty(key, Boolean.toString(value));
}
@Override
public void putInt(String key, int value)
{
props.setProperty(key, Integer.toString(value));
flush();
}
private void flush()
{
try
{
props.store(new FileOutputStream(file), "");
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
@Override
public void putLong(String key, long value)
{
props.setProperty(key, Long.toString(value));
flush();
}
@Override
public void putString(String key, String value)
{
props.setProperty(key, value);
flush();
}
@Override
public void remove(String key)
{
props.remove(key);
flush();
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.core.preferences;
import org.isoron.uhabits.core.AppScope;
import org.isoron.uhabits.core.models.HabitNotFoundException;
import javax.inject.Inject;
@AppScope
public class WidgetPreferences {
private Preferences.Storage storage;
@Inject
public WidgetPreferences(Preferences.Storage storage) {
this.storage = storage;
}
public void addWidget(int widgetId, long habitIds[]) {
storage.putLongArray(getHabitIdKey(widgetId), habitIds);
}
public long[] getHabitIdsFromWidgetId(int widgetId) {
long habitIds[] = storage.getLongArray(getHabitIdKey(widgetId));
if (habitIds.length == 0) throw new HabitNotFoundException();
return habitIds;
}
public void removeWidget(int id) {
String habitIdKey = getHabitIdKey(id);
storage.remove(habitIdKey);
}
private String getHabitIdKey(int id) {
return String.format("widget-%06d-habit", id);
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.core.reminders;
import android.support.annotation.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import javax.inject.*;
import static org.isoron.uhabits.core.utils.DateUtils.*;
@AppScope
public class ReminderScheduler implements CommandRunner.Listener
{
private CommandRunner commandRunner;
private HabitList habitList;
private SystemScheduler sys;
@Inject
public ReminderScheduler(@NonNull CommandRunner commandRunner,
@NonNull HabitList habitList,
@NonNull SystemScheduler sys)
{
this.commandRunner = commandRunner;
this.habitList = habitList;
this.sys = sys;
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (command instanceof ToggleRepetitionCommand) return;
if (command instanceof ChangeHabitColorCommand) return;
scheduleAll();
}
public void schedule(@NonNull Habit habit)
{
if (!habit.hasReminder()) return;
Long reminderTime = habit.getReminder().getTimeInMillis();
scheduleAtTime(habit, reminderTime);
}
public void scheduleAtTime(@NonNull Habit habit, @NonNull Long reminderTime)
{
if (reminderTime == null) throw new IllegalArgumentException();
if (!habit.hasReminder()) return;
if (habit.isArchived()) return;
long timestamp = getStartOfDay(removeTimezone(reminderTime));
sys.scheduleShowReminder(reminderTime, habit, timestamp);
}
public synchronized void scheduleAll()
{
HabitList reminderHabits =
habitList.getFiltered(HabitMatcher.WITH_ALARM);
for (Habit habit : reminderHabits)
schedule(habit);
}
public void startListening()
{
commandRunner.addListener(this);
}
public void stopListening()
{
commandRunner.removeListener(this);
}
public void scheduleMinutesFromNow(Habit habit, long minutes)
{
long now = applyTimezone(getLocalTime());
long reminderTime = now + minutes * 60 * 1000;
scheduleAtTime(habit, reminderTime);
}
public interface SystemScheduler
{
void scheduleShowReminder(long reminderTime, Habit habit, long timestamp);
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2017 Á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.core.tasks;
import android.support.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.*;
import java.io.*;
import java.util.*;
@AutoFactory(allowSubclasses = true)
public class ExportCSVTask implements Task
{
private String archiveFilename;
@NonNull
private final List<Habit> selectedHabits;
private File outputDir;
@NonNull
private final ExportCSVTask.Listener listener;
@NonNull
private final HabitList habitList;
public ExportCSVTask(@Provided @NonNull HabitList habitList,
@NonNull List<Habit> selectedHabits,
@NonNull File outputDir,
@NonNull Listener listener)
{
this.listener = listener;
this.habitList = habitList;
this.selectedHabits = selectedHabits;
this.outputDir = outputDir;
}
@Override
public void doInBackground()
{
try
{
HabitsCSVExporter exporter;
exporter = new HabitsCSVExporter(habitList, selectedHabits, outputDir);
archiveFilename = exporter.writeArchive();
}
catch (IOException e)
{
e.printStackTrace();
}
}
@Override
public void onPostExecute()
{
listener.onExportCSVFinished(archiveFilename);
}
public interface Listener
{
void onExportCSVFinished(@Nullable String archiveFilename);
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.tasks;
import java.util.*;
public class SingleThreadTaskRunner implements TaskRunner
{
private List<Listener> listeners = new LinkedList<>();
@Override
public void addListener(Listener listener)
{
listeners.add(listener);
}
@Override
public void execute(Task task)
{
for(Listener l : listeners) l.onTaskStarted(task);
task.onAttached(this);
task.onPreExecute();
task.doInBackground();
task.onPostExecute();
for(Listener l : listeners) l.onTaskFinished(task);
}
@Override
public int getActiveTaskCount()
{
return 0;
}
@Override
public void publishProgress(Task task, int progress)
{
task.onProgressUpdate(progress);
}
@Override
public void removeListener(Listener listener)
{
listeners.remove(listener);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.tasks;
import android.support.annotation.*;
public interface Task
{
default void cancel() {}
void doInBackground();
default void onAttached(@NonNull TaskRunner runner) {}
default void onPostExecute() {}
default void onPreExecute() {}
default void onProgressUpdate(int value) {}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.core.tasks;
public interface TaskRunner
{
void addListener(Listener listener);
void removeListener(Listener listener);
void execute(Task task);
void publishProgress(Task task, int progress);
int getActiveTaskCount();
interface Listener
{
void onTaskStarted(Task task);
void onTaskFinished(Task task);
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2017 Á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.core.test;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.sqlite.*;
import org.isoron.uhabits.core.utils.*;
public class HabitFixtures
{
public boolean NON_DAILY_HABIT_CHECKS[] = {
true, false, false, true, true, true, false, false, true, true
};
private final ModelFactory modelFactory;
private HabitList habitList;
public HabitFixtures(ModelFactory modelFactory, HabitList habitList)
{
this.modelFactory = modelFactory;
this.habitList = habitList;
}
public Habit createEmptyHabit()
{
Habit habit = modelFactory.buildHabit();
habit.setName("Meditate");
habit.setDescription("Did you meditate this morning?");
habit.setColor(3);
habit.setFrequency(Frequency.DAILY);
saveIfSQLite(habit);
return habit;
}
public Habit createLongHabit()
{
Habit habit = createEmptyHabit();
habit.setFrequency(new Frequency(3, 7));
habit.setColor(4);
Timestamp today = DateUtils.getToday();
int marks[] = {0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27,
28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80,
81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120};
for (int mark : marks)
habit.getRepetitions().toggle(today.minus(mark));
return habit;
}
public Habit createNumericalHabit()
{
Habit habit = modelFactory.buildHabit();
habit.setType(Habit.NUMBER_HABIT);
habit.setName("Run");
habit.setDescription("How many miles did you run today?");
habit.setUnit("miles");
habit.setTargetType(Habit.AT_LEAST);
habit.setTargetValue(2.0);
habit.setColor(1);
saveIfSQLite(habit);
Timestamp today = DateUtils.getToday();
int times[] = {0, 1, 3, 5, 7, 8, 9, 10};
int values[] = {100, 200, 300, 400, 500, 600, 700, 800};
for (int i = 0; i < times.length; i++)
{
Timestamp timestamp = today.minus(times[i]);
habit.getRepetitions().add(new Repetition(timestamp, values[i]));
}
return habit;
}
public Habit createLongNumericalHabit(Timestamp reference)
{
Habit habit = modelFactory.buildHabit();
habit.setType(Habit.NUMBER_HABIT);
habit.setName("Walk");
habit.setDescription("How many steps did you walk today?");
habit.setUnit("steps");
habit.setTargetType(Habit.AT_LEAST);
habit.setTargetValue(100);
habit.setColor(1);
saveIfSQLite(habit);
int times[] = {0, 5, 9, 15, 17, 21, 23, 27, 28, 35, 41, 45, 47, 53, 56, 62, 70, 73, 78,
83, 86, 94, 101, 106, 113, 114, 120, 126, 130, 133, 141, 143, 148, 151, 157, 164,
166, 171, 173, 176, 179, 183, 191, 259, 264, 268, 270, 275, 282, 284, 289, 295,
302, 306, 310, 315, 323, 325, 328, 335, 343, 349, 351, 353, 357, 359, 360, 367,
372, 376, 380, 385, 393, 400, 404, 412, 415, 418, 422, 425, 433, 437, 444, 449,
455, 460, 462, 465, 470, 471, 479, 481, 485, 489, 494, 495, 500, 501, 503, 507};
int values[] = {230, 306, 148, 281, 134, 285, 104, 158, 325, 236, 303, 210, 118, 124,
301, 201, 156, 376, 347, 367, 396, 134, 160, 381, 155, 354, 231, 134, 164, 354,
236, 398, 199, 221, 208, 397, 253, 276, 214, 341, 299, 221, 353, 250, 341, 168,
374, 205, 182, 217, 297, 321, 104, 237, 294, 110, 136, 229, 102, 271, 250, 294,
158, 319, 379, 126, 282, 155, 288, 159, 215, 247, 207, 226, 244, 158, 371, 219,
272, 228, 350, 153, 356, 279, 394, 202, 213, 214, 112, 248, 139, 245, 165, 256,
370, 187, 208, 231, 341, 312};
for (int i = 0; i < times.length; i++)
{
Timestamp timestamp = reference.minus(times[i]);
habit.getRepetitions().add(new Repetition(timestamp, values[i]));
}
return habit;
}
public Habit createShortHabit()
{
Habit habit = modelFactory.buildHabit();
habit.setName("Wake up early");
habit.setDescription("Did you wake up before 6am?");
habit.setFrequency(new Frequency(2, 3));
saveIfSQLite(habit);
Timestamp timestamp = DateUtils.getToday();
for (boolean c : NON_DAILY_HABIT_CHECKS)
{
if (c) habit.getRepetitions().toggle(timestamp);
timestamp = timestamp.minus(1);
}
return habit;
}
private void saveIfSQLite(Habit habit)
{
if (!(habit.getRepetitions() instanceof SQLiteRepetitionList)) return;
habitList.add(habit);
}
}

View File

@@ -0,0 +1,210 @@
/*
* 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.core.ui;
import android.support.annotation.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.tasks.*;
import java.util.*;
import javax.inject.*;
@AppScope
public class NotificationTray
implements CommandRunner.Listener, Preferences.Listener
{
public static final String REMINDERS_CHANNEL_ID = "REMINDERS";
@NonNull
private final TaskRunner taskRunner;
@NonNull
private final CommandRunner commandRunner;
@NonNull
private final Preferences preferences;
private SystemTray systemTray;
@NonNull
private final HashMap<Habit, NotificationData> active;
@Inject
public NotificationTray(@NonNull TaskRunner taskRunner,
@NonNull CommandRunner commandRunner,
@NonNull Preferences preferences,
@NonNull SystemTray systemTray)
{
this.taskRunner = taskRunner;
this.commandRunner = commandRunner;
this.preferences = preferences;
this.systemTray = systemTray;
this.active = new HashMap<>();
}
public void cancel(@NonNull Habit habit)
{
int notificationId = getNotificationId(habit);
systemTray.removeNotification(notificationId);
active.remove(habit);
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (command instanceof ToggleRepetitionCommand)
{
ToggleRepetitionCommand toggleCmd =
(ToggleRepetitionCommand) command;
Habit habit = toggleCmd.getHabit();
taskRunner.execute(() ->
{
if (habit.getCheckmarks().getTodayValue() !=
Checkmark.UNCHECKED) cancel(habit);
});
}
if (command instanceof DeleteHabitsCommand)
{
DeleteHabitsCommand deleteCommand = (DeleteHabitsCommand) command;
List<Habit> deleted = deleteCommand.getSelected();
for (Habit habit : deleted)
cancel(habit);
}
}
@Override
public void onNotificationsChanged()
{
reshowAll();
}
public void show(@NonNull Habit habit, Timestamp timestamp, long reminderTime)
{
NotificationData data = new NotificationData(timestamp, reminderTime);
active.put(habit, data);
taskRunner.execute(new ShowNotificationTask(habit, data));
}
public void startListening()
{
commandRunner.addListener(this);
preferences.addListener(this);
}
public void stopListening()
{
commandRunner.removeListener(this);
preferences.removeListener(this);
}
private int getNotificationId(Habit habit)
{
Long id = habit.getId();
if (id == null) return 0;
return (int) (id % Integer.MAX_VALUE);
}
private void reshowAll()
{
for (Habit habit : active.keySet())
{
NotificationData data = active.get(habit);
taskRunner.execute(new ShowNotificationTask(habit, data));
}
}
public interface SystemTray
{
void removeNotification(int notificationId);
void showNotification(Habit habit,
int notificationId,
Timestamp timestamp,
long reminderTime);
}
class NotificationData
{
public final Timestamp timestamp;
public final long reminderTime;
public NotificationData(Timestamp timestamp, long reminderTime)
{
this.timestamp = timestamp;
this.reminderTime = reminderTime;
}
}
private class ShowNotificationTask implements Task
{
int todayValue;
private final Habit habit;
private final Timestamp timestamp;
private final long reminderTime;
public ShowNotificationTask(Habit habit, NotificationData data)
{
this.habit = habit;
this.timestamp = data.timestamp;
this.reminderTime = data.reminderTime;
}
@Override
public void doInBackground()
{
todayValue = habit.getCheckmarks().getTodayValue();
}
@Override
public void onPostExecute()
{
if (todayValue != Checkmark.UNCHECKED) return;
if (!shouldShowReminderToday()) return;
if (!habit.hasReminder()) return;
systemTray.showNotification(habit, getNotificationId(habit), timestamp,
reminderTime);
}
private boolean shouldShowReminderToday()
{
if (!habit.hasReminder()) return false;
Reminder reminder = habit.getReminder();
boolean reminderDays[] = reminder.getDays().toArray();
int weekday = timestamp.getWeekday();
return reminderDays[weekday];
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2017 Á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.core.ui;
import android.support.annotation.*;
import org.isoron.uhabits.core.preferences.*;
public abstract class ThemeSwitcher
{
public static final int THEME_DARK = 1;
public static final int THEME_LIGHT = 0;
private final Preferences preferences;
public ThemeSwitcher(@NonNull Preferences preferences)
{
this.preferences = preferences;
}
public void apply()
{
if (preferences.getTheme() == THEME_DARK)
{
if (preferences.isPureBlackEnabled()) applyPureBlackTheme();
else applyDarkTheme();
}
else
{
applyLightTheme();
}
}
public abstract void applyDarkTheme();
public abstract void applyLightTheme();
public abstract void applyPureBlackTheme();
public boolean isNightMode()
{
return preferences.getTheme() == THEME_DARK;
}
public void setTheme(int theme)
{
preferences.setTheme(theme);
}
public void toggleNightMode()
{
if (isNightMode()) setTheme(THEME_LIGHT);
else setTheme(THEME_DARK);
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2017 Á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.core.ui.callbacks;
public interface OnColorPickedCallback
{
void onColorPicked(int color);
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2017 Á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.core.ui.callbacks;
public interface OnConfirmedCallback
{
void onConfirmed();
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2017 Á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.core.ui.callbacks;
public interface OnFinishedCallback
{
void onFinish();
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.about;
import android.support.annotation.*;
import org.isoron.uhabits.core.preferences.*;
import javax.inject.*;
public class AboutBehavior
{
private int developerCountdown = 5;
@NonNull
private Preferences prefs;
@NonNull
private Screen screen;
@Inject
public AboutBehavior(@NonNull Preferences prefs, @NonNull Screen screen)
{
this.prefs = prefs;
this.screen = screen;
}
public void onPressDeveloperCountdown()
{
developerCountdown--;
if (developerCountdown == 0)
{
prefs.setDeveloper(true);
screen.showMessage(Message.YOU_ARE_NOW_A_DEVELOPER);
}
}
public void onRateApp()
{
screen.showRateAppWebsite();
}
public void onSendFeedback()
{
screen.showSendFeedbackScreen();
}
public void onTranslateApp()
{
screen.showTranslationWebsite();
}
public void onViewSourceCode()
{
screen.showSourceCodeWebsite();
}
public enum Message
{
YOU_ARE_NOW_A_DEVELOPER
}
public interface Screen
{
void showMessage(Message message);
void showRateAppWebsite();
void showSendFeedbackScreen();
void showSourceCodeWebsite();
void showTranslationWebsite();
}
}

View File

@@ -0,0 +1,415 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.habits.list;
import android.support.annotation.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
import javax.inject.*;
/**
* A HabitCardListCache fetches and keeps a cache of all the data necessary to
* render a HabitCardListView.
* <p>
* This is needed since performing database lookups during scrolling can make
* the ListView very slow. It also registers itself as an observer of the
* models, in order to update itself automatically.
* <p>
* Note that this class is singleton-scoped, therefore it is shared among all
* activities.
*/
@AppScope
public class HabitCardListCache implements CommandRunner.Listener
{
private int checkmarkCount;
private Task currentFetchTask;
@NonNull
private Listener listener;
@NonNull
private CacheData data;
@NonNull
private HabitList allHabits;
@NonNull
private HabitList filteredHabits;
private final TaskRunner taskRunner;
private final CommandRunner commandRunner;
@Inject
public HabitCardListCache(@NonNull HabitList allHabits,
@NonNull CommandRunner commandRunner,
@NonNull TaskRunner taskRunner)
{
this.allHabits = allHabits;
this.commandRunner = commandRunner;
this.filteredHabits = allHabits;
this.taskRunner = taskRunner;
this.listener = new Listener() {};
data = new CacheData();
}
public void cancelTasks()
{
if (currentFetchTask != null) currentFetchTask.cancel();
}
public int[] getCheckmarks(long habitId)
{
return data.checkmarks.get(habitId);
}
/**
* Returns the habits that occupies a certain position on the list.
*
* @param position the position of the habit
* @return the habit at given position or null if position is invalid
*/
@Nullable
public synchronized Habit getHabitByPosition(int position)
{
if(position < 0 || position >= data.habits.size()) return null;
return data.habits.get(position);
}
public int getHabitCount()
{
return data.habits.size();
}
public HabitList.Order getOrder()
{
return filteredHabits.getOrder();
}
public double getScore(long habitId)
{
return data.scores.get(habitId);
}
public void onAttached()
{
refreshAllHabits();
commandRunner.addListener(this);
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (refreshKey == null) refreshAllHabits();
else refreshHabit(refreshKey);
}
public void onDetached()
{
commandRunner.removeListener(this);
}
public void refreshAllHabits()
{
if (currentFetchTask != null) currentFetchTask.cancel();
currentFetchTask = new RefreshTask();
taskRunner.execute(currentFetchTask);
}
public void refreshHabit(long id)
{
taskRunner.execute(new RefreshTask(id));
}
public void remove(@NonNull Long id)
{
Habit h = data.id_to_habit.get(id);
if (h == null) return;
int position = data.habits.indexOf(h);
data.habits.remove(position);
data.id_to_habit.remove(id);
data.checkmarks.remove(id);
data.scores.remove(id);
listener.onItemRemoved(position);
}
public void reorder(int from, int to)
{
Habit fromHabit = data.habits.get(from);
data.habits.remove(from);
data.habits.add(to, fromHabit);
listener.onItemMoved(from, to);
}
public void setCheckmarkCount(int checkmarkCount)
{
this.checkmarkCount = checkmarkCount;
}
public void setFilter(HabitMatcher matcher)
{
filteredHabits = allHabits.getFiltered(matcher);
}
public void setListener(@NonNull Listener listener)
{
this.listener = listener;
}
public void setOrder(HabitList.Order order)
{
allHabits.setOrder(order);
filteredHabits.setOrder(order);
refreshAllHabits();
}
/**
* Interface definition for a callback to be invoked when the data on the
* cache has been modified.
*/
public interface Listener
{
default void onItemChanged(int position) {}
default void onItemInserted(int position) {}
default void onItemMoved(int oldPosition, int newPosition) {}
default void onItemRemoved(int position) {}
default void onRefreshFinished() {}
}
private class CacheData
{
@NonNull
public HashMap<Long, Habit> id_to_habit;
@NonNull
public List<Habit> habits;
@NonNull
public HashMap<Long, int[]> checkmarks;
@NonNull
public HashMap<Long, Double> scores;
/**
* Creates a new CacheData without any content.
*/
public CacheData()
{
id_to_habit = new HashMap<>();
habits = new LinkedList<>();
checkmarks = new HashMap<>();
scores = new HashMap<>();
}
public void copyCheckmarksFrom(@NonNull CacheData oldData)
{
int[] empty = new int[checkmarkCount];
for (Long id : id_to_habit.keySet())
{
if (oldData.checkmarks.containsKey(id))
checkmarks.put(id, oldData.checkmarks.get(id));
else checkmarks.put(id, empty);
}
}
public void copyScoresFrom(@NonNull CacheData oldData)
{
for (Long id : id_to_habit.keySet())
{
if (oldData.scores.containsKey(id))
scores.put(id, oldData.scores.get(id));
else scores.put(id, 0.0);
}
}
public void fetchHabits()
{
for (Habit h : filteredHabits)
{
habits.add(h);
id_to_habit.put(h.getId(), h);
}
}
}
private class RefreshTask implements Task
{
@NonNull
private CacheData newData;
@Nullable
private Long targetId;
private boolean isCancelled;
private TaskRunner runner;
public RefreshTask()
{
newData = new CacheData();
targetId = null;
isCancelled = false;
}
public RefreshTask(long targetId)
{
newData = new CacheData();
this.targetId = targetId;
}
@Override
public void cancel()
{
isCancelled = true;
}
@Override
public void doInBackground()
{
newData.fetchHabits();
newData.copyScoresFrom(data);
newData.copyCheckmarksFrom(data);
Timestamp dateTo = DateUtils.getToday();
Timestamp dateFrom = dateTo.minus(checkmarkCount - 1);
runner.publishProgress(this, -1);
for (int position = 0; position < newData.habits.size(); position++)
{
if (isCancelled) return;
Habit habit = newData.habits.get(position);
Long id = habit.getId();
if (targetId != null && !targetId.equals(id)) continue;
newData.scores.put(id, habit.getScores().getTodayValue());
newData.checkmarks.put(id, habit
.getCheckmarks()
.getValues(dateFrom, dateTo));
runner.publishProgress(this, position);
}
}
@Override
public void onAttached(@NonNull TaskRunner runner)
{
this.runner = runner;
}
@Override
public void onPostExecute()
{
currentFetchTask = null;
listener.onRefreshFinished();
}
@Override
public void onProgressUpdate(int currentPosition)
{
if (currentPosition < 0) processRemovedHabits();
else processPosition(currentPosition);
}
private void performInsert(Habit habit, int position)
{
Long id = habit.getId();
data.habits.add(position, habit);
data.id_to_habit.put(id, habit);
data.scores.put(id, newData.scores.get(id));
data.checkmarks.put(id, newData.checkmarks.get(id));
listener.onItemInserted(position);
}
private void performMove(Habit habit, int fromPosition, int toPosition)
{
data.habits.remove(fromPosition);
data.habits.add(toPosition, habit);
listener.onItemMoved(fromPosition, toPosition);
}
private void performUpdate(Long id, int position)
{
double oldScore = data.scores.get(id);
int[] oldCheckmarks = data.checkmarks.get(id);
double newScore = newData.scores.get(id);
int[] newCheckmarks = newData.checkmarks.get(id);
boolean unchanged = true;
if (oldScore != newScore) unchanged = false;
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false;
if (unchanged) return;
data.scores.put(id, newScore);
data.checkmarks.put(id, newCheckmarks);
listener.onItemChanged(position);
}
private void processPosition(int currentPosition)
{
Habit habit = newData.habits.get(currentPosition);
Long id = habit.getId();
int prevPosition = data.habits.indexOf(habit);
if (prevPosition < 0)
{
performInsert(habit, currentPosition);
}
else
{
if (prevPosition != currentPosition)
performMove(habit, prevPosition, currentPosition);
performUpdate(id, currentPosition);
}
}
private void processRemovedHabits()
{
Set<Long> before = data.id_to_habit.keySet();
Set<Long> after = newData.id_to_habit.keySet();
Set<Long> removed = new TreeSet<>(before);
removed.removeAll(after);
for (Long id : removed) remove(id);
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.habits.list;
import android.support.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.utils.*;
/**
* Provides a list of hints to be shown at the application startup, and takes
* care of deciding when a new hint should be shown.
*/
@AutoFactory
public class HintList
{
private final Preferences prefs;
@NonNull
private final String[] hints;
/**
* Constructs a new list containing the provided hints.
*
* @param hints initial list of hints
*/
public HintList(@Provided @NonNull Preferences prefs,
@NonNull String hints[])
{
this.prefs = prefs;
this.hints = hints;
}
/**
* Returns a new hint to be shown to the user.
* <p>
* The hint returned is marked as read on the list, and will not be returned
* again. In case all hints have already been read, and there is nothing
* left, returns null.
*
* @return the next hint to be shown, or null if none
*/
public String pop()
{
int next = prefs.getLastHintNumber() + 1;
if (next >= hints.length) return null;
prefs.updateLastHint(next, DateUtils.getToday());
return hints[next];
}
/**
* Returns whether it is time to show a new hint or not.
*
* @return true if hint should be shown, false otherwise
*/
public boolean shouldShow()
{
Timestamp today = DateUtils.getToday();
Timestamp lastHintTimestamp = prefs.getLastHintTimestamp();
return (lastHintTimestamp.isOlderThan(today));
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.habits.list;
import android.support.annotation.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
public class ListHabitsBehavior
{
@NonNull
private final HabitList habitList;
@NonNull
private final DirFinder dirFinder;
@NonNull
private final TaskRunner taskRunner;
@NonNull
private final Screen screen;
@NonNull
private final CommandRunner commandRunner;
@NonNull
private final Preferences prefs;
@NonNull
private final BugReporter bugReporter;
@Inject
public ListHabitsBehavior(@NonNull HabitList habitList,
@NonNull DirFinder dirFinder,
@NonNull TaskRunner taskRunner,
@NonNull Screen screen,
@NonNull CommandRunner commandRunner,
@NonNull Preferences prefs,
@NonNull BugReporter bugReporter)
{
this.habitList = habitList;
this.dirFinder = dirFinder;
this.taskRunner = taskRunner;
this.screen = screen;
this.commandRunner = commandRunner;
this.prefs = prefs;
this.bugReporter = bugReporter;
}
public void onClickHabit(@NonNull Habit h)
{
screen.showHabitScreen(h);
}
public void onEdit(@NonNull Habit habit, Timestamp timestamp)
{
CheckmarkList checkmarks = habit.getCheckmarks();
double oldValue = checkmarks.getValues(timestamp, timestamp)[0];
screen.showNumberPicker(oldValue / 1000, habit.getUnit(), newValue ->
{
newValue = Math.round(newValue * 1000);
commandRunner.execute(
new CreateRepetitionCommand(habit, timestamp, (int) newValue),
habit.getId());
});
}
public void onExportCSV()
{
List<Habit> selected = new LinkedList<>();
for (Habit h : habitList) selected.add(h);
File outputDir = dirFinder.getCSVOutputDir();
taskRunner.execute(
new ExportCSVTask(habitList, selected, outputDir, filename ->
{
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(Message.COULD_NOT_EXPORT);
}));
}
public void onFirstRun()
{
prefs.setFirstRun(false);
prefs.updateLastHint(-1, DateUtils.getToday());
screen.showIntroScreen();
}
public void onReorderHabit(@NonNull Habit from, @NonNull Habit to)
{
taskRunner.execute(() -> habitList.reorder(from, to));
}
public void onRepairDB()
{
taskRunner.execute(() ->
{
habitList.repair();
screen.showMessage(Message.DATABASE_REPAIRED);
});
}
public void onSendBugReport()
{
bugReporter.dumpBugReportToFile();
try
{
String log = bugReporter.getBugReport();
screen.showSendBugReportToDeveloperScreen(log);
}
catch (IOException e)
{
e.printStackTrace();
screen.showMessage(Message.COULD_NOT_GENERATE_BUG_REPORT);
}
}
public void onStartup()
{
prefs.incrementLaunchCount();
if (prefs.isFirstRun()) onFirstRun();
}
public void onToggle(@NonNull Habit habit, Timestamp timestamp)
{
commandRunner.execute(
new ToggleRepetitionCommand(habitList, habit, timestamp),
habit.getId());
}
public enum Message
{
COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED,
COULD_NOT_GENERATE_BUG_REPORT, FILE_NOT_RECOGNIZED
}
public interface BugReporter
{
void dumpBugReportToFile();
String getBugReport() throws IOException;
}
public interface DirFinder
{
File getCSVOutputDir();
}
public interface NumberPickerCallback
{
void onNumberPicked(double newValue);
}
public interface Screen
{
void showHabitScreen(@NonNull Habit h);
void showIntroScreen();
void showMessage(@NonNull Message m);
void showNumberPicker(double value,
@NonNull String unit,
@NonNull NumberPickerCallback callback);
void showSendBugReportToDeveloperScreen(String log);
void showSendFileScreen(@NonNull String filename);
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.habits.list;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.*;
import javax.inject.*;
public class ListHabitsMenuBehavior
{
@NonNull
private final Screen screen;
@NonNull
private final Adapter adapter;
@NonNull
private final Preferences preferences;
@NonNull
private final ThemeSwitcher themeSwitcher;
private boolean showCompleted;
private boolean showArchived;
@Inject
public ListHabitsMenuBehavior(@NonNull Screen screen,
@NonNull Adapter adapter,
@NonNull Preferences preferences,
@NonNull ThemeSwitcher themeSwitcher)
{
this.screen = screen;
this.adapter = adapter;
this.preferences = preferences;
this.themeSwitcher = themeSwitcher;
showCompleted = preferences.getShowCompleted();
showArchived = preferences.getShowArchived();
updateAdapterFilter();
}
public void onCreateHabit()
{
screen.showCreateHabitScreen();
}
public void onViewFAQ()
{
screen.showFAQScreen();
}
public void onViewAbout()
{
screen.showAboutScreen();
}
public void onViewSettings()
{
screen.showSettingsScreen();
}
public void onToggleShowArchived()
{
showArchived = !showArchived;
preferences.setShowArchived(showArchived);
updateAdapterFilter();
}
public void onToggleShowCompleted()
{
showCompleted = !showCompleted;
preferences.setShowCompleted(showCompleted);
updateAdapterFilter();
}
public void onSortByColor()
{
adapter.setOrder(HabitList.Order.BY_COLOR);
}
public void onSortByManually()
{
adapter.setOrder(HabitList.Order.BY_POSITION);
}
public void onSortByScore()
{
adapter.setOrder(HabitList.Order.BY_SCORE);
}
public void onSortByName()
{
adapter.setOrder(HabitList.Order.BY_NAME);
}
public void onToggleNightMode()
{
themeSwitcher.toggleNightMode();
screen.applyTheme();
}
private void updateAdapterFilter()
{
adapter.setFilter(new HabitMatcherBuilder()
.setArchivedAllowed(showArchived)
.setCompletedAllowed(showCompleted)
.build());
adapter.refresh();
}
public interface Adapter
{
void refresh();
void setFilter(HabitMatcher build);
void setOrder(HabitList.Order order);
}
public interface Screen
{
void applyTheme();
void showAboutScreen();
void showCreateHabitScreen();
void showFAQScreen();
void showSettingsScreen();
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.habits.list;
import android.support.annotation.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import java.util.*;
import javax.inject.*;
public class ListHabitsSelectionMenuBehavior
{
@NonNull
private final Screen screen;
@NonNull
CommandRunner commandRunner;
@NonNull
private final Adapter adapter;
@NonNull
private final HabitList habitList;
@Inject
public ListHabitsSelectionMenuBehavior(@NonNull HabitList habitList,
@NonNull Screen screen,
@NonNull Adapter adapter,
@NonNull CommandRunner commandRunner)
{
this.habitList = habitList;
this.screen = screen;
this.adapter = adapter;
this.commandRunner = commandRunner;
}
public boolean canArchive()
{
for (Habit h : adapter.getSelected())
if (h.isArchived()) return false;
return true;
}
public boolean canEdit()
{
return (adapter.getSelected().size() == 1);
}
public boolean canUnarchive()
{
for (Habit h : adapter.getSelected())
if (!h.isArchived()) return false;
return true;
}
public void onArchiveHabits()
{
commandRunner.execute(
new ArchiveHabitsCommand(habitList, adapter.getSelected()), null);
adapter.clearSelection();
}
public void onChangeColor()
{
List<Habit> selected = adapter.getSelected();
Habit first = selected.get(0);
screen.showColorPicker(first.getColor(), selectedColor ->
{
commandRunner.execute(
new ChangeHabitColorCommand(habitList, selected, selectedColor),
null);
adapter.clearSelection();
});
}
public void onDeleteHabits()
{
List<Habit> selected = adapter.getSelected();
screen.showDeleteConfirmationScreen(() ->
{
adapter.performRemove(selected);
commandRunner.execute(new DeleteHabitsCommand(habitList, selected),
null);
adapter.clearSelection();
});
}
public void onEditHabits()
{
screen.showEditHabitsScreen(adapter.getSelected());
}
public void onUnarchiveHabits()
{
commandRunner.execute(
new UnarchiveHabitsCommand(habitList, adapter.getSelected()), null);
adapter.clearSelection();
}
public interface Adapter
{
void clearSelection();
List<Habit> getSelected();
void performRemove(List<Habit> selected);
}
public interface Screen
{
void showColorPicker(int defaultColor,
@NonNull OnColorPickedCallback callback);
void showDeleteConfirmationScreen(
@NonNull OnConfirmedCallback callback);
void showEditHabitsScreen(@NonNull List<Habit> selected);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.habits.show;
import android.support.annotation.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import javax.inject.*;
public class ShowHabitBehavior
{
private HabitList habitList;
@NonNull
private final Habit habit;
@NonNull
private final CommandRunner commandRunner;
@NonNull
private Screen screen;
@Inject
public ShowHabitBehavior(@NonNull HabitList habitList,
@NonNull CommandRunner commandRunner,
@NonNull Habit habit,
@NonNull Screen screen)
{
this.habitList = habitList;
this.habit = habit;
this.commandRunner = commandRunner;
this.screen = screen;
}
public void onEditHistory()
{
screen.showEditHistoryScreen();
}
public void onToggleCheckmark(Timestamp timestamp)
{
commandRunner.execute(
new ToggleRepetitionCommand(habitList, habit, timestamp), null);
}
public interface Screen
{
void showEditHistoryScreen();
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2017 Á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.core.ui.screens.habits.show;
import android.support.annotation.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
import static java.lang.Math.*;
public class ShowHabitMenuBehavior
{
private HabitList habitList;
@NonNull
private final Habit habit;
@NonNull
private final TaskRunner taskRunner;
@NonNull
private Screen screen;
@NonNull
private System system;
@NonNull
private CommandRunner commandRunner;
@Inject
public ShowHabitMenuBehavior(@NonNull HabitList habitList,
@NonNull Habit habit,
@NonNull TaskRunner taskRunner,
@NonNull Screen screen,
@NonNull System system,
@NonNull CommandRunner commandRunner)
{
this.habitList = habitList;
this.habit = habit;
this.taskRunner = taskRunner;
this.screen = screen;
this.system = system;
this.commandRunner = commandRunner;
}
public void onEditHabit()
{
screen.showEditHabitScreen(habit);
}
public void onExportCSV()
{
List<Habit> selected = Collections.singletonList(habit);
File outputDir = system.getCSVOutputDir();
taskRunner.execute(
new ExportCSVTask(habitList, selected, outputDir, filename ->
{
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(Message.COULD_NOT_EXPORT);
}));
}
public void onDeleteHabit()
{
List<Habit> selected = Collections.singletonList(habit);
screen.showDeleteConfirmationScreen(() -> {
commandRunner.execute(new DeleteHabitsCommand(habitList, selected),
null);
screen.close();
});
}
public void onRandomize()
{
Random random = new Random();
habit.getRepetitions().removeAll();
double strength = 50;
for (int i = 0; i < 365 * 5; i++)
{
if (i % 7 == 0) strength = max(0, min(100, strength + 10 * random.nextGaussian()));
if (random.nextInt(100) > strength) continue;
int value = 1;
if (habit.isNumerical())
value = (int) (1000 + 250 * random.nextGaussian() * strength / 100) * 1000;
habit.getRepetitions().add(new Repetition(DateUtils.getToday().minus(i), value));
}
habit.invalidateNewerThan(Timestamp.ZERO);
}
public enum Message
{
COULD_NOT_EXPORT, HABIT_DELETED
}
public interface Screen
{
void showEditHabitScreen(@NonNull Habit habit);
void showMessage(Message m);
void showSendFileScreen(String filename);
void showDeleteConfirmationScreen(
@NonNull OnConfirmedCallback callback);
void close();
}
public interface System
{
File getCSVOutputDir();
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2017 Á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.core.ui.widgets;
import android.support.annotation.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.*;
import javax.inject.*;
public class WidgetBehavior
{
private HabitList habitList;
@NonNull
private final CommandRunner commandRunner;
private NotificationTray notificationTray;
@Inject
public WidgetBehavior(@NonNull HabitList habitList,
@NonNull CommandRunner commandRunner,
@NonNull NotificationTray notificationTray)
{
this.habitList = habitList;
this.commandRunner = commandRunner;
this.notificationTray = notificationTray;
}
public void onAddRepetition(@NonNull Habit habit, Timestamp timestamp)
{
notificationTray.cancel(habit);
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
if (rep != null) return;
performToggle(habit, timestamp);
}
public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp)
{
notificationTray.cancel(habit);
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
if (rep == null) return;
performToggle(habit, timestamp);
}
public void onToggleRepetition(@NonNull Habit habit, Timestamp timestamp)
{
performToggle(habit, timestamp);
}
private void performToggle(@NonNull Habit habit, Timestamp timestamp)
{
commandRunner.execute(
new ToggleRepetitionCommand(habitList, habit, timestamp),
habit.getId());
}
}

View File

@@ -0,0 +1,27 @@
package org.isoron.uhabits.core.utils;
public class ColorConstants
{
public static String CSV_PALETTE[] = {
"#D32F2F", // 0 red
"#E64A19", // 1 deep orange
"#F57C00", // 2 orange
"#FF8F00", // 3 amber
"#F9A825", // 4 yellow
"#AFB42B", // 5 lime
"#7CB342", // 6 light green
"#388E3C", // 7 green
"#00897B", // 8 teal
"#00ACC1", // 9 cyan
"#039BE5", // 10 light blue
"#1976D2", // 11 blue
"#303F9F", // 12 indigo
"#5E35B1", // 13 deep purple
"#8E24AA", // 14 purple
"#D81B60", // 15 pink
"#5D4037", // 16 brown
"#303030", // 17 dark grey
"#757575", // 18 grey
"#aaaaaa" // 19 light grey
};
}

View File

@@ -0,0 +1,47 @@
/*
* 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.core.utils;
import android.support.annotation.*;
import java.text.*;
import java.util.*;
public class DateFormats
{
@NonNull
public static SimpleDateFormat fromSkeleton(@NonNull String skeleton,
@NonNull Locale locale)
{
SimpleDateFormat df = new SimpleDateFormat(skeleton, locale);
df.setTimeZone(TimeZone.getTimeZone("UTC"));
return df;
}
public static SimpleDateFormat getBackupDateFormat()
{
return fromSkeleton("yyyy-MM-dd HHmmss", Locale.US);
}
public static SimpleDateFormat getCSVDateFormat()
{
return fromSkeleton("yyyy-MM-dd", Locale.US);
}
}

View File

@@ -0,0 +1,261 @@
/*
* 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.core.utils;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import java.util.*;
import static java.util.Calendar.*;
public abstract class DateUtils
{
private static Long fixedLocalTime = null;
private static TimeZone fixedTimeZone = null;
private static Locale fixedLocale = null;
/**
* Time of the day when the new day starts.
*/
public static final int NEW_DAY_OFFSET = 3;
/**
* Number of milliseconds in one day.
*/
public static final long DAY_LENGTH = 24 * 60 * 60 * 1000;
/**
* Number of milliseconds in one hour.
*/
public static final long HOUR_LENGTH = 60 * 60 * 1000;
public static long applyTimezone(long localTimestamp)
{
TimeZone tz = getTimezone();
return localTimestamp - tz.getOffset(localTimestamp - tz.getOffset(localTimestamp));
}
public static String formatHeaderDate(GregorianCalendar day)
{
Locale locale = getLocale();
String dayOfMonth = Integer.toString(day.get(DAY_OF_MONTH));
String dayOfWeek = day.getDisplayName(DAY_OF_WEEK, SHORT, locale);
return dayOfWeek + "\n" + dayOfMonth;
}
private static GregorianCalendar getCalendar(long timestamp)
{
GregorianCalendar day =
new GregorianCalendar(TimeZone.getTimeZone("GMT"), getLocale());
day.setTimeInMillis(timestamp);
return day;
}
private static String[] getDayNames(int format)
{
String[] wdays = new String[7];
Calendar day = new GregorianCalendar();
day.set(DAY_OF_WEEK, Calendar.SATURDAY);
for (int i = 0; i < wdays.length; i++)
{
wdays[i] =
day.getDisplayName(DAY_OF_WEEK, format, getLocale());
day.add(DAY_OF_MONTH, 1);
}
return wdays;
}
public static long getLocalTime()
{
if (fixedLocalTime != null) return fixedLocalTime;
TimeZone tz = getTimezone();
long now = new Date().getTime();
return now + tz.getOffset(now);
}
/**
* @return array with weekday names starting according to locale settings,
* e.g. [Mo,Di,Mi,Do,Fr,Sa,So] in Germany
*/
public static String[] getLocaleDayNames(int format)
{
String[] days = new String[7];
Calendar calendar = new GregorianCalendar();
calendar.set(DAY_OF_WEEK, calendar.getFirstDayOfWeek());
for (int i = 0; i < days.length; i++)
{
days[i] = calendar.getDisplayName(DAY_OF_WEEK, format,
getLocale());
calendar.add(DAY_OF_MONTH, 1);
}
return days;
}
/**
* @return array with week days numbers starting according to locale
* settings, e.g. [2,3,4,5,6,7,1] in Europe
*/
public static Integer[] getLocaleWeekdayList()
{
Integer[] dayNumbers = new Integer[7];
Calendar calendar = new GregorianCalendar();
calendar.set(DAY_OF_WEEK, calendar.getFirstDayOfWeek());
for (int i = 0; i < dayNumbers.length; i++)
{
dayNumbers[i] = calendar.get(DAY_OF_WEEK);
calendar.add(DAY_OF_MONTH, 1);
}
return dayNumbers;
}
public static String[] getLongDayNames()
{
return getDayNames(GregorianCalendar.LONG);
}
public static String[] getShortDayNames()
{
return getDayNames(SHORT);
}
@NonNull
public static Timestamp getToday()
{
return new Timestamp(getStartOfToday());
}
public static long getStartOfDay(long timestamp)
{
return (timestamp / DAY_LENGTH) * DAY_LENGTH;
}
public static long getStartOfToday()
{
return getStartOfDay(getLocalTime() - NEW_DAY_OFFSET * HOUR_LENGTH);
}
public static long millisecondsUntilTomorrow()
{
return getStartOfToday() + DAY_LENGTH -
(getLocalTime() - NEW_DAY_OFFSET * HOUR_LENGTH);
}
public static GregorianCalendar getStartOfTodayCalendar()
{
return getCalendar(getStartOfToday());
}
private static TimeZone getTimezone()
{
if(fixedTimeZone != null) return fixedTimeZone;
return TimeZone.getDefault();
}
public static void setFixedTimeZone(TimeZone tz)
{
fixedTimeZone = tz;
}
public static long removeTimezone(long timestamp)
{
TimeZone tz = getTimezone();
return timestamp + tz.getOffset(timestamp);
}
public static void setFixedLocalTime(Long timestamp)
{
fixedLocalTime = timestamp;
}
public static void setFixedLocale(Locale locale)
{
fixedLocale = locale;
}
private static Locale getLocale()
{
if(fixedLocale != null) return fixedLocale;
return Locale.getDefault();
}
public static Long truncate(TruncateField field, long timestamp)
{
GregorianCalendar cal = DateUtils.getCalendar(timestamp);
switch (field)
{
case MONTH:
cal.set(DAY_OF_MONTH, 1);
return cal.getTimeInMillis();
case WEEK_NUMBER:
int firstWeekday = cal.getFirstDayOfWeek();
int weekday = cal.get(DAY_OF_WEEK);
int delta = weekday - firstWeekday;
if (delta < 0) delta += 7;
cal.add(Calendar.DAY_OF_YEAR, -delta);
return cal.getTimeInMillis();
case QUARTER:
int quarter = cal.get(Calendar.MONTH) / 3;
cal.set(DAY_OF_MONTH, 1);
cal.set(Calendar.MONTH, quarter * 3);
return cal.getTimeInMillis();
case YEAR:
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(DAY_OF_MONTH, 1);
return cal.getTimeInMillis();
default:
throw new IllegalArgumentException();
}
}
public static long getUpcomingTimeInMillis(int hour, int minute)
{
Calendar calendar = DateUtils.getStartOfTodayCalendar();
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, 0);
Long time = calendar.getTimeInMillis();
if (DateUtils.getLocalTime() > time)
time += DateUtils.DAY_LENGTH;
return applyTimezone(time);
}
public enum TruncateField
{
MONTH, WEEK_NUMBER, YEAR, QUARTER
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2017 Á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.core.utils;
import org.isoron.uhabits.core.*;
import java.util.*;
import java.util.concurrent.*;
import javax.inject.*;
/**
* A class that emits events when a new day starts.
*/
@AppScope
public class MidnightTimer
{
private final List<MidnightListener> listeners;
private ScheduledExecutorService executor;
@Inject
public MidnightTimer()
{
this.listeners = new LinkedList<>();
}
public synchronized void addListener(MidnightListener listener)
{
this.listeners.add(listener);
}
public synchronized void onPause()
{
executor.shutdownNow();
}
public synchronized void onResume()
{
executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> notifyListeners(),
DateUtils.millisecondsUntilTomorrow() + 1000,
DateUtils.DAY_LENGTH, TimeUnit.MILLISECONDS);
}
public synchronized void removeListener(MidnightListener listener)
{
this.listeners.remove(listener);
}
private synchronized void notifyListeners()
{
for (MidnightListener l : listeners) l.atMidnight();
}
public interface MidnightListener
{
void atMidnight();
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2017 Á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.core.utils;
import org.apache.commons.lang3.builder.*;
import java.math.*;
import java.util.*;
public class StringUtils
{
private static StandardToStringStyle toStringStyle = null;
public static String getRandomId()
{
return new BigInteger(260, new Random()).toString(32).substring(0, 32);
}
public static ToStringStyle defaultToStringStyle()
{
if (toStringStyle == null)
{
toStringStyle = new StandardToStringStyle();
toStringStyle.setFieldSeparator(", ");
toStringStyle.setUseClassName(false);
toStringStyle.setUseIdentityHashCode(false);
toStringStyle.setContentStart("{");
toStringStyle.setContentEnd("}");
toStringStyle.setFieldNameValueSeparator(": ");
toStringStyle.setArrayStart("[");
toStringStyle.setArrayEnd("]");
}
return toStringStyle;
}
public static String joinLongs(long values[])
{
return org.apache.commons.lang3.StringUtils.join(values, ',');
}
public static long[] splitLongs(String str)
{
String parts[] = org.apache.commons.lang3.StringUtils.split(str, ',');
long numbers[] = new long[parts.length];
for (int i = 0; i < parts.length; i++) numbers[i] = Long.valueOf(parts[i]);
return numbers;
}
}

View File

@@ -0,0 +1,41 @@
create table Habits (
id integer primary key autoincrement,
archived integer,
color integer,
description text,
freq_den integer,
freq_num integer,
highlight integer,
name text,
position integer,
reminder_hour integer,
reminder_min integer
);
create table Checkmarks (
id integer primary key autoincrement,
habit integer references habits(id),
timestamp integer,
value integer
);
create table Repetitions (
id integer primary key autoincrement,
habit integer references habits(id),
timestamp integer
);
create table Streak (
id integer primary key autoincrement,
end integer,
habit integer references habits(id),
length integer,
start integer
);
create table Score (
id integer primary key autoincrement,
habit integer references habits(id),
score integer,
timestamp integer
);

View File

@@ -0,0 +1,3 @@
delete from Score;
delete from Streak;
delete from Checkmarks;

View File

@@ -0,0 +1 @@
alter table Habits add column reminder_days integer not null default 127;

View File

@@ -0,0 +1,3 @@
delete from Score;
delete from Streak;
delete from Checkmarks;

View File

@@ -0,0 +1,4 @@
create index idx_score_habit_timestamp on Score(habit, timestamp);
create index idx_checkmark_habit_timestamp on Checkmarks(habit, timestamp);
create index idx_repetitions_habit_timestamp on Repetitions(habit, timestamp);
create index idx_streak_habit_end on Streak(habit, end);

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