Simplify SQLite lists

pull/312/head
Alinson S. Xavier 8 years ago
parent edeba897fb
commit 6d06e06840

@ -115,9 +115,8 @@ public class HabitFixtures
return habit; return habit;
} }
public void purgeHabits(HabitList habitList) public synchronized void purgeHabits(HabitList habitList)
{ {
for (Habit h : habitList) habitList.removeAll();
habitList.remove(h);
} }
} }

@ -22,6 +22,9 @@ package org.isoron.uhabits.models.sqlite;
import android.support.test.runner.*; import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*; import android.test.suitebuilder.annotation.*;
import com.activeandroid.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
@ -35,25 +38,18 @@ import static org.hamcrest.core.IsEqual.*;
@MediumTest @MediumTest
public class HabitRecordTest extends BaseAndroidTest public class HabitRecordTest extends BaseAndroidTest
{ {
private Habit habit;
private SQLiteRepository<HabitRecord> sqlite =
new SQLiteRepository<>(HabitRecord.class, Cache.openDatabase());
@Before
@Override @Override
public void setUp() public void setUp()
{ {
super.setUp(); super.setUp();
Habit h = component.getModelFactory().buildHabit(); habit = component.getModelFactory().buildHabit();
h.setName("Hello world");
h.setId(1000L);
HabitRecord record = new HabitRecord();
record.copyFrom(h);
record.position = 0;
record.save(1000L);
}
@Test
public void testCopyFrom()
{
Habit habit = component.getModelFactory().buildHabit();
habit.setName("Hello world"); habit.setName("Hello world");
habit.setDescription("Did you greet the world today?"); habit.setDescription("Did you greet the world today?");
habit.setColor(1); habit.setColor(1);
@ -61,7 +57,11 @@ public class HabitRecordTest extends BaseAndroidTest
habit.setFrequency(Frequency.THREE_TIMES_PER_WEEK); habit.setFrequency(Frequency.THREE_TIMES_PER_WEEK);
habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY)); habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY));
habit.setId(1000L); habit.setId(1000L);
}
@Test
public void testCopyFrom()
{
HabitRecord rec = new HabitRecord(); HabitRecord rec = new HabitRecord();
rec.copyFrom(habit); rec.copyFrom(habit);

@ -22,11 +22,12 @@ package org.isoron.uhabits.models.sqlite;
import android.support.test.runner.*; import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*; import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*; import com.activeandroid.*;
import com.google.common.collect.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
import org.junit.*; import org.junit.*;
import org.junit.rules.*; import org.junit.rules.*;
@ -49,6 +50,8 @@ public class SQLiteHabitListTest extends BaseAndroidTest
private ModelFactory modelFactory; private ModelFactory modelFactory;
private SQLiteRepository<HabitRecord> repository;
@Override @Override
public void setUp() public void setUp()
{ {
@ -57,6 +60,8 @@ public class SQLiteHabitListTest extends BaseAndroidTest
fixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
modelFactory = component.getModelFactory(); modelFactory = component.getModelFactory();
repository =
new SQLiteRepository<>(HabitRecord.class, Cache.openDatabase());
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
@ -68,8 +73,10 @@ public class SQLiteHabitListTest extends BaseAndroidTest
HabitRecord record = new HabitRecord(); HabitRecord record = new HabitRecord();
record.copyFrom(h); record.copyFrom(h);
record.position = i; record.position = i;
record.save(i); repository.save(record);
} }
habitList.reload();
} }
@Test @Test
@ -91,7 +98,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
habitList.add(habit); habitList.add(habit);
assertThat(habit.getId(), equalTo(12300L)); assertThat(habit.getId(), equalTo(12300L));
HabitRecord record = getRecord(12300L); HabitRecord record = repository.find(12300L);
assertNotNull(record); assertNotNull(record);
assertThat(record.name, equalTo(habit.getName())); assertThat(record.name, equalTo(habit.getName()));
} }
@ -106,7 +113,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
habitList.add(habit); habitList.add(habit);
assertNotNull(habit.getId()); assertNotNull(habit.getId());
HabitRecord record = getRecord(habit.getId()); HabitRecord record = repository.find(habit.getId());
assertNotNull(record); assertNotNull(record);
assertThat(record.name, equalTo(habit.getName())); assertThat(record.name, equalTo(habit.getName()));
} }
@ -120,7 +127,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
@Test @Test
public void testGetAll_withArchived() public void testGetAll_withArchived()
{ {
List<Habit> habits = habitList.toList(); List<Habit> habits = Lists.newArrayList(habitList.iterator());
assertThat(habits.size(), equalTo(10)); assertThat(habits.size(), equalTo(10));
assertThat(habits.get(3).getName(), equalTo("habit 3")); assertThat(habits.get(3).getName(), equalTo("habit 3"));
} }
@ -166,12 +173,4 @@ public class SQLiteHabitListTest extends BaseAndroidTest
h2.setId(1000L); h2.setId(1000L);
assertThat(habitList.indexOf(h2), equalTo(-1)); assertThat(habitList.indexOf(h2), equalTo(-1));
} }
private HabitRecord getRecord(long id)
{
return new Select()
.from(HabitRecord.class)
.where("id = ?", id)
.executeSingle();
}
} }

