Merge branch 'feature/raw-sqlite' into dev

pull/312/head
Alinson S. Xavier 8 years ago
commit 96c1a046d4

@ -1,6 +1,5 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
@ -32,6 +31,7 @@ dependencies {
implementation 'com.google.dagger:dagger:2.9'
implementation 'com.android.support:design:25.3.1'
implementation 'com.android.support:appcompat-v7:25.3.1'
implementation 'org.apache.commons:commons-lang3:3.5'
annotationProcessor 'com.google.dagger:dagger-compiler:2.9'
androidTestAnnotationProcessor 'com.google.dagger:dagger-compiler:2.9'

@ -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.androidbase.storage;
import android.content.*;
import android.database.sqlite.*;
import org.isoron.androidbase.*;
import java.io.*;
import java.util.*;
public class BaseSQLiteOpenHelper extends SQLiteOpenHelper
{
private final Context context;
private final int version;
public BaseSQLiteOpenHelper(@AppContext Context context,
String databaseFilename,
int version)
{
super(context, databaseFilename, null, version);
this.context = context;
this.version = version;
}
@Override
public void onCreate(SQLiteDatabase db)
{
executeMigrations(db, -1, version);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
executeMigrations(db, oldVersion, newVersion);
}
private void executeMigrations(SQLiteDatabase db,
int oldVersion,
int newVersion)
{
try
{
for (int v = oldVersion + 1; v <= newVersion; v++)
{
String fname = String.format(Locale.US, "migrations/%d.sql", v);
InputStream stream = context.getAssets().open(fname);
for (String command : SQLParser.parse(stream))
db.execSQL(command);
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
throw new UnsupportedDatabaseVersionException();
}
}

@ -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.androidbase.storage;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
public @interface Column
{
String name() default "";
}

@ -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.androidbase.storage;
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 == ' ';
}
}

@ -0,0 +1,282 @@
/*
* 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.androidbase.storage;
import android.content.*;
import android.database.*;
import android.database.sqlite.*;
import android.support.annotation.*;
import android.util.*;
import org.apache.commons.lang3.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class SQLiteRepository<T>
{
@NonNull
private final Class klass;
@NonNull
private final SQLiteDatabase db;
public SQLiteRepository(@NonNull Class<T> klass, @NonNull SQLiteDatabase db)
{
this.klass = klass;
this.db = db;
}
@Nullable
public T find(@NonNull Long id)
{
return findFirst(String.format("where %s=?", getIdName()),
id.toString());
}
@NonNull
public List<T> findAll(String query, String... params)
{
try (Cursor c = db.rawQuery(buildSelectQuery() + query, params))
{
return cursorToMultipleRecords(c);
}
}
@Nullable
public T findFirst(String query, String... params)
{
try (Cursor c = db.rawQuery(buildSelectQuery() + query, params))
{
if (!c.moveToNext()) return null;
return cursorToSingleRecord(c);
}
}
public void execSQL(String query, Object... params)
{
db.execSQL(query, params);
}
public void save(T record)
{
try
{
Field fields[] = getFields();
String columns[] = getColumnNames();
ContentValues values = new ContentValues();
for (int i = 0; i < fields.length; i++)
fieldToContentValue(values, columns[i], fields[i], record);
Long id = (Long) getIdField().get(record);
int affectedRows = 0;
if (id != null) affectedRows =
db.update(getTableName(), values, getIdName() + "=?",
new String[]{ id.toString() });
if (id == null || affectedRows == 0)
{
id = db.insertOrThrow(getTableName(), null, values);
getIdField().set(record, id);
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
public void remove(T record)
{
try
{
Long id = (Long) getIdField().get(record);
if (id == null) return;
db.delete(getTableName(), getIdName() + "=?",
new String[]{ 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 void fieldToContentValue(ContentValues values,
String columnName,
Field field,
T record)
{
try
{
if (field.getType().isAssignableFrom(Integer.class))
values.put(columnName, (Integer) field.get(record));
else if (field.getType().isAssignableFrom(Long.class))
values.put(columnName, (Long) field.get(record));
else if (field.getType().isAssignableFrom(Double.class))
values.put(columnName, (Double) field.get(record));
else if (field.getType().isAssignableFrom(String.class))
values.put(columnName, (String) field.get(record));
else throw new RuntimeException(
"Type not supported: " + field.getName());
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
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 Pair<>(field, column));
}
return fields;
}
@NonNull
private Field[] getFields()
{
List<Field> fields = new ArrayList<>();
List<Pair<Field, Column>> columns = getFieldColumnPairs();
for (Pair<Field, Column> pair : columns) fields.add(pair.first);
return fields.toArray(new Field[]{});
}
@NonNull
private String[] getColumnNames()
{
List<String> names = new ArrayList<>();
List<Pair<Field, Column>> columns = getFieldColumnPairs();
for (Pair<Field, Column> pair : columns)
{
String cname = pair.second.name();
if (cname.isEmpty()) cname = pair.first.getName();
if (names.contains(cname))
throw new RuntimeException("duplicated column : " + cname);
names.add(cname);
}
return names.toArray(new String[]{});
}
@NonNull
private String getTableName()
{
String name = getTableAnnotation().name();
if (name.isEmpty()) throw new RuntimeException("Table name is empty");
return name;
}
@NonNull
private String getIdName()
{
String id = getTableAnnotation().id();
if (id.isEmpty()) throw new RuntimeException("Table id is empty");
return id;
}
@NonNull
private Field getIdField()
{
Field fields[] = getFields();
String idName = getIdName();
for (Field f : fields)
if (f.getName().equals(idName)) return f;
throw new RuntimeException("Field not found: " + idName);
}
@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;
}
}

@ -0,0 +1,32 @@
/*
* 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.androidbase.storage;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table
{
String name();
String id() default "id";
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
@ -15,13 +15,12 @@
*
* 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.*;
package org.isoron.androidbase.storage;
public interface SQLiteRecord
public class UnsupportedDatabaseVersionException extends RuntimeException
{
void copyFrom(Cursor c);
}

@ -5,7 +5,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-alpha3'
classpath 'com.android.tools.build:gradle:3.0.0-alpha4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-4.0-20170417000025+0000-all.zip
distributionUrl=http\://services.gradle.org/distributions/gradle-4.0-all.zip

@ -64,7 +64,6 @@ dependencies {
implementation 'com.github.paolorotolo:appintro:3.4.0'
implementation 'com.google.dagger:dagger:2.9'
implementation 'com.jakewharton:butterknife:8.6.1-SNAPSHOT'
implementation 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
implementation 'org.apmem.tools:layouts:1.10'
implementation 'org.jetbrains:annotations-java5:15.0'
implementation 'com.google.code.gson:gson:2.7'

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

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

@ -22,12 +22,13 @@ package org.isoron.uhabits.models.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import com.google.common.collect.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.rules.*;
import org.junit.runner.*;
@ -36,6 +37,8 @@ import java.util.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@ -49,6 +52,10 @@ public class SQLiteHabitListTest extends BaseAndroidTest
private ModelFactory modelFactory;
private SQLiteRepository<HabitRecord> repository;
private ModelObservable.Listener listener;
@Override
public void setUp()
{
@ -57,6 +64,9 @@ public class SQLiteHabitListTest extends BaseAndroidTest
fixtures.purgeHabits(habitList);
modelFactory = component.getModelFactory();
repository =
new SQLiteRepository<>(HabitRecord.class,
DatabaseUtils.openDatabase());
for (int i = 0; i < 10; i++)
{
@ -68,8 +78,20 @@ public class SQLiteHabitListTest extends BaseAndroidTest
HabitRecord record = new HabitRecord();
record.copyFrom(h);
record.position = i;
record.save(i);
repository.save(record);
}
habitList.reload();
listener = mock(ModelObservable.Listener.class);
habitList.getObservable().addListener(listener);
}
@Override
protected void tearDown() throws Exception
{
habitList.getObservable().removeListener(listener);
super.tearDown();
}
@Test
@ -77,6 +99,8 @@ public class SQLiteHabitListTest extends BaseAndroidTest
{
Habit habit = modelFactory.buildHabit();
habitList.add(habit);
verify(listener).onModelChange();
exception.expect(IllegalArgumentException.class);
habitList.add(habit);
}
@ -91,7 +115,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
habitList.add(habit);
assertThat(habit.getId(), equalTo(12300L));
HabitRecord record = getRecord(12300L);
HabitRecord record = repository.find(12300L);
assertNotNull(record);
assertThat(record.name, equalTo(habit.getName()));
}
@ -106,7 +130,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
habitList.add(habit);
assertNotNull(habit.getId());
HabitRecord record = getRecord(habit.getId());
HabitRecord record = repository.find(habit.getId());
assertNotNull(record);
assertThat(record.name, equalTo(habit.getName()));
}
@ -120,7 +144,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
@Test
public void testGetAll_withArchived()
{
List<Habit> habits = habitList.toList();
List<Habit> habits = Lists.newArrayList(habitList.iterator());
assertThat(habits.size(), equalTo(10));
assertThat(habits.get(3).getName(), equalTo("habit 3"));
}
@ -166,12 +190,4 @@ public class SQLiteHabitListTest extends BaseAndroidTest
h2.setId(1000L);
assertThat(habitList.indexOf(h2), equalTo(-1));
}
private HabitRecord getRecord(long id)
{
return new Select()
.from(HabitRecord.class)
.where("id = ?", id)
.executeSingle();
}
}

@ -23,19 +23,19 @@ import android.support.annotation.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
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;
@RunWith(AndroidJUnit4.class)
@ -50,6 +50,8 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
private long day;
private SQLiteRepository<RepetitionRecord> sqlite;
@Override
public void setUp()
{
@ -59,6 +61,8 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
repetitions = habit.getRepetitions();
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
sqlite = new SQLiteRepository<>(RepetitionRecord.class,
DatabaseUtils.openDatabase());
}
@Test
@ -130,15 +134,13 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
@Nullable
private RepetitionRecord getByTimestamp(long timestamp)
{
return selectByTimestamp(timestamp).executeSingle();
}
String query = "where habit = ? and timestamp = ?";
@NonNull
private From selectByTimestamp(long timestamp)
{
return new Select()
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp = ?", timestamp);
String params[] = {
Long.toString(habit.getId()),
Long.toString(timestamp)
};
return sqlite.findFirst(query, params);
}
}

@ -0,0 +1,189 @@
/*
* 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.sqlite.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.apache.commons.lang3.builder.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import static org.hamcrest.core.IsEqual.*;
import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteRepositoryTest extends BaseAndroidTest
{
private SQLiteRepository<ThingRecord> repository;
private SQLiteDatabase db;
@Before
@Override
public void setUp()
{
super.setUp();
this.db = DatabaseUtils.openDatabase();
repository = new SQLiteRepository<>(ThingRecord.class, db);
db.execSQL("drop table if exists tests");
db.execSQL("create table tests(" +
"id integer not null primary key autoincrement, " +
"color_number integer not null, score float not null, " +
"name string not null)");
}
@Test
public void testFind() throws Exception
{
db.execSQL("insert into tests(id, color_number, name, score) " +
"values (10, 20, 'hello', 8.0)");
ThingRecord record = repository.find(10L);
assertNotNull(record);
assertThat(record.id, equalTo(10L));
assertThat(record.color, equalTo(20));
assertThat(record.name, equalTo("hello"));
assertThat(record.score, equalTo(8.0));
}
@Test
public void testSave_withId() throws Exception
{
ThingRecord record = new ThingRecord();
record.id = 50L;
record.color = 10;
record.name = "hello";
record.score = 5.0;
repository.save(record);
assertThat(record, equalTo(repository.find(50L)));
record.name = "world";
record.score = 128.0;
repository.save(record);
assertThat(record, equalTo(repository.find(50L)));
}
@Test
public void testSave_withoutId() throws Exception
{
ThingRecord r1 = new ThingRecord();
r1.color = 10;
r1.name = "hello";
r1.score = 16.0;
repository.save(r1);
ThingRecord r2 = new ThingRecord();
r2.color = 20;
r2.name = "world";
r2.score = 2.0;
repository.save(r2);
assertThat(r1.id, equalTo(1L));
assertThat(r2.id, equalTo(2L));
}
@Test
public void testRemove() throws Exception
{
ThingRecord rec1 = new ThingRecord();
rec1.color = 10;
rec1.name = "hello";
rec1.score = 16.0;
repository.save(rec1);
ThingRecord rec2 = new ThingRecord();
rec2.color = 20;
rec2.name = "world";
rec2.score = 32.0;
repository.save(rec2);
long id = rec1.id;
assertThat(rec1, equalTo(repository.find(id)));
assertThat(rec2, equalTo(repository.find(rec2.id)));
repository.remove(rec1);
assertThat(rec1.id, equalTo(null));
assertNull(repository.find(id));
assertThat(rec2, equalTo(repository.find(rec2.id)));
repository.remove(rec1); // should have no effect
assertNull(repository.find(id));
}
}
@Table(name = "tests")
class ThingRecord
{
@Column
public Long id;
@Column
public String name;
@Column(name = "color_number")
public Integer color;
@Column
public Double score;
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ThingRecord record = (ThingRecord) o;
return new EqualsBuilder()
.append(id, record.id)
.append(name, record.name)
.append(color, record.color)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(id)
.append(name)
.append(color)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("id", id)
.append("name", name)
.append("color", color)
.toString();
}
}

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

@ -1,4 +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);
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);

@ -1,5 +1,11 @@
DROP TABLE Score;
CREATE TABLE Score (Id INTEGER PRIMARY KEY AUTOINCREMENT, habit INTEGER REFERENCES Habits(Id), score REAL, timestamp INTEGER);
CREATE INDEX idx_score_habit_timestamp on score(habit, timestamp);
delete from Streak;
delete from Checkmarks;
drop table Score;
create table Score (
id integer primary key autoincrement,
habit integer references habits(id),
score real,
timestamp integer);
create index idx_score_habit_timestamp on Score(habit, timestamp);
delete from streak;
delete from checkmarks;

@ -0,0 +1,6 @@
create table Events (
id integer primary key autoincrement,
timestamp integer,
message text,
server_id integer
);

@ -1,2 +0,0 @@
alter table habits add column reminder_hour integer;
alter table habits add column reminder_min integer;

@ -1 +0,0 @@
alter table habits add column highlight integer not null default 0;

@ -1 +0,0 @@
alter table habits add column archived integer not null default 0;

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

@ -22,8 +22,6 @@ package org.isoron.uhabits;
import android.app.*;
import android.content.*;
import com.activeandroid.*;
import org.isoron.androidbase.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.reminders.*;
@ -92,13 +90,13 @@ public class HabitsApplication extends Application
try
{
DatabaseUtils.initializeActiveAndroid(context);
DatabaseUtils.initializeDatabase(context);
}
catch (InvalidDatabaseVersionException e)
{
File db = DatabaseUtils.getDatabaseFile(context);
db.renameTo(new File(db.getAbsolutePath() + ".invalid"));
DatabaseUtils.initializeActiveAndroid(context);
DatabaseUtils.initializeDatabase(context);
}
widgetUpdater = component.getWidgetUpdater();
@ -124,8 +122,6 @@ public class HabitsApplication extends Application
public void onTerminate()
{
context = null;
ActiveAndroid.dispose();
reminderScheduler.stopListening();
widgetUpdater.stopListening();
notificationTray.stopListening();

@ -0,0 +1,54 @@
/*
* 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;
import android.content.*;
import android.database.sqlite.*;
import org.isoron.androidbase.storage.*;
public class HabitsDatabaseOpener extends BaseSQLiteOpenHelper
{
private final int version;
public HabitsDatabaseOpener(Context context,
String databaseFilename,
int version)
{
super(context, databaseFilename, version);
this.version = version;
}
@Override
public void onCreate(SQLiteDatabase db)
{
onUpgrade(db, 8, version);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
if(oldVersion < 8) throw new UnsupportedDatabaseVersionException();
super.onUpgrade(db, oldVersion, newVersion);
}
}

@ -21,7 +21,6 @@ package org.isoron.uhabits.io;
import android.support.annotation.*;
import com.activeandroid.*;
import com.opencsv.*;
import org.isoron.uhabits.core.models.*;
@ -32,6 +31,8 @@ import java.util.*;
import javax.inject.*;
import static org.isoron.uhabits.utils.DatabaseUtils.executeAsTransaction;
/**
* Class that imports data from HabitBull CSV files.
*/
@ -59,16 +60,7 @@ public class HabitBullCSVImporter extends AbstractImporter
@Override
public void importHabitsFromFile(@NonNull final File file) throws IOException
{
ActiveAndroid.beginTransaction();
try
{
parseFile(file);
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
executeAsTransaction(() -> parseFile(file));
}
private void parseFile(@NonNull File file) throws IOException

@ -25,8 +25,6 @@ import android.database.sqlite.*;
import android.support.annotation.*;
import android.util.*;
import com.activeandroid.*;
import org.isoron.androidbase.*;
import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.BuildConfig;
@ -89,9 +87,9 @@ public class LoopDBImporter extends AbstractImporter
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
ActiveAndroid.dispose();
DatabaseUtils.dispose();
File originalDB = DatabaseUtils.getDatabaseFile(context);
FileUtils.copy(file, originalDB);
DatabaseUtils.initializeActiveAndroid(context);
DatabaseUtils.initializeDatabase(context);
}
}

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

