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' apply plugin: 'com.android.library'
android { android {
compileSdkVersion 25 compileSdkVersion 25
buildToolsVersion "25.0.2" buildToolsVersion "25.0.2"
@ -32,6 +31,7 @@ dependencies {
implementation 'com.google.dagger:dagger:2.9' implementation 'com.google.dagger:dagger:2.9'
implementation 'com.android.support:design:25.3.1' implementation 'com.android.support:design:25.3.1'
implementation 'com.android.support:appcompat-v7: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' annotationProcessor 'com.google.dagger:dagger-compiler:2.9'
androidTestAnnotationProcessor '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. * 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 * You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
*/ */
package org.isoron.uhabits.models.sqlite.records; package org.isoron.androidbase.storage;
import android.database.*;
public interface SQLiteRecord public class UnsupportedDatabaseVersionException extends RuntimeException
{ {
void copyFrom(Cursor c);
} }

@ -5,7 +5,7 @@ buildscript {
} }
dependencies { 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.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.4' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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.github.paolorotolo:appintro:3.4.0'
implementation 'com.google.dagger:dagger:2.9' implementation 'com.google.dagger:dagger:2.9'
implementation 'com.jakewharton:butterknife:8.6.1-SNAPSHOT' 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.apmem.tools:layouts:1.10'
implementation 'org.jetbrains:annotations-java5:15.0' implementation 'org.jetbrains:annotations-java5:15.0'
implementation 'com.google.code.gson:gson:2.7' implementation 'com.google.code.gson:gson:2.7'

@ -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,9 +22,11 @@ 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 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.*;
import org.isoron.uhabits.utils.*;
import org.junit.*; import org.junit.*;
import org.junit.runner.*; import org.junit.runner.*;
@ -35,25 +37,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, DatabaseUtils.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 +56,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,12 +22,13 @@ 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.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.isoron.uhabits.utils.*;
import org.junit.*; import org.junit.*;
import org.junit.rules.*; import org.junit.rules.*;
import org.junit.runner.*; import org.junit.runner.*;
@ -36,6 +37,8 @@ import java.util.*;
import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.*; import static org.hamcrest.core.IsEqual.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@SuppressWarnings("JavaDoc") @SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -49,6 +52,10 @@ public class SQLiteHabitListTest extends BaseAndroidTest
private ModelFactory modelFactory; private ModelFactory modelFactory;
private SQLiteRepository<HabitRecord> repository;
private ModelObservable.Listener listener;
@Override @Override
public void setUp() public void setUp()
{ {
@ -57,6 +64,9 @@ public class SQLiteHabitListTest extends BaseAndroidTest
fixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
modelFactory = component.getModelFactory(); modelFactory = component.getModelFactory();
repository =
new SQLiteRepository<>(HabitRecord.class,
DatabaseUtils.openDatabase());
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
@ -68,8 +78,20 @@ 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();
listener = mock(ModelObservable.Listener.class);
habitList.getObservable().addListener(listener);
}
@Override
protected void tearDown() throws Exception
{
habitList.getObservable().removeListener(listener);
super.tearDown();
} }
@Test @Test
@ -77,6 +99,8 @@ public class SQLiteHabitListTest extends BaseAndroidTest
{ {
Habit habit = modelFactory.buildHabit(); Habit habit = modelFactory.buildHabit();
habitList.add(habit); habitList.add(habit);
verify(listener).onModelChange();
exception.expect(IllegalArgumentException.class); exception.expect(IllegalArgumentException.class);
habitList.add(habit); habitList.add(habit);
} }
@ -91,7 +115,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 +130,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 +144,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 +190,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,19 +23,19 @@ 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 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.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*; import org.junit.*;
import org.junit.runner.*; 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 +50,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 +61,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,
DatabaseUtils.openDatabase());
} }
@Test @Test
@ -130,15 +134,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);
} }
} }

@ -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,3 +1,3 @@
delete from Score; delete from Score;
delete from Streak; delete from Streak;
delete from Checkmarks; delete from Checkmarks;