@ -23,8 +23,9 @@ import android.support.annotation.*;
import android.support.test.runner.*; import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*; import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*; import com.activeandroid.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
@ -35,7 +36,7 @@ import org.junit.runner.*;
import java.util.*; import java.util.*;
import static android.support.test.espresso.matcher.ViewMatchers.assertThat; import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.*;
import static org.isoron.uhabits.core.models.Checkmark.CHECKED_EXPLICITLY; import static org.isoron.uhabits.core.models.Checkmark.CHECKED_EXPLICITLY;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -50,6 +51,8 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
private long day; private long day;
private SQLiteRepository<RepetitionRecord> sqlite;
@Override @Override
public void setUp() public void setUp()
{ {
@ -59,6 +62,8 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
repetitions = habit.getRepetitions(); repetitions = habit.getRepetitions();
today = DateUtils.getStartOfToday(); today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay; day = DateUtils.millisecondsInOneDay;
sqlite = new SQLiteRepository<>(RepetitionRecord.class,
Cache.openDatabase());
} }
@Test @Test
@ -130,15 +135,13 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
@Nullable @Nullable
private RepetitionRecord getByTimestamp(long timestamp) private RepetitionRecord getByTimestamp(long timestamp)
{ {
return selectByTimestamp(timestamp).executeSingle(); String query = "where habit = ? and timestamp = ?";
}
@NonNull String params[] = {
private From selectByTimestamp(long timestamp) Long.toString(habit.getId()),
{ Long.toString(timestamp)
return new Select() };
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId()) return sqlite.findFirst(query, params);
.and("timestamp = ?", timestamp);
} }
} }

@ -39,9 +39,9 @@ public class SQLModelFactory implements ModelFactory
@Provides @Provides
@AppScope @AppScope
public static HabitList provideHabitList() public static HabitList provideHabitList(ModelFactory modelFactory)
{ {
return SQLiteHabitList.getInstance(provideModelFactory()); return new SQLiteHabitList(modelFactory);
} }
@Override @Override