@ -19,55 +19,60 @@
package org.isoron.uhabits.models.sqlite;
import android.database.sqlite.*;
import android.support.annotation.*;
import com.activeandroid.query.*;
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.memory.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import static org.isoron.uhabits.utils.DatabaseUtils.executeAsTransaction;
/**
* Implementation of a {@link HabitList} that is backed by SQLite.
*/
public class SQLiteHabitList extends HabitList
{
private static HashMap<Long, Habit> cache;
private static SQLiteHabitList instance;
@NonNull
private final SQLiteUtils<HabitRecord> sqlite;
private final SQLiteRepository<HabitRecord> repository;
@NonNull
private final ModelFactory modelFactory;
@NonNull
private Order order;
private final MemoryHabitList list;
private boolean loaded = false;
public SQLiteHabitList(@NonNull ModelFactory modelFactory)
{
super();
this.modelFactory = modelFactory;
this.list = new MemoryHabitList();
if (cache == null) cache = new HashMap<>();
sqlite = new SQLiteUtils<>(HabitRecord.class);
order = Order.BY_POSITION;
repository =
new SQLiteRepository<>(HabitRecord.class, DatabaseUtils.openDatabase());
}
protected SQLiteHabitList(@NonNull ModelFactory modelFactory,
@NonNull HabitMatcher filter,
@NonNull Order order)
private void loadRecords()
{
super(filter);
this.modelFactory = modelFactory;
if(loaded) return;
loaded = true;
if (cache == null) cache = new HashMap<>();
sqlite = new SQLiteUtils<>(HabitRecord.class);
this.order = order;
list.removeAll();
List<HabitRecord> records = repository.findAll("order by position");
for (HabitRecord rec : records)
{
Habit h = modelFactory.buildHabit();
rec.copyTo(h);
list.add(h);
}
}
public static SQLiteHabitList getInstance(
@ -78,127 +83,123 @@ public class SQLiteHabitList extends HabitList
}
@Override
public void add(@NonNull Habit habit)
public synchronized void add(@NonNull Habit habit)
{
if (cache.containsValue(habit))
throw new IllegalArgumentException("habit already added");
loadRecords();
list.add(habit);
HabitRecord record = new HabitRecord();
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);
getObservable().notifyListeners();
}
@Override
@Nullable
public Habit getById(long id)
{
if (!cache.containsKey(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);
loadRecords();
return list.getById(id);
}
@Override
@NonNull
public Habit getByPosition(int position)
{
return toList().get(position);
loadRecords();
return list.getByPosition(position);
}
@NonNull
@Override
public HabitList getFiltered(HabitMatcher filter)
{
return new SQLiteHabitList(modelFactory, filter, order);
loadRecords();
return list.getFiltered(filter);
}
@Override
@NonNull
public Order getOrder()
{
return order;
return list.getOrder();
}
@Override
public void setOrder(@NonNull Order order)
{
this.order = order;
list.setOrder(order);
}
@Override
public int indexOf(@NonNull Habit h)
{
return toList().indexOf(h);
loadRecords();
return list.indexOf(h);
}
@Override
public Iterator<Habit> iterator()
{
return Collections.unmodifiableCollection(toList()).iterator();
loadRecords();
return list.iterator();
}
public void rebuildOrder()
private void rebuildOrder()
{
List<Habit> habits = toList();
int i = 0;
for (Habit h : habits)
{
HabitRecord record = HabitRecord.get(h.getId());
if (record == null)
throw new RuntimeException("habit not in database");
record.position = i++;
record.save();
}
update(habits);
// List<Habit> habits = toList();
//
// int i = 0;
// for (Habit h : habits)
// {
// HabitRecord record = repository.find(h.getId());
// if (record == null)
// throw new RuntimeException("habit not in database");
//
// record.position = i++;
// repository.save(record);
// }
//
// update(habits);
}
@Override
public void remove(@NonNull Habit habit)
public synchronized void remove(@NonNull Habit habit)
{
if (!cache.containsKey(habit.getId()))
throw new RuntimeException("habit not in cache");
loadRecords();
list.remove(habit);
cache.remove(habit.getId());
HabitRecord record = HabitRecord.get(habit.getId());
HabitRecord record = repository.find(habit.getId());
if (record == null) throw new RuntimeException("habit not in database");
record.cascadeDelete();
executeAsTransaction(() ->
{
((SQLiteRepetitionList) habit.getRepetitions()).removeAll();
repository.remove(record);
});
rebuildOrder();
getObservable().notifyListeners();
}
@Override
public void removeAll()
public synchronized void removeAll()
{
sqlite.query("delete from repetitions", null);
sqlite.query("delete from habits", null);
list.removeAll();
SQLiteDatabase db = DatabaseUtils.openDatabase();
db.execSQL("delete from habits");
db.execSQL("delete from repetitions");
getObservable().notifyListeners();
}
@Override
public synchronized void reorder(Habit from, Habit to)
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
{
if (from == to) return;
loadRecords();
list.reorder(from, to);
HabitRecord fromRecord = HabitRecord.get(from.getId());
HabitRecord toRecord = HabitRecord.get(to.getId());
HabitRecord fromRecord = repository.find(from.getId());
HabitRecord toRecord = repository.find(to.getId());
if (fromRecord == null)
throw new RuntimeException("habit not in database");
@ -207,128 +208,59 @@ public class SQLiteHabitList extends HabitList
Integer fromPos = fromRecord.position;
Integer toPos = toRecord.position;
Log.d("SQLiteHabitList",
String.format("reorder: %d %d", fromPos, toPos));
SQLiteDatabase db = DatabaseUtils.openDatabase();
if (toPos < fromPos)
{
new Update(HabitRecord.class)
.set("position = position + 1")
.where("position >= ? and position < ?", toPos, fromPos)
.execute();
db.execSQL("update habits set position = position + 1 " +
"where position >= ? and position < ?",
new String[]{ toPos.toString(), fromPos.toString() });
}
else
{
new Update(HabitRecord.class)
.set("position = position - 1")
.where("position > ? and position <= ?", fromPos, toPos)
.execute();
db.execSQL("update habits set position = position - 1 " +
"where position > ? and position <= ?",
new String[]{ fromPos.toString(), toPos.toString() });
}
fromRecord.position = toPos;
fromRecord.save();
repository.save(fromRecord);
update(from);
getObservable().notifyListeners();
}
@Override
public void repair()
{
super.repair();
loadRecords();
rebuildOrder();
}
@Override
public int size()
{
return toList().size();
loadRecords();
return list.size();
}
@Override
public void update(List<Habit> habits)
public synchronized void update(List<Habit> habits)
{
loadRecords();
for (Habit h : habits)
{
HabitRecord record = HabitRecord.get(h.getId());
HabitRecord record = repository.find(h.getId());
if (record == null)
throw new RuntimeException("habit not in database");
record.copyFrom(h);
record.save();
}
}
public synchronized List<Habit> toList()
{
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;
repository.save(record);
}
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(" ");
getObservable().notifyListeners();
}
private String buildSelectQuery()
public void reload()
{
StringBuilder query = new StringBuilder();
appendSelect(query);
appendWhere(query);
appendOrderBy(query);
return query.toString();
loaded = false;
}
}

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