@ -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_score_habit_timestamp on Score(habit, timestamp);
create index idx_checkmark_habit_timestamp on checkmarks(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_repetitions_habit_timestamp on Repetitions(habit, timestamp);
create index idx_streak_habit_end on streak(habit, end); create index idx_streak_habit_end on Streak(habit, end);

@ -1,5 +1,11 @@
DROP TABLE Score; drop table Score;
CREATE TABLE Score (Id INTEGER PRIMARY KEY AUTOINCREMENT, habit INTEGER REFERENCES Habits(Id), score REAL, timestamp INTEGER); create table Score (
CREATE INDEX idx_score_habit_timestamp on score(habit, timestamp); id integer primary key autoincrement,
delete from Streak; habit integer references habits(id),
delete from Checkmarks; 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.app.*;
import android.content.*; import android.content.*;
import com.activeandroid.*;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.reminders.*; import org.isoron.uhabits.core.reminders.*;
@ -92,13 +90,13 @@ public class HabitsApplication extends Application
try try
{ {
DatabaseUtils.initializeActiveAndroid(context); DatabaseUtils.initializeDatabase(context);
} }
catch (InvalidDatabaseVersionException e) catch (InvalidDatabaseVersionException e)
{ {
File db = DatabaseUtils.getDatabaseFile(context); File db = DatabaseUtils.getDatabaseFile(context);
db.renameTo(new File(db.getAbsolutePath() + ".invalid")); db.renameTo(new File(db.getAbsolutePath() + ".invalid"));
DatabaseUtils.initializeActiveAndroid(context); DatabaseUtils.initializeDatabase(context);
} }
widgetUpdater = component.getWidgetUpdater(); widgetUpdater = component.getWidgetUpdater();
@ -124,8 +122,6 @@ public class HabitsApplication extends Application
public void onTerminate() public void onTerminate()
{ {
context = null; context = null;
ActiveAndroid.dispose();
reminderScheduler.stopListening(); reminderScheduler.stopListening();
widgetUpdater.stopListening(); widgetUpdater.stopListening();
notificationTray.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 android.support.annotation.*;
import com.activeandroid.*;
import com.opencsv.*; import com.opencsv.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
@ -32,6 +31,8 @@ import java.util.*;
import javax.inject.*; import javax.inject.*;
import static org.isoron.uhabits.utils.DatabaseUtils.executeAsTransaction;
/** /**
* Class that imports data from HabitBull CSV files. * Class that imports data from HabitBull CSV files.
*/ */
@ -59,16 +60,7 @@ public class HabitBullCSVImporter extends AbstractImporter
@Override @Override
public void importHabitsFromFile(@NonNull final File file) throws IOException public void importHabitsFromFile(@NonNull final File file) throws IOException
{ {
ActiveAndroid.beginTransaction(); executeAsTransaction(() -> parseFile(file));
try
{
parseFile(file);
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
} }
private void parseFile(@NonNull File file) throws IOException private void parseFile(@NonNull File file) throws IOException

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

@ -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,60 @@
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 org.isoron.androidbase.storage.*;
import com.activeandroid.util.*;
import org.apache.commons.lang3.*;
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.isoron.uhabits.utils.*;
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, DatabaseUtils.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 +83,123 @@ 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(); getObservable().notifyListeners();
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();
getObservable().notifyListeners();
} }
@Override @Override
public void removeAll() public synchronized void removeAll()
{ {
sqlite.query("delete from repetitions", null); list.removeAll();
sqlite.query("delete from habits", null); SQLiteDatabase db = DatabaseUtils.openDatabase();
db.execSQL("delete from habits");
db.execSQL("delete from repetitions");
getObservable().notifyListeners();
} }
@Override @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 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,128 +208,59 @@ public class SQLiteHabitList extends HabitList
Integer fromPos = fromRecord.position; Integer fromPos = fromRecord.position;
Integer toPos = toRecord.position; Integer toPos = toRecord.position;
SQLiteDatabase db = DatabaseUtils.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();
} }
@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()
{
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) getObservable().notifyListeners();
{
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() public void reload()
{ {
StringBuilder query = new StringBuilder(); loaded = false;
appendSelect(query);
appendWhere(query);
appendOrderBy(query);
return query.toString();
} }
} }