@ -19,55 +19,61 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.database.sqlite.*;
import android.support.annotation.*; import android.support.annotation.*;
import com.activeandroid.query.*; import com.activeandroid.*;
import com.activeandroid.util.*;
import org.apache.commons.lang3.*; import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.memory.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
import java.util.*; import java.util.*;
import static org.isoron.uhabits.utils.DatabaseUtils.executeAsTransaction;
/** /**
* Implementation of a {@link HabitList} that is backed by SQLite. * Implementation of a {@link HabitList} that is backed by SQLite.
*/ */
public class SQLiteHabitList extends HabitList public class SQLiteHabitList extends HabitList
{ {
private static HashMap<Long, Habit> cache;
private static SQLiteHabitList instance; private static SQLiteHabitList instance;
@NonNull @NonNull
private final SQLiteUtils<HabitRecord> sqlite; private final SQLiteRepository<HabitRecord> repository;
@NonNull @NonNull
private final ModelFactory modelFactory; private final ModelFactory modelFactory;
@NonNull @NonNull
private Order order; private final MemoryHabitList list;
private boolean loaded = false;
public SQLiteHabitList(@NonNull ModelFactory modelFactory) public SQLiteHabitList(@NonNull ModelFactory modelFactory)
{ {
super(); super();
this.modelFactory = modelFactory; this.modelFactory = modelFactory;
this.list = new MemoryHabitList();
if (cache == null) cache = new HashMap<>(); repository =
sqlite = new SQLiteUtils<>(HabitRecord.class); new SQLiteRepository<>(HabitRecord.class, Cache.openDatabase());
order = Order.BY_POSITION;
} }
protected SQLiteHabitList(@NonNull ModelFactory modelFactory, private void loadRecords()
@NonNull HabitMatcher filter,
@NonNull Order order)
{ {
super(filter); if(loaded) return;
this.modelFactory = modelFactory; loaded = true;
if (cache == null) cache = new HashMap<>(); list.removeAll();
sqlite = new SQLiteUtils<>(HabitRecord.class); List<HabitRecord> records = repository.findAll("order by position");
this.order = order; for (HabitRecord rec : records)
{
Habit h = modelFactory.buildHabit();
rec.copyTo(h);
list.add(h);
}
} }
public static SQLiteHabitList getInstance( public static SQLiteHabitList getInstance(
@ -78,127 +84,120 @@ public class SQLiteHabitList extends HabitList
} }
@Override @Override
public void add(@NonNull Habit habit) public synchronized void add(@NonNull Habit habit)
{ {
if (cache.containsValue(habit)) loadRecords();
throw new IllegalArgumentException("habit already added"); list.add(habit);
HabitRecord record = new HabitRecord(); HabitRecord record = new HabitRecord();
record.copyFrom(habit); record.copyFrom(habit);
record.position = size(); record.position = list.indexOf(habit);
repository.save(record);
Long id = habit.getId();
if (id == null) id = record.save();
else record.save(id);
if (id < 0)
throw new IllegalArgumentException("habit could not be saved");
habit.setId(id);
cache.put(id, habit);
} }
@Override @Override
@Nullable @Nullable
public Habit getById(long id) public Habit getById(long id)
{ {
if (!cache.containsKey(id)) loadRecords();
{ return list.getById(id);
HabitRecord record = HabitRecord.get(id);
if (record == null) return null;
Habit habit = modelFactory.buildHabit();
record.copyTo(habit);
cache.put(id, habit);
}
return cache.get(id);
} }
@Override @Override
@NonNull @NonNull
public Habit getByPosition(int position) public Habit getByPosition(int position)
{ {
return toList().get(position); loadRecords();
return list.getByPosition(position);
} }
@NonNull @NonNull
@Override @Override
public HabitList getFiltered(HabitMatcher filter) public HabitList getFiltered(HabitMatcher filter)
{ {
return new SQLiteHabitList(modelFactory, filter, order); loadRecords();
return list.getFiltered(filter);
} }
@Override @Override
@NonNull @NonNull
public Order getOrder() public Order getOrder()
{ {
return order; return list.getOrder();
} }
@Override @Override
public void setOrder(@NonNull Order order) public void setOrder(@NonNull Order order)
{ {
this.order = order; list.setOrder(order);
} }
@Override @Override
public int indexOf(@NonNull Habit h) public int indexOf(@NonNull Habit h)
{ {
return toList().indexOf(h); loadRecords();
return list.indexOf(h);
} }
@Override @Override
public Iterator<Habit> iterator() public Iterator<Habit> iterator()
{ {
return Collections.unmodifiableCollection(toList()).iterator(); loadRecords();
return list.iterator();
} }
public void rebuildOrder() private void rebuildOrder()
{ {
List<Habit> habits = toList(); // List<Habit> habits = toList();
//
int i = 0; // int i = 0;
for (Habit h : habits) // for (Habit h : habits)
{ // {
HabitRecord record = HabitRecord.get(h.getId()); // HabitRecord record = repository.find(h.getId());
if (record == null) // if (record == null)
throw new RuntimeException("habit not in database"); // throw new RuntimeException("habit not in database");
//
record.position = i++; // record.position = i++;
record.save(); // repository.save(record);
} // }
//
update(habits); // update(habits);
} }
@Override @Override
public void remove(@NonNull Habit habit) public synchronized void remove(@NonNull Habit habit)
{ {
if (!cache.containsKey(habit.getId())) loadRecords();
throw new RuntimeException("habit not in cache"); list.remove(habit);
cache.remove(habit.getId()); HabitRecord record = repository.find(habit.getId());
HabitRecord record = HabitRecord.get(habit.getId());
if (record == null) throw new RuntimeException("habit not in database"); if (record == null) throw new RuntimeException("habit not in database");
record.cascadeDelete(); executeAsTransaction(() ->
{
((SQLiteRepetitionList) habit.getRepetitions()).removeAll();
repository.remove(record);
});
rebuildOrder(); rebuildOrder();
} }
@Override @Override
public void removeAll() public synchronized void removeAll()
{ {
sqlite.query("delete from repetitions", null); loadRecords();
sqlite.query("delete from habits", null); list.removeAll();
SQLiteDatabase db = Cache.openDatabase();
db.execSQL("delete from habits");
db.execSQL("delete from repetitions");
} }
@Override @Override
public synchronized void reorder(Habit from, Habit to) public synchronized void reorder(Habit from, Habit to)
{ {
if (from == to) return; loadRecords();
list.reorder(from, to);
HabitRecord fromRecord = HabitRecord.get(from.getId()); HabitRecord fromRecord = repository.find(from.getId());
HabitRecord toRecord = HabitRecord.get(to.getId()); HabitRecord toRecord = repository.find(to.getId());
if (fromRecord == null) if (fromRecord == null)
throw new RuntimeException("habit not in database"); throw new RuntimeException("habit not in database");
@ -207,27 +206,22 @@ public class SQLiteHabitList extends HabitList
Integer fromPos = fromRecord.position; Integer fromPos = fromRecord.position;
Integer toPos = toRecord.position; Integer toPos = toRecord.position;
SQLiteDatabase db = Cache.openDatabase();
Log.d("SQLiteHabitList",
String.format("reorder: %d %d", fromPos, toPos));
if (toPos < fromPos) if (toPos < fromPos)
{ {
new Update(HabitRecord.class) db.execSQL("update habits set position = position + 1 " +
.set("position = position + 1") "where position >= ? and position < ?",
.where("position >= ? and position < ?", toPos, fromPos) new String[]{ toPos.toString(), fromPos.toString() });
.execute();
} }
else else
{ {
new Update(HabitRecord.class) db.execSQL("update habits set position = position - 1 " +
.set("position = position - 1") "where position > ? and position <= ?",
.where("position > ? and position <= ?", fromPos, toPos) new String[]{ fromPos.toString(), toPos.toString() });
.execute();
} }
fromRecord.position = toPos; fromRecord.position = toPos;
fromRecord.save(); repository.save(fromRecord);
update(from); update(from);
getObservable().notifyListeners(); getObservable().notifyListeners();
} }
@ -235,100 +229,33 @@ public class SQLiteHabitList extends HabitList
@Override @Override
public void repair() public void repair()
{ {
super.repair(); loadRecords();
rebuildOrder(); rebuildOrder();
} }
@Override @Override
public int size() public int size()
{ {
return toList().size(); loadRecords();
return list.size();
} }
@Override @Override
public void update(List<Habit> habits) public synchronized void update(List<Habit> habits)
{ {
loadRecords();
for (Habit h : habits) for (Habit h : habits)
{ {
HabitRecord record = HabitRecord.get(h.getId()); HabitRecord record = repository.find(h.getId());
if (record == null) if (record == null)
throw new RuntimeException("habit not in database"); throw new RuntimeException("habit not in database");
record.copyFrom(h); record.copyFrom(h);
record.save(); repository.save(record);
} }
} }
public synchronized List<Habit> toList() public void reload()
{
String query = buildSelectQuery();
List<HabitRecord> recordList = sqlite.query(query, null);
List<Habit> habits = new LinkedList<>();
for (HabitRecord record : recordList)
{
Habit habit = getById(record.getId());
if (habit == null) continue;
if (!filter.matches(habit)) continue;
habits.add(habit);
}
if(order == Order.BY_SCORE)
{
Collections.sort(habits, (lhs, rhs) -> {
double s1 = lhs.getScores().getTodayValue();
double s2 = rhs.getScores().getTodayValue();
return Double.compare(s2, s1);
});
}
return habits;
}
private void appendOrderBy(StringBuilder query)
{
switch (order)
{
case BY_POSITION:
query.append("order by position ");
break;
case BY_NAME:
case BY_SCORE:
query.append("order by name ");
break;
case BY_COLOR:
query.append("order by color, name ");
break;
default:
throw new IllegalStateException();
}
}
private void appendSelect(StringBuilder query)
{
query.append(HabitRecord.SELECT);
}
private void appendWhere(StringBuilder query)
{
ArrayList<Object> where = new ArrayList<>();
if (filter.isReminderRequired()) where.add("reminder_hour is not null");
if (!filter.isArchivedAllowed()) where.add("archived = 0");
if (where.isEmpty()) return;
query.append("where ");
query.append(StringUtils.join(where, " and "));
query.append(" ");
}
private String buildSelectQuery()
{ {
StringBuilder query = new StringBuilder(); loaded = false;
appendSelect(query);
appendWhere(query);
appendOrderBy(query);
return query.toString();
} }
} }