@ -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,67 @@
package org.isoron.uhabits.models.sqlite.records;
import android.annotation.*;
import android.database.*;
import android.support.annotation.*;
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.utils.DatabaseUtils;
import java.lang.reflect.*;
/**
* The SQLite database record corresponding to a {@link Habit}.
*/
@Table(name = "Habits")
public class HabitRecord extends Model implements SQLiteRecord
@Table(name = "habits")
public class HabitRecord
{
public static String SELECT =
"select id, color, description, freq_den, freq_num, " +
"name, position, reminder_hour, reminder_min, " +
"highlight, archived, reminder_days, type, target_type, " +
"target_value, unit from habits ";
@Column
public String description;
@Column(name = "name")
@Column
public String name;
@Column(name = "description")
public String description;
@Column(name = "freq_num")
public int freqNum;
public Integer freqNum;
@Column(name = "freq_den")
public int freqDen;
public Integer freqDen;
@Column(name = "color")
public int color;
@Column
public Integer color;
@Column(name = "position")
public int position;
@Column
public Integer position;
@Nullable
@Column(name = "reminder_hour")
public Integer reminderHour;
@Nullable
@Column(name = "reminder_min")
public Integer reminderMin;
@Column(name = "reminder_days")
public int reminderDays;
public Integer reminderDays;
@Column(name = "highlight")
public int highlight;
@Column
public Integer highlight;
@Column(name = "archived")
public int archived;
@Column
public Integer archived;
@Column(name = "type")
public int type;
@Column
public Integer type;
@Column(name = "target_value")
public double targetValue;
public Double targetValue;
@Column(name = "target_type")
public int targetType;
public Integer targetType;
@Column(name = "unit")
@Column
public String unit;
public HabitRecord()
{
}
@Nullable
public static HabitRecord get(long id)
{
return HabitRecord.load(HabitRecord.class, id);
}
/**
* Changes the id of a habit on the database.
*
* @param oldId the original id
* @param newId the new id
*/
@SuppressLint("DefaultLocale")
public static void updateId(long oldId, long newId)
{
SQLiteUtils.execSql(
String.format("update Habits set Id = %d where Id = %d", newId,
oldId));
}
/**
* Deletes the habit and all data associated to it, including checkmarks,
* repetitions and scores.
*/
public void cascadeDelete()
{
Long id = getId();
DatabaseUtils.executeAsTransaction(() -> {
new Delete()
.from(RepetitionRecord.class)
.where("habit = ?", id)
.execute();
delete();
});
}
@Column
public Long id;
public void copyFrom(Habit model)
{
this.id = model.getId();
this.name = model.getName();
this.description = model.getDescription();
this.highlight = 0;
@ -161,35 +106,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)
{
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.setId(this.getId());
habit.setType(this.type);
habit.setTargetType(this.targetType);
habit.setTargetValue(this.targetValue);
@ -202,28 +126,77 @@ public class HabitRecord extends Model implements SQLiteRecord
}
}
/**
* Saves the habit on the database, and assigns the specified id to it.
*
* @param id the id that the habit should receive
*/
public void save(long id)
@Override
public boolean equals(Object o)
{
save();
updateId(getId(), id);
if (this == o) return true;
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)
{
try
@Override
public int hashCode()
{
Field f = (Model.class).getDeclaredField("mId");
f.setAccessible(true);
f.set(this, id);
return new HashCodeBuilder(17, 37)
.appendSuper(super.hashCode())
.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,28 @@
package org.isoron.uhabits.models.sqlite.records;
import android.database.*;
import com.activeandroid.*;
import com.activeandroid.annotation.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.core.models.*;
/**
* The SQLite database record corresponding to a {@link Repetition}.
*/
@Table(name = "Repetitions")
public class RepetitionRecord extends Model implements SQLiteRecord
public class RepetitionRecord
{
@Column(name = "habit")
public HabitRecord habit;
@Column(name = "timestamp")
@Column(name = "habit")
public Long habit_id;
@Column
public Long timestamp;
@Column(name = "value")
public int value;
@Column
public Integer value;
public static RepetitionRecord get(Long id)
{
return RepetitionRecord.load(RepetitionRecord.class, id);
}
@Column
public Long id;
public void copyFrom(Repetition repetition)
{
@ -52,13 +48,6 @@ public class RepetitionRecord extends Model implements SQLiteRecord
value = repetition.getValue();
}
@Override
public void copyFrom(Cursor c)
{
timestamp = c.getLong(1);
value = c.getInt(2);
}
public Repetition toRepetition()
{
return new Repetition(timestamp, value);

@ -19,18 +19,17 @@
package org.isoron.uhabits.sync;
import android.support.annotation.NonNull;
import android.support.annotation.*;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Select;
import java.util.List;
import org.isoron.androidbase.storage.*;
@Table(name = "Events")
public class Event extends Model
public class Event
{
@Nullable
@Column
public Long id;
@NonNull
@Column(name = "timestamp")
public Long timestamp;
@ -56,10 +55,4 @@ public class Event extends Model
this.timestamp = timestamp;
this.message = message;
}
@NonNull
public static List<Event> getAll()
{
return new Select().from(Event.class).orderBy("timestamp").execute();
}
}

@ -23,10 +23,12 @@ import android.support.annotation.*;
import android.util.*;
import org.isoron.androidbase.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.utils.*;
import org.json.*;
import java.net.*;
@ -95,6 +97,8 @@ public class SyncManager implements CommandRunner.Listener
private SSLContextProvider sslProvider;
private final SQLiteRepository<Event> repository;
@Inject
public SyncManager(@NonNull SSLContextProvider sslProvider,
@NonNull Preferences prefs,
@ -109,8 +113,10 @@ public class SyncManager implements CommandRunner.Listener
this.commandParser = commandParser;
this.isListening = false;
repository =
new SQLiteRepository<>(Event.class, DatabaseUtils.openDatabase());
pendingConfirmation = new LinkedList<>();
pendingEmit = new LinkedList<>(Event.getAll());
pendingEmit = new LinkedList<>(repository.findAll("order by timestamp"));
groupKey = prefs.getSyncKey();
clientId = prefs.getSyncClientId();
@ -129,7 +135,7 @@ public class SyncManager implements CommandRunner.Listener
JSONObject msg = toJSONObject(command.toJson());
Long now = new Date().getTime();
Event e = new Event(command.getId(), now, msg.toString());
e.save();
repository.save(e);
Log.i("SyncManager", "Adding to outbox: " + msg.toString());
@ -337,7 +343,7 @@ public class SyncManager implements CommandRunner.Listener
{
Log.i("SyncManager", "Pending command confirmed");
pendingConfirmation.remove(e);
e.delete();
repository.remove(e);
return;
}
}

@ -20,33 +20,40 @@
package org.isoron.uhabits.utils;
import android.content.*;
import android.database.sqlite.*;
import android.support.annotation.*;
import com.activeandroid.*;
import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.sync.*;
import java.io.*;
import java.text.*;
public abstract class DatabaseUtils
{
@Nullable
private static HabitsDatabaseOpener opener = null;
public static void executeAsTransaction(Callback callback)
{
ActiveAndroid.beginTransaction();
try (SQLiteDatabase db = openDatabase())
{
db.beginTransaction();
try
{
callback.execute();
ActiveAndroid.setTransactionSuccessful();
db.setTransactionSuccessful();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
ActiveAndroid.endTransaction();
db.endTransaction();
}
}
}
@ -71,17 +78,12 @@ public abstract class DatabaseUtils
}
@SuppressWarnings("unchecked")
public static void initializeActiveAndroid(Context context)
public static void initializeDatabase(Context context)
{
Configuration dbConfig = new Configuration.Builder(context)
.setDatabaseName(getDatabaseFilename())
.setDatabaseVersion(BuildConfig.databaseVersion)
.addModelClasses(HabitRecord.class, RepetitionRecord.class,
Event.class).create();
try
{
ActiveAndroid.initialize(dbConfig);
opener = new HabitsDatabaseOpener(context, getDatabaseFilename(),
BuildConfig.databaseVersion);
}
catch (RuntimeException e)
{
@ -92,7 +94,8 @@ public abstract class DatabaseUtils
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static String saveDatabaseCopy(Context context, File dir) throws IOException
public static String saveDatabaseCopy(Context context, File dir)
throws IOException
{
SimpleDateFormat dateFormat = DateFormats.getBackupDateFormat();
String date = dateFormat.format(DateUtils.getLocalTime());
@ -106,8 +109,20 @@ public abstract class DatabaseUtils
return dbCopy.getAbsolutePath();
}
@NonNull
public static SQLiteDatabase openDatabase()
{
if(opener == null) throw new IllegalStateException();
return opener.getWritableDatabase();
}
public static void dispose()
{
opener = null;
}
public interface Callback
{
void execute();
void execute() throws Exception;
}
}

@ -25,14 +25,15 @@ import android.os.*;
import android.support.annotation.*;
import android.widget.*;
import com.activeandroid.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import static android.appwidget.AppWidgetManager.*;
import static org.isoron.androidbase.utils.InterfaceUtils.*;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
import static org.isoron.androidbase.utils.InterfaceUtils.dpToPixels;
public abstract class BaseWidgetProvider extends AppWidgetProvider
{
@ -109,7 +110,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
}
catch (HabitNotFoundException e)
{
Log.e("BaseWidgetProvider", e);
e.printStackTrace();
}
}
}

@ -155,7 +155,7 @@ public abstract class HabitList implements Iterable<Habit>
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public abstract void reorder(Habit from, Habit to);
public abstract void reorder(@NonNull Habit from, @NonNull Habit to);
public void repair()
{

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

@ -22,6 +22,7 @@ package org.isoron.uhabits.core.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
@ -60,8 +61,10 @@ public class MemoryCheckmarkList extends CheckmarkList
Checkmark oldest = getOldestComputed();
if(newest != null) newestTimestamp = newest.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)
{
if(time > newestTimestamp || time < oldestTimestamp)

@ -25,7 +25,10 @@ import org.isoron.uhabits.core.models.*;
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}.
@ -33,30 +36,37 @@ import static org.isoron.uhabits.core.models.HabitList.Order.*;
public class MemoryHabitList extends HabitList
{
@NonNull
private LinkedList<Habit> list;
private LinkedList<Habit> list = new LinkedList<>();
private Comparator<Habit> comparator = null;
@NonNull
private Order order;
private Order order = Order.BY_POSITION;
@Nullable
private MemoryHabitList parent = null;
public MemoryHabitList()
{
super();
list = new LinkedList<>();
order = Order.BY_POSITION;
}
protected MemoryHabitList(@NonNull HabitMatcher matcher)
protected MemoryHabitList(@NonNull HabitMatcher matcher,
Comparator<Habit> comparator,
@NonNull MemoryHabitList parent)
{
super(matcher);
list = new LinkedList<>();
order = Order.BY_POSITION;
this.parent = parent;
this.comparator = comparator;
parent.getObservable().addListener(this::loadFromParent);
loadFromParent();
}
@Override
public void add(@NonNull Habit habit) throws IllegalArgumentException
public synchronized void add(@NonNull Habit habit)
throws IllegalArgumentException
{
throwIfHasParent();
if (list.contains(habit))
throw new IllegalArgumentException("habit already added");
@ -67,14 +77,16 @@ public class MemoryHabitList extends HabitList
if (id == null) habit.setId((long) list.size());
list.addLast(habit);
resort();
getObservable().notifyListeners();
}
@Override
public Habit getById(long id)
public synchronized Habit getById(long id)
{
for (Habit h : list)
{
if (h.getId() == null) continue;
if (h.getId() == null) throw new IllegalStateException();
if (h.getId() == id) return h;
}
return null;
@ -82,33 +94,66 @@ public class MemoryHabitList extends HabitList
@NonNull
@Override
public Habit getByPosition(int position)
public synchronized Habit getByPosition(int position)
{
return list.get(position);
}
@NonNull
@Override
public HabitList getFiltered(HabitMatcher matcher)
public synchronized HabitList getFiltered(HabitMatcher matcher)
{
MemoryHabitList habits = new MemoryHabitList(matcher);
habits.comparator = comparator;
for (Habit h : this) if (matcher.matches(h)) habits.add(h);
return habits;
return new MemoryHabitList(matcher, comparator, this);
}
@Override
public Order getOrder()
public synchronized Order getOrder()
{
return order;
}
@Override
public synchronized void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
}
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();
return Double.compare(s2, s1);
};
if (order == BY_POSITION) return null;
if (order == BY_NAME) return nameComparator;
if (order == BY_COLOR) return colorComparator;
if (order == BY_SCORE) return scoreComparator;
throw new IllegalStateException();
}
@Override
public int indexOf(@NonNull Habit h)
{
return list.indexOf(h);
}
@NonNull
@Override
public Iterator<Habit> iterator()
{
@ -116,27 +161,29 @@ public class MemoryHabitList extends HabitList
}
@Override
public void remove(@NonNull Habit habit)
public synchronized void remove(@NonNull Habit habit)
{
throwIfHasParent();
list.remove(habit);
}
@Override
public void reorder(Habit from, Habit to)
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
{
throwIfHasParent();
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);
}
@Override
public void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
}
@Override
public int size()
{
@ -149,32 +196,23 @@ public class MemoryHabitList extends HabitList
// NOP
}
private Comparator<Habit> getComparatorByOrder(Order order)
private void throwIfHasParent()
{
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);
};
if (parent != null) throw new IllegalStateException(
"Filtered lists cannot be modified directly. " +
"You should modify the parent list instead.");
}
Comparator<Habit> scoreComparator = (h1, h2) -> {
double s1 = h1.getScores().getTodayValue();
double s2 = h2.getScores().getTodayValue();
return Double.compare(s2, s1);
};
private synchronized void loadFromParent()
{
if (parent == null) throw new IllegalStateException();
if (order == BY_POSITION) return null;
if (order == BY_NAME) return nameComparator;
if (order == BY_COLOR) return colorComparator;
if (order == BY_SCORE) return scoreComparator;
throw new IllegalStateException();
list.clear();
for (Habit h : parent) if (filter.matches(h)) list.add(h);
resort();
}
private void resort()
private synchronized void resort()
{
if (comparator != null) Collections.sort(list, comparator);
}

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