@ -19,16 +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 org.isoron.androidbase.storage.*;
import com.activeandroid.query.*;
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.isoron.uhabits.utils.*;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
import java.util.*; import java.util.*;
@ -38,165 +36,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 HabitRecord habitRecord;
private SQLiteStatement addStatement;
public static final String ADD_QUERY = private boolean loaded = false;
"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,
DatabaseUtils.openDatabase());
list = new MemoryRepetitionList(habit);
}
private void loadRecords()
{
if (loaded) return;
loaded = true;
SQLiteDatabase db = Cache.openDatabase(); check(habit.getId());
addStatement = db.compileStatement(ADD_QUERY); List<RepetitionRecord> records =
repository.findAll("where habit = ? order by timestamp",
habit.getId().toString());
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 @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 = ?",
List<Repetition> reps = new LinkedList<>(); habit.getId());
for (RepetitionRecord record : records)
{
record.habit = habitRecord;
reps.add(record.toRepetition());
}
return reps;
} }
@Override @Override
public long getTotalCount() public long getTotalCount()
{ {
SQLiteDatabase db = Cache.openDatabase(); loadRecords();
return list.getTotalCount();
}
return DatabaseUtils.queryNumEntries(db, "Repetitions", public void reload()
"habit=?", new String[] { Long.toString(habit.getId()) }); {
loaded = false;
}
@Contract("null -> fail")
private void check(Long value)
{
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; package org.isoron.uhabits.models.sqlite.records;
import android.annotation.*; import org.apache.commons.lang3.builder.*;
import android.database.*; import org.isoron.androidbase.storage.*;
import android.support.annotation.*;
import com.activeandroid.*;
import com.activeandroid.annotation.*;
import com.activeandroid.query.*;
import com.activeandroid.util.*;
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 public class HabitRecord
{ {
public static String SELECT = @Column
"select id, color, description, freq_den, freq_num, " + public String description;
"name, position, reminder_hour, reminder_min, " +
"highlight, archived, reminder_days, type, target_type, " +
"target_value, unit from habits ";
@Column(name = "name") @Column
public String name; public String name;
@Column(name = "description")
public String description;
@Column(name = "freq_num") @Column(name = "freq_num")
public int freqNum; public Integer freqNum;
@Column(name = "freq_den") @Column(name = "freq_den")
public int freqDen; public Integer freqDen;
@Column(name = "color") @Column
public int color; public Integer color;
@Column(name = "position") @Column
public int position; public Integer position;
@Nullable
@Column(name = "reminder_hour") @Column(name = "reminder_hour")
public Integer reminderHour; public Integer reminderHour;
@Nullable
@Column(name = "reminder_min") @Column(name = "reminder_min")
public Integer reminderMin; public Integer reminderMin;
@Column(name = "reminder_days") @Column(name = "reminder_days")
public int reminderDays; public Integer reminderDays;
@Column(name = "highlight") @Column
public int highlight; public Integer highlight;
@Column(name = "archived") @Column
public int archived; public Integer archived;
@Column(name = "type") @Column
public int type; public Integer type;
@Column(name = "target_value") @Column(name = "target_value")
public double targetValue; public Double targetValue;
@Column(name = "target_type") @Column(name = "target_type")
public int targetType; public Integer targetType;
@Column(name = "unit") @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 +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) 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 +126,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 return new HashCodeBuilder(17, 37)
{ .appendSuper(super.hashCode())
Field f = (Model.class).getDeclaredField("mId"); .append(name)
f.setAccessible(true); .append(description)
f.set(this, id); .append(freqNum)
} .append(freqDen)
catch (Exception e) .append(color)
{ .append(position)
throw new RuntimeException(e); .append(reminderHour)
} .append(reminderMin)
.append(reminderDays)
.append(highlight)
.append(archived)
.append(type)
.append(targetValue)
.append(targetType)
.append(unit)
.toHashCode();
}
@Override
public String toString()
{
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; package org.isoron.uhabits.models.sqlite.records;
import android.database.*; import org.isoron.androidbase.storage.*;
import com.activeandroid.*;
import com.activeandroid.annotation.*;
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 public class RepetitionRecord
{ {
@Column(name = "habit")
public HabitRecord habit; public HabitRecord habit;
@Column(name = "timestamp") @Column(name = "habit")
public Long habit_id;
@Column
public Long timestamp; public Long timestamp;
@Column(name = "value") @Column
public int 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 +48,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);

@ -19,18 +19,17 @@
package org.isoron.uhabits.sync; package org.isoron.uhabits.sync;
import android.support.annotation.NonNull; import android.support.annotation.*;
import com.activeandroid.Model; import org.isoron.androidbase.storage.*;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Select;
import java.util.List;
@Table(name = "Events") @Table(name = "Events")
public class Event extends Model public class Event
{ {
@Nullable
@Column
public Long id;
@NonNull @NonNull
@Column(name = "timestamp") @Column(name = "timestamp")
public Long timestamp; public Long timestamp;
@ -56,10 +55,4 @@ public class Event extends Model
this.timestamp = timestamp; this.timestamp = timestamp;
this.message = message; 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 android.util.*;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.isoron.androidbase.storage.*;
import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.utils.*;
import org.json.*; import org.json.*;
import java.net.*; import java.net.*;
@ -95,6 +97,8 @@ public class SyncManager implements CommandRunner.Listener
private SSLContextProvider sslProvider; private SSLContextProvider sslProvider;
private final SQLiteRepository<Event> repository;
@Inject @Inject
public SyncManager(@NonNull SSLContextProvider sslProvider, public SyncManager(@NonNull SSLContextProvider sslProvider,
@NonNull Preferences prefs, @NonNull Preferences prefs,
@ -109,8 +113,10 @@ public class SyncManager implements CommandRunner.Listener
this.commandParser = commandParser; this.commandParser = commandParser;
this.isListening = false; this.isListening = false;
repository =
new SQLiteRepository<>(Event.class, DatabaseUtils.openDatabase());
pendingConfirmation = new LinkedList<>(); pendingConfirmation = new LinkedList<>();
pendingEmit = new LinkedList<>(Event.getAll()); pendingEmit = new LinkedList<>(repository.findAll("order by timestamp"));
groupKey = prefs.getSyncKey(); groupKey = prefs.getSyncKey();
clientId = prefs.getSyncClientId(); clientId = prefs.getSyncClientId();
@ -129,7 +135,7 @@ public class SyncManager implements CommandRunner.Listener
JSONObject msg = toJSONObject(command.toJson()); JSONObject msg = toJSONObject(command.toJson());
Long now = new Date().getTime(); Long now = new Date().getTime();
Event e = new Event(command.getId(), now, msg.toString()); Event e = new Event(command.getId(), now, msg.toString());
e.save(); repository.save(e);
Log.i("SyncManager", "Adding to outbox: " + msg.toString()); Log.i("SyncManager", "Adding to outbox: " + msg.toString());
@ -337,7 +343,7 @@ public class SyncManager implements CommandRunner.Listener
{ {
Log.i("SyncManager", "Pending command confirmed"); Log.i("SyncManager", "Pending command confirmed");
pendingConfirmation.remove(e); pendingConfirmation.remove(e);
e.delete(); repository.remove(e);
return; return;
} }
} }

@ -20,33 +20,40 @@
package org.isoron.uhabits.utils; package org.isoron.uhabits.utils;
import android.content.*; import android.content.*;
import android.database.sqlite.*;
import android.support.annotation.*; import android.support.annotation.*;
import com.activeandroid.*;
import org.isoron.androidbase.utils.*; import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.models.sqlite.*; import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.sync.*;
import java.io.*; import java.io.*;
import java.text.*; import java.text.*;
public abstract class DatabaseUtils public abstract class DatabaseUtils
{ {
@Nullable
private static HabitsDatabaseOpener opener = null;
public static void executeAsTransaction(Callback callback) public static void executeAsTransaction(Callback callback)
{ {
ActiveAndroid.beginTransaction(); try (SQLiteDatabase db = openDatabase())
try
{ {
callback.execute(); db.beginTransaction();
ActiveAndroid.setTransactionSuccessful(); try
} {
finally callback.execute();
{ db.setTransactionSuccessful();
ActiveAndroid.endTransaction(); }
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
db.endTransaction();
}
} }
} }
@ -71,28 +78,24 @@ public abstract class DatabaseUtils
} }
@SuppressWarnings("unchecked") @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 try
{ {
ActiveAndroid.initialize(dbConfig); opener = new HabitsDatabaseOpener(context, getDatabaseFilename(),
BuildConfig.databaseVersion);
} }
catch (RuntimeException e) catch (RuntimeException e)
{ {
if(e.getMessage().contains("downgrade")) if (e.getMessage().contains("downgrade"))
throw new InvalidDatabaseVersionException(); throw new InvalidDatabaseVersionException();
else throw e; else throw e;
} }
} }
@SuppressWarnings("ResultOfMethodCallIgnored") @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(); SimpleDateFormat dateFormat = DateFormats.getBackupDateFormat();
String date = dateFormat.format(DateUtils.getLocalTime()); String date = dateFormat.format(DateUtils.getLocalTime());
@ -106,8 +109,20 @@ public abstract class DatabaseUtils
return dbCopy.getAbsolutePath(); 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 public interface Callback
{ {
void execute(); void execute() throws Exception;
} }
} }

@ -25,14 +25,15 @@ import android.os.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.widget.*; import android.widget.*;
import com.activeandroid.util.*;
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.preferences.*; import org.isoron.uhabits.core.preferences.*;
import static android.appwidget.AppWidgetManager.*; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
import static org.isoron.androidbase.utils.InterfaceUtils.*; 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 public abstract class BaseWidgetProvider extends AppWidgetProvider
{ {
@ -109,7 +110,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
} }
catch (HabitNotFoundException e) 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 from the habit that should be moved
* @param to the habit that currently occupies the desired position * @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() public void repair()
{ {

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

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