@ -19,15 +19,14 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.database.*;
import android.database.sqlite.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.*; import com.activeandroid.*;
import com.activeandroid.query.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.memory.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
@ -38,165 +37,112 @@ import java.util.*;
*/ */
public class SQLiteRepetitionList extends RepetitionList public class SQLiteRepetitionList extends RepetitionList
{ {
private final SQLiteRepository<RepetitionRecord> repository;
private final SQLiteUtils<RepetitionRecord> sqlite; private final MemoryRepetitionList list;
@Nullable private boolean loaded = false;
private HabitRecord habitRecord;
private SQLiteStatement addStatement;
public static final String ADD_QUERY =
"insert into repetitions(habit, timestamp, value) " +
"values (?,?,?)";
public SQLiteRepetitionList(@NonNull Habit habit) public SQLiteRepetitionList(@NonNull Habit habit)
{ {
super(habit); super(habit);
sqlite = new SQLiteUtils<>(RepetitionRecord.class); repository = new SQLiteRepository<>(RepetitionRecord.class,
Cache.openDatabase());
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());
SQLiteDatabase db = Cache.openDatabase(); for (RepetitionRecord rec : records)
addStatement = db.compileStatement(ADD_QUERY); list.add(rec.toRepetition());
} }
/**
* Adds a repetition to the global SQLite database.
* <p>
* Given a repetition, this creates and saves the corresponding
* RepetitionRecord to the database.
*
* @param rep the repetition to be added
*/
@Override @Override
public void add(Repetition rep) public void add(Repetition rep)
{ {
loadRecords();
list.add(rep);
check(habit.getId()); check(habit.getId());
addStatement.bindLong(1, habit.getId()); RepetitionRecord record = new RepetitionRecord();
addStatement.bindLong(2, rep.getTimestamp()); record.habit_id = habit.getId();
addStatement.bindLong(3, rep.getValue()); record.copyFrom(rep);
addStatement.execute(); repository.save(record);
observable.notifyListeners(); observable.notifyListeners();
} }
@Override @Override
public List<Repetition> getByInterval(long timeFrom, long timeTo) public List<Repetition> getByInterval(long timeFrom, long timeTo)
{ {
check(habit.getId()); loadRecords();
String query = "select habit, timestamp, value " + return list.getByInterval(timeFrom, timeTo);
"from Repetitions " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"order by timestamp";
String params[] = {
Long.toString(habit.getId()),
Long.toString(timeFrom),
Long.toString(timeTo)
};
List<RepetitionRecord> records = sqlite.query(query, params);
return toRepetitions(records);
} }
@Override @Override
@Nullable @Nullable
public Repetition getByTimestamp(long timestamp) public Repetition getByTimestamp(long timestamp)
{ {
check(habit.getId()); loadRecords();
String query = "select habit, timestamp, value " + return list.getByTimestamp(timestamp);
"from Repetitions " +
"where habit = ? and timestamp = ? " +
"limit 1";
String params[] =
{ Long.toString(habit.getId()), Long.toString(timestamp) };
RepetitionRecord record = sqlite.querySingle(query, params);
if (record == null) return null;
record.habit = habitRecord;
return record.toRepetition();
} }
@Override @Override
public Repetition getOldest() public Repetition getOldest()
{ {
check(habit.getId()); loadRecords();
String query = "select habit, timestamp, value " + return list.getOldest();
"from Repetitions " +
"where habit = ? " +
"order by timestamp asc " +
"limit 1";
String params[] = { Long.toString(habit.getId()) };
RepetitionRecord record = sqlite.querySingle(query, params);
if (record == null) return null;
record.habit = habitRecord;
return record.toRepetition();
} }
@Override @Override
public Repetition getNewest() public Repetition getNewest()
{ {
check(habit.getId()); loadRecords();
String query = "select habit, timestamp, value " + return list.getNewest();
"from Repetitions " +
"where habit = ? " +
"order by timestamp desc " +
"limit 1";
String params[] = { Long.toString(habit.getId()) };
RepetitionRecord record = sqlite.querySingle(query, params);
if (record == null) return null;
record.habit = habitRecord;
return record.toRepetition();
} }
@Override @Override
public void remove(@NonNull Repetition repetition) public void remove(@NonNull Repetition repetition)
{ {
new Delete() loadRecords();
.from(RepetitionRecord.class) list.remove(repetition);
.where("habit = ?", habit.getId()) check(habit.getId());
.and("timestamp = ?", repetition.getTimestamp()) repository.execSQL(
.execute(); "delete from repetitions where habit = ? and timestamp = ?",
habit.getId());
observable.notifyListeners(); observable.notifyListeners();
} }
@Contract("null -> fail") public void removeAll()
private void check(Long id)
{
if (id == null) throw new RuntimeException("habit is not saved");
if (habitRecord != null) return;
habitRecord = HabitRecord.get(id);
if (habitRecord == null) throw new RuntimeException("habit not found");
}
@NonNull
private List<Repetition> toRepetitions(
@NonNull List<RepetitionRecord> records)
{ {
loadRecords();
list.removeAll();
check(habit.getId()); check(habit.getId());
repository.execSQL("delete from repetitions where habit = ?",
habit.getId());
}
List<Repetition> reps = new LinkedList<>(); @Override
for (RepetitionRecord record : records) public long getTotalCount()
{ {
record.habit = habitRecord; loadRecords();
reps.add(record.toRepetition()); return list.getTotalCount();
} }
return reps; public void reload()
{
loaded = false;
} }
@Override @Contract("null -> fail")
public long getTotalCount() private void check(Long value)
{ {
SQLiteDatabase db = Cache.openDatabase(); if (value == null) throw new RuntimeException("null check failed");
return DatabaseUtils.queryNumEntries(db, "Repetitions",
"habit=?", new String[] { Long.toString(habit.getId()) });
} }
} }