@ -19,20 +19,29 @@
package org.isoron.uhabits.core.models;
import org.hamcrest.*;
import org.isoron.uhabits.*;
import org.junit.*;
import org.junit.rules.*;
import java.io.*;
import java.util.*;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.fail;
import static org.hamcrest.CoreMatchers.*;
import static org.isoron.uhabits.core.models.HabitList.Order.*;
import static org.junit.Assert.*;
import static org.hamcrest.MatcherAssert.*;
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.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@SuppressWarnings("JavaDoc")
public class HabitListTest extends BaseUnitTest
{
@Rule
public ExpectedException thrown = ExpectedException.none();
private ArrayList<Habit> habitsArray;
private HabitList activeHabits;
@ -69,45 +78,41 @@ public class HabitListTest extends BaseUnitTest
}
@Test
public void test_countActive()
public void testSize()
{
assertThat(habitList.size(), equalTo(10));
assertThat(activeHabits.size(), equalTo(6));
assertThat(reminderHabits.size(), equalTo(4));
}
@Test
public void test_getByPosition()
public void testGetByPosition()
{
assertThat(habitList.getByPosition(0), equalTo(habitsArray.get(0)));
assertThat(habitList.getByPosition(3), equalTo(habitsArray.get(3)));
assertThat(habitList.getByPosition(9), equalTo(habitsArray.get(9)));
assertThat(activeHabits.getByPosition(0), equalTo(habitsArray.get(2)));
}
@Test
public void test_getHabitsWithReminder()
{
assertThat(reminderHabits.size(), equalTo(4));
assertThat(reminderHabits.getByPosition(1),
equalTo(habitsArray.get(3)));
}
@Test
public void test_get_withInvalidId()
public void testGetById()
{
assertThat(habitList.getById(100L), is(nullValue()));
Habit habit1 = habitsArray.get(0);
Habit habit2 = habitList.getById(habit1.getId());
assertThat(habit1, equalTo(habit2));
}
@Test
public void test_get_withValidId()
public void testGetById_withInvalidId()
{
Habit habit1 = habitsArray.get(0);
Habit habit2 = habitList.getById(habit1.getId());
assertThat(habit1, equalTo(habit2));
assertNull(habitList.getById(100L));
}
@Test
public void test_ordering()
public void testOrdering()
{
HabitList list = modelFactory.buildHabitList();
Habit h1 = fixtures.createEmptyHabit();
@ -155,7 +160,7 @@ public class HabitListTest extends BaseUnitTest
}
@Test
public void test_reorder()
public void testReorder()
{
int operations[][] = {
{ 5, 2 }, { 3, 7 }, { 4, 4 }, { 3, 2 }
@ -191,13 +196,16 @@ public class HabitListTest extends BaseUnitTest
}
@Test
public void test_size()
public void testReorder_withInvalidArguments() throws Exception
{
assertThat(habitList.size(), equalTo(10));
Habit h1 = habitsArray.get(0);
Habit h2 = fixtures.createEmptyHabit();
thrown.expect(IllegalArgumentException.class);
habitList.reorder(h1, h2);
}
@Test
public void test_writeCSV() throws IOException
public void testWriteCSV() throws IOException
{
HabitList list = modelFactory.buildHabitList();
@ -224,6 +232,43 @@ public class HabitListTest extends BaseUnitTest
StringWriter writer = new StringWriter();
list.writeCSV(writer);
MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV));
assertThat(writer.toString(), equalTo(expectedCSV));
}
@Test
public void testAdd() throws Exception
{
Habit h1 = fixtures.createEmptyHabit();
assertFalse(h1.isArchived());
assertNull(h1.getId());
assertThat(habitList.indexOf(h1), equalTo(-1));
habitList.add(h1);
assertNotNull(h1.getId());
assertThat(habitList.indexOf(h1), not(equalTo(-1)));
assertThat(activeHabits.indexOf(h1), not(equalTo(-1)));
}
@Test
public void testAdd_withFilteredList() throws Exception
{
thrown.expect(IllegalStateException.class);
activeHabits.add(fixtures.createEmptyHabit());
}
@Test
public void testRemove_onFilteredList() throws Exception
{
thrown.expect(IllegalStateException.class);
activeHabits.remove(fixtures.createEmptyHabit());
}
@Test
public void testReorder_onFilteredList() throws Exception
{
Habit h1 = fixtures.createEmptyHabit();
Habit h2 = fixtures.createEmptyHabit();
thrown.expect(IllegalStateException.class);
activeHabits.reorder(h1, h2);
}
}
Loading…
Cancel
Save