@ -1,84 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.database.*;
import android.database.sqlite.*;
import android.support.annotation.*;
import com.activeandroid.*;
import org.isoron.uhabits.models.sqlite.records.*;
import java.util.*;
public class SQLiteUtils<T extends SQLiteRecord>
{
private Class klass;
public SQLiteUtils(Class klass)
{
this.klass = klass;
}
@NonNull
public List<T> query(String query, String params[])
{
SQLiteDatabase db = Cache.openDatabase();
try (Cursor c = db.rawQuery(query, params))
{
return cursorToMultipleRecords(c);
}
}
@Nullable
public T querySingle(String query, String params[])
{
SQLiteDatabase db = Cache.openDatabase();
try(Cursor c = db.rawQuery(query, params))
{
if (!c.moveToNext()) return null;
return cursorToSingleRecord(c);
}
}
@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 c)
{
try
{
T record = (T) klass.newInstance();
record.copyFrom(c);
return record;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}

@ -19,122 +19,85 @@
package org.isoron.uhabits.models.sqlite.records; package org.isoron.uhabits.models.sqlite.records;
import android.annotation.*;
import android.database.*;
import android.support.annotation.*;
import com.activeandroid.*; import com.activeandroid.*;
import com.activeandroid.annotation.*;
import com.activeandroid.query.*;
import com.activeandroid.util.*;
import org.apache.commons.lang3.builder.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.utils.DatabaseUtils;
import java.lang.reflect.*;
/** /**
* The SQLite database record corresponding to a {@link Habit}. * The SQLite database record corresponding to a {@link Habit}.
*/ */
@Table(name = "Habits") @Table(name = "habits")
public class HabitRecord extends Model implements SQLiteRecord @com.activeandroid.annotation.Table(name = "Habits")
public class HabitRecord extends Model
{ {
public static String SELECT = @Column
"select id, color, description, freq_den, freq_num, " + @com.activeandroid.annotation.Column
"name, position, reminder_hour, reminder_min, " + public String description;
"highlight, archived, reminder_days, type, target_type, " +
"target_value, unit from habits ";
@Column(name = "name") @Column
@com.activeandroid.annotation.Column
public String name; public String name;
@Column(name = "description")
public String description;
@Column(name = "freq_num") @Column(name = "freq_num")
public int freqNum; @com.activeandroid.annotation.Column(name = "freq_num")
public Integer freqNum;
@Column(name = "freq_den") @Column(name = "freq_den")
public int freqDen; @com.activeandroid.annotation.Column(name = "freq_den")
public Integer freqDen;
@Column(name = "color") @Column
public int color; @com.activeandroid.annotation.Column
public Integer color;
@Column(name = "position") @Column
public int position; @com.activeandroid.annotation.Column
public Integer position;
@Nullable
@Column(name = "reminder_hour") @Column(name = "reminder_hour")
@com.activeandroid.annotation.Column(name = "reminder_hour")
public Integer reminderHour; public Integer reminderHour;
@Nullable
@Column(name = "reminder_min") @Column(name = "reminder_min")
@com.activeandroid.annotation.Column(name = "reminder_min")
public Integer reminderMin; public Integer reminderMin;
@Column(name = "reminder_days") @Column(name = "reminder_days")
public int reminderDays; @com.activeandroid.annotation.Column(name = "reminder_days")
public Integer reminderDays;
@Column(name = "highlight") @Column
public int highlight; @com.activeandroid.annotation.Column
public Integer highlight;
@Column(name = "archived") @Column
public int archived; @com.activeandroid.annotation.Column
public Integer archived;
@Column(name = "type") @Column
public int type; @com.activeandroid.annotation.Column
public Integer type;
@Column(name = "target_value") @Column(name = "target_value")
public double targetValue; @com.activeandroid.annotation.Column(name = "target_value")
public Double targetValue;
@Column(name = "target_type") @Column(name = "target_type")
public int targetType; @com.activeandroid.annotation.Column(name = "target_type")
public Integer targetType;
@Column(name = "unit") @Column
@com.activeandroid.annotation.Column
public String unit; public String unit;
public HabitRecord() @Column
{ public Long id;
}
@Nullable
public static HabitRecord get(long id)
{
return HabitRecord.load(HabitRecord.class, id);
}
/**
* Changes the id of a habit on the database.
*
* @param oldId the original id
* @param newId the new id
*/
@SuppressLint("DefaultLocale")
public static void updateId(long oldId, long newId)
{
SQLiteUtils.execSql(
String.format("update Habits set Id = %d where Id = %d", newId,
oldId));
}
/**
* Deletes the habit and all data associated to it, including checkmarks,
* repetitions and scores.
*/
public void cascadeDelete()
{
Long id = getId();
DatabaseUtils.executeAsTransaction(() -> {
new Delete()
.from(RepetitionRecord.class)
.where("habit = ?", id)
.execute();
delete();
});
}
public void copyFrom(Habit model) public void copyFrom(Habit model)
{ {
this.id = model.getId();
this.name = model.getName(); this.name = model.getName();
this.description = model.getDescription(); this.description = model.getDescription();
this.highlight = 0; this.highlight = 0;
@ -161,35 +124,14 @@ public class HabitRecord extends Model implements SQLiteRecord
} }
} }
@Override
public void copyFrom(Cursor c)
{
setId(c.getLong(0));
color = c.getInt(1);
description = c.getString(2);
freqDen = c.getInt(3);
freqNum = c.getInt(4);
name = c.getString(5);
position = c.getInt(6);
reminderHour = c.getInt(7);
reminderMin = c.getInt(8);
highlight = c.getInt(9);
archived = c.getInt(10);
reminderDays = c.getInt(11);
type = c.getInt(12);
targetType = c.getInt(13);
targetValue = c.getDouble(14);
unit = c.getString(15);
}
public void copyTo(Habit habit) public void copyTo(Habit habit)
{ {
habit.setId(this.id);
habit.setName(this.name); habit.setName(this.name);
habit.setDescription(this.description); habit.setDescription(this.description);
habit.setFrequency(new Frequency(this.freqNum, this.freqDen)); habit.setFrequency(new Frequency(this.freqNum, this.freqDen));
habit.setColor(this.color); habit.setColor(this.color);
habit.setArchived(this.archived != 0); habit.setArchived(this.archived != 0);
habit.setId(this.getId());
habit.setType(this.type); habit.setType(this.type);
habit.setTargetType(this.targetType); habit.setTargetType(this.targetType);
habit.setTargetValue(this.targetValue); habit.setTargetValue(this.targetValue);
@ -202,28 +144,77 @@ public class HabitRecord extends Model implements SQLiteRecord
} }
} }
/** @Override
* Saves the habit on the database, and assigns the specified id to it. public boolean equals(Object o)
*
* @param id the id that the habit should receive
*/
public void save(long id)
{ {
save(); if (this == o) return true;
updateId(getId(), id);
if (o == null || getClass() != o.getClass()) return false;
HabitRecord that = (HabitRecord) o;
return new EqualsBuilder()
.appendSuper(super.equals(o))
.append(freqNum, that.freqNum)
.append(freqDen, that.freqDen)
.append(color, that.color)
.append(position, that.position)
.append(reminderDays, that.reminderDays)
.append(highlight, that.highlight)
.append(archived, that.archived)
.append(type, that.type)
.append(targetValue, that.targetValue)
.append(targetType, that.targetType)
.append(name, that.name)
.append(description, that.description)
.append(reminderHour, that.reminderHour)
.append(reminderMin, that.reminderMin)
.append(unit, that.unit)
.isEquals();
} }
private void setId(Long id) @Override
{ public int hashCode()
try
{ {
Field f = (Model.class).getDeclaredField("mId"); return new HashCodeBuilder(17, 37)
f.setAccessible(true); .appendSuper(super.hashCode())
f.set(this, id); .append(name)
.append(description)
.append(freqNum)
.append(freqDen)
.append(color)
.append(position)
.append(reminderHour)
.append(reminderMin)
.append(reminderDays)
.append(highlight)
.append(archived)
.append(type)
.append(targetValue)
.append(targetType)
.append(unit)
.toHashCode();
} }
catch (Exception e)
@Override
public String toString()
{ {
throw new RuntimeException(e); return new ToStringBuilder(this)
} .append("name", name)
.append("description", description)
.append("freqNum", freqNum)
.append("freqDen", freqDen)
.append("color", color)
.append("position", position)
.append("reminderHour", reminderHour)
.append("reminderMin", reminderMin)
.append("reminderDays", reminderDays)
.append("highlight", highlight)
.append("archived", archived)
.append("type", type)
.append("targetValue", targetValue)
.append("targetType", targetType)
.append("unit", unit)
.toString();
} }
} }

@ -19,32 +19,34 @@
package org.isoron.uhabits.models.sqlite.records; package org.isoron.uhabits.models.sqlite.records;
import android.database.*;
import com.activeandroid.*; import com.activeandroid.*;
import com.activeandroid.annotation.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
/** /**
* The SQLite database record corresponding to a {@link Repetition}. * The SQLite database record corresponding to a {@link Repetition}.
*/ */
@Table(name = "Repetitions") @Table(name = "Repetitions")
public class RepetitionRecord extends Model implements SQLiteRecord @com.activeandroid.annotation.Table(name = "Repetitions")
public class RepetitionRecord extends Model
{ {
@Column(name = "habit") @com.activeandroid.annotation.Column(name = "habit")
public HabitRecord habit; public HabitRecord habit;
@Column(name = "timestamp") @Column(name = "habit")
public Long habit_id;
@Column
@com.activeandroid.annotation.Column(name = "timestamp")
public Long timestamp; public Long timestamp;
@Column(name = "value") @Column
public int value; @com.activeandroid.annotation.Column(name = "value")
public Integer value;
public static RepetitionRecord get(Long id) @Column
{ public Long id;
return RepetitionRecord.load(RepetitionRecord.class, id);
}
public void copyFrom(Repetition repetition) public void copyFrom(Repetition repetition)
{ {
@ -52,13 +54,6 @@ public class RepetitionRecord extends Model implements SQLiteRecord
value = repetition.getValue(); value = repetition.getValue();
} }
@Override
public void copyFrom(Cursor c)
{
timestamp = c.getLong(1);
value = c.getInt(2);
}
public Repetition toRepetition() public Repetition toRepetition()
{ {
return new Repetition(timestamp, value); return new Repetition(timestamp, value);

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

@ -221,4 +221,6 @@ public abstract class RepetitionList
add(new Repetition(timestamp, value)); add(new Repetition(timestamp, value));
habit.invalidateNewerThan(timestamp); habit.invalidateNewerThan(timestamp);
} }
public abstract void removeAll();
} }

@ -22,6 +22,7 @@ package org.isoron.uhabits.core.models.memory;
import android.support.annotation.*; import android.support.annotation.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*; import java.util.*;
@ -60,8 +61,10 @@ public class MemoryCheckmarkList extends CheckmarkList
Checkmark oldest = getOldestComputed(); Checkmark oldest = getOldestComputed();
if(newest != null) newestTimestamp = newest.getTimestamp(); if(newest != null) newestTimestamp = newest.getTimestamp();
if(oldest != null) oldestTimestamp = oldest.getTimestamp(); if(oldest != null) oldestTimestamp = oldest.getTimestamp();
long days = (newestTimestamp - oldestTimestamp) /
DateUtils.millisecondsInOneDay;
List<Checkmark> filtered = new LinkedList<>(); List<Checkmark> filtered = new ArrayList<>((int) days);
for(long time = toTimestamp; time >= fromTimestamp; time -= millisecondsInOneDay) for(long time = toTimestamp; time >= fromTimestamp; time -= millisecondsInOneDay)
{ {
if(time > newestTimestamp || time < oldestTimestamp) if(time > newestTimestamp || time < oldestTimestamp)

@ -25,7 +25,10 @@ import org.isoron.uhabits.core.models.*;
import java.util.*; import java.util.*;
import static org.isoron.uhabits.core.models.HabitList.Order.*; import static org.isoron.uhabits.core.models.HabitList.Order.BY_COLOR;
import static org.isoron.uhabits.core.models.HabitList.Order.BY_NAME;
import static org.isoron.uhabits.core.models.HabitList.Order.BY_POSITION;
import static org.isoron.uhabits.core.models.HabitList.Order.BY_SCORE;
/** /**
* In-memory implementation of {@link HabitList}. * In-memory implementation of {@link HabitList}.
@ -55,7 +58,8 @@ public class MemoryHabitList extends HabitList
} }
@Override @Override
public void add(@NonNull Habit habit) throws IllegalArgumentException public synchronized void add(@NonNull Habit habit)
throws IllegalArgumentException
{ {
if (list.contains(habit)) if (list.contains(habit))
throw new IllegalArgumentException("habit already added"); throw new IllegalArgumentException("habit already added");
@ -70,7 +74,7 @@ public class MemoryHabitList extends HabitList
} }
@Override @Override
public Habit getById(long id) public synchronized Habit getById(long id)
{ {
for (Habit h : list) for (Habit h : list)
{ {
@ -82,14 +86,14 @@ public class MemoryHabitList extends HabitList
@NonNull @NonNull
@Override @Override
public Habit getByPosition(int position) public synchronized Habit getByPosition(int position)
{ {
return list.get(position); return list.get(position);
} }
@NonNull @NonNull
@Override @Override
public HabitList getFiltered(HabitMatcher matcher) public synchronized HabitList getFiltered(HabitMatcher matcher)
{ {
MemoryHabitList habits = new MemoryHabitList(matcher); MemoryHabitList habits = new MemoryHabitList(matcher);
habits.comparator = comparator; habits.comparator = comparator;
@ -98,11 +102,19 @@ public class MemoryHabitList extends HabitList
} }
@Override @Override
public Order getOrder() public synchronized Order getOrder()
{ {
return order; return order;
} }
@Override
public synchronized void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
}
@Override @Override
public int indexOf(@NonNull Habit h) public int indexOf(@NonNull Habit h)
{ {
@ -116,27 +128,19 @@ public class MemoryHabitList extends HabitList
} }
@Override @Override
public void remove(@NonNull Habit habit) public synchronized void remove(@NonNull Habit habit)
{ {
list.remove(habit); list.remove(habit);
} }
@Override @Override
public void reorder(Habit from, Habit to) public synchronized void reorder(Habit from, Habit to)
{ {
int toPos = indexOf(to); int toPos = indexOf(to);
list.remove(from); list.remove(from);
list.add(toPos, from); list.add(toPos, from);
} }
@Override
public void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
}
@Override @Override
public int size() public int size()
{ {
@ -154,14 +158,16 @@ public class MemoryHabitList extends HabitList
Comparator<Habit> nameComparator = Comparator<Habit> nameComparator =
(h1, h2) -> h1.getName().compareTo(h2.getName()); (h1, h2) -> h1.getName().compareTo(h2.getName());
Comparator<Habit> colorComparator = (h1, h2) -> { Comparator<Habit> colorComparator = (h1, h2) ->
{
Integer c1 = h1.getColor(); Integer c1 = h1.getColor();
Integer c2 = h2.getColor(); Integer c2 = h2.getColor();
if (c1.equals(c2)) return nameComparator.compare(h1, h2); if (c1.equals(c2)) return nameComparator.compare(h1, h2);
else return c1.compareTo(c2); else return c1.compareTo(c2);
}; };
Comparator<Habit> scoreComparator = (h1, h2) -> { Comparator<Habit> scoreComparator = (h1, h2) ->
{
double s1 = h1.getScores().getTodayValue(); double s1 = h1.getScores().getTodayValue();
double s2 = h2.getScores().getTodayValue(); double s2 = h2.getScores().getTodayValue();
return Double.compare(s2, s1); return Double.compare(s2, s1);
@ -174,7 +180,7 @@ public class MemoryHabitList extends HabitList
throw new IllegalStateException(); throw new IllegalStateException();
} }
private void resort() private synchronized void resort()
{ {
if (comparator != null) Collections.sort(list, comparator); if (comparator != null) Collections.sort(list, comparator);
} }

@ -30,12 +30,12 @@ import java.util.*;
*/ */
public class MemoryRepetitionList extends RepetitionList public class MemoryRepetitionList extends RepetitionList
{ {
LinkedList<Repetition> list; ArrayList<Repetition> list;
public MemoryRepetitionList(Habit habit) public MemoryRepetitionList(Habit habit)
{ {
super(habit); super(habit);
list = new LinkedList<>(); list = new ArrayList<>();
} }
@Override @Override
@ -48,7 +48,7 @@ public class MemoryRepetitionList extends RepetitionList
@Override @Override
public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp) public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp)
{ {
LinkedList<Repetition> filtered = new LinkedList<>(); ArrayList<Repetition> filtered = new ArrayList<>();
for (Repetition r : list) for (Repetition r : list)
{ {
@ -57,7 +57,7 @@ public class MemoryRepetitionList extends RepetitionList
} }
Collections.sort(filtered, Collections.sort(filtered,
(r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp())); (r1, r2) -> Long.compare(r1.getTimestamp(), r2.getTimestamp()));
return filtered; return filtered;
} }
@ -122,4 +122,10 @@ public class MemoryRepetitionList extends RepetitionList
{ {
return list.size(); return list.size();
} }
@Override
public void removeAll()
{
list.clear();
}
} }

Loading…
Cancel
Save