diff --git a/android-base/build.gradle b/android-base/build.gradle index ad4a4ded3..9501f86de 100644 --- a/android-base/build.gradle +++ b/android-base/build.gradle @@ -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' diff --git a/android-base/src/main/java/org/isoron/androidbase/storage/BaseSQLiteOpenHelper.java b/android-base/src/main/java/org/isoron/androidbase/storage/BaseSQLiteOpenHelper.java new file mode 100644 index 000000000..64d99b170 --- /dev/null +++ b/android-base/src/main/java/org/isoron/androidbase/storage/BaseSQLiteOpenHelper.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * 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 . + * + * + */ + +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(); + } +} diff --git a/android-base/src/main/java/org/isoron/androidbase/storage/Column.java b/android-base/src/main/java/org/isoron/androidbase/storage/Column.java new file mode 100644 index 000000000..e4c228a58 --- /dev/null +++ b/android-base/src/main/java/org/isoron/androidbase/storage/Column.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * 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 . + * + * + */ + +package org.isoron.androidbase.storage; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Column +{ + String name() default ""; +} diff --git a/android-base/src/main/java/org/isoron/androidbase/storage/SQLParser.java b/android-base/src/main/java/org/isoron/androidbase/storage/SQLParser.java new file mode 100644 index 000000000..d995ffeb0 --- /dev/null +++ b/android-base/src/main/java/org/isoron/androidbase/storage/SQLParser.java @@ -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 parse(final InputStream stream) throws IOException { + + final BufferedInputStream buffer = new BufferedInputStream(stream); + final List commands = new ArrayList(); + 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 == ' '; + } +} \ No newline at end of file diff --git a/android-base/src/main/java/org/isoron/androidbase/storage/SQLiteRepository.java b/android-base/src/main/java/org/isoron/androidbase/storage/SQLiteRepository.java new file mode 100644 index 000000000..4fd3f1906 --- /dev/null +++ b/android-base/src/main/java/org/isoron/androidbase/storage/SQLiteRepository.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * 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 . + * + * + */ + +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 +{ + @NonNull + private final Class klass; + + @NonNull + private final SQLiteDatabase db; + + public SQLiteRepository(@NonNull Class 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 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 cursorToMultipleRecords(Cursor c) + { + List 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> getFieldColumnPairs() + { + List> 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 fields = new ArrayList<>(); + List> columns = getFieldColumnPairs(); + for (Pair pair : columns) fields.add(pair.first); + return fields.toArray(new Field[]{}); + } + + @NonNull + private String[] getColumnNames() + { + List names = new ArrayList<>(); + List> columns = getFieldColumnPairs(); + for (Pair 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; + } +} diff --git a/android-base/src/main/java/org/isoron/androidbase/storage/Table.java b/android-base/src/main/java/org/isoron/androidbase/storage/Table.java new file mode 100644 index 000000000..c56711191 --- /dev/null +++ b/android-base/src/main/java/org/isoron/androidbase/storage/Table.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * 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 . + * + * + */ + +package org.isoron.androidbase.storage; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Table +{ + String name(); + String id() default "id"; +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/SQLiteRecord.java b/android-base/src/main/java/org/isoron/androidbase/storage/UnsupportedDatabaseVersionException.java similarity index 78% rename from uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/SQLiteRecord.java rename to android-base/src/main/java/org/isoron/androidbase/storage/UnsupportedDatabaseVersionException.java index 1991b1276..7e00f7d55 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/SQLiteRecord.java +++ b/android-base/src/main/java/org/isoron/androidbase/storage/UnsupportedDatabaseVersionException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Álinson Santos Xavier + * Copyright (C) 2017 Álinson Santos Xavier * * 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 . + * + * */ -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); } diff --git a/build.gradle b/build.gradle index 09f82f33e..64d00d09b 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4d42a9f55..e36e02cae 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/uhabits-android/build.gradle b/uhabits-android/build.gradle index 121a2245b..3f90d2c1b 100644 --- a/uhabits-android/build.gradle +++ b/uhabits-android/build.gradle @@ -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' diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java index 5b0a62dc9..4eeb5f6b9 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java @@ -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(); } } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/HabitRecordTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/HabitRecordTest.java index 872b3382c..18c393b2a 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/HabitRecordTest.java +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/HabitRecordTest.java @@ -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 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); diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java index a7b308c73..beb9f3b2a 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java @@ -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 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 habits = habitList.toList(); + List 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(); - } } \ No newline at end of file diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java index dd3657a62..2927d3198 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java @@ -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 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); } } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepositoryTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepositoryTest.java new file mode 100644 index 000000000..57bde37cb --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepositoryTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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 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(); + } +} diff --git a/uhabits-android/src/main/assets/migrations/10.sql b/uhabits-android/src/main/assets/migrations/10.sql index 70c3de873..e13df67f7 100644 --- a/uhabits-android/src/main/assets/migrations/10.sql +++ b/uhabits-android/src/main/assets/migrations/10.sql @@ -1,3 +1,3 @@ delete from Score; delete from Streak; -delete from Checkmarks; +delete from Checkmarks; \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/11.sql b/uhabits-android/src/main/assets/migrations/11.sql index 25b30ef51..4c9493cc1 100644 --- a/uhabits-android/src/main/assets/migrations/11.sql +++ b/uhabits-android/src/main/assets/migrations/11.sql @@ -1 +1 @@ -alter table habits add column reminder_days integer not null default 127; \ No newline at end of file +alter table Habits add column reminder_days integer not null default 127; \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/13.sql b/uhabits-android/src/main/assets/migrations/13.sql index 1d7eeafcf..3df9bc5de 100644 --- a/uhabits-android/src/main/assets/migrations/13.sql +++ b/uhabits-android/src/main/assets/migrations/13.sql @@ -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); \ No newline at end of file +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); \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/17.sql b/uhabits-android/src/main/assets/migrations/17.sql index 9c9a9b409..fa8f50719 100644 --- a/uhabits-android/src/main/assets/migrations/17.sql +++ b/uhabits-android/src/main/assets/migrations/17.sql @@ -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; \ No newline at end of file +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; \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/19.sql b/uhabits-android/src/main/assets/migrations/19.sql new file mode 100644 index 000000000..f7e9ec24b --- /dev/null +++ b/uhabits-android/src/main/assets/migrations/19.sql @@ -0,0 +1,6 @@ +create table Events ( + id integer primary key autoincrement, + timestamp integer, + message text, + server_id integer +); \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/5.sql b/uhabits-android/src/main/assets/migrations/5.sql deleted file mode 100644 index bde8462b1..000000000 --- a/uhabits-android/src/main/assets/migrations/5.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table habits add column reminder_hour integer; -alter table habits add column reminder_min integer; \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/6.sql b/uhabits-android/src/main/assets/migrations/6.sql deleted file mode 100644 index 7f1e33c94..000000000 --- a/uhabits-android/src/main/assets/migrations/6.sql +++ /dev/null @@ -1 +0,0 @@ -alter table habits add column highlight integer not null default 0; \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/7.sql b/uhabits-android/src/main/assets/migrations/7.sql deleted file mode 100644 index b666e9bf6..000000000 --- a/uhabits-android/src/main/assets/migrations/7.sql +++ /dev/null @@ -1 +0,0 @@ -alter table habits add column archived integer not null default 0; \ No newline at end of file diff --git a/uhabits-android/src/main/assets/migrations/9.sql b/uhabits-android/src/main/assets/migrations/9.sql new file mode 100644 index 000000000..5fb4502b4 --- /dev/null +++ b/uhabits-android/src/main/assets/migrations/9.sql @@ -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 +); diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.java b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.java index 7e555c77e..a4b05e391 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -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(); diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsDatabaseOpener.java b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsDatabaseOpener.java new file mode 100644 index 000000000..e75368bf8 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsDatabaseOpener.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * 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 . + * + * + */ + +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); + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java b/uhabits-android/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java index cbfa366c5..91fec9a81 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -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 diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/uhabits-android/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java index e08b10c9f..e6cb283e3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -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); } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java index 4025e824c..795c60ae4 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java @@ -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 diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java index 43eed3ce4..3d0496936 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java @@ -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 cache; - private static SQLiteHabitList instance; @NonNull - private final SQLiteUtils sqlite; + private final SQLiteRepository 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 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 iterator() { - return Collections.unmodifiableCollection(toList()).iterator(); + loadRecords(); + return list.iterator(); } - public void rebuildOrder() + private void rebuildOrder() { - List 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 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 habits) + public synchronized void update(List 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 toList() - { - String query = buildSelectQuery(); - List recordList = sqlite.query(query, null); - - List 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(); + repository.save(record); } - } - - private void appendSelect(StringBuilder query) - { - query.append(HabitRecord.SELECT); - } - private void appendWhere(StringBuilder query) - { - ArrayList 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; } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java index 1ebac67f7..9a7bfea9c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java @@ -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 repository; - private final SQLiteUtils 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; - SQLiteDatabase db = Cache.openDatabase(); - addStatement = db.compileStatement(ADD_QUERY); + check(habit.getId()); + List 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. - *

- * 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 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 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 toRepetitions( - @NonNull List records) + public void removeAll() { + loadRecords(); + list.removeAll(); check(habit.getId()); - - List reps = new LinkedList<>(); - for (RepetitionRecord record : records) - { - record.habit = habitRecord; - reps.add(record.toRepetition()); - } - - return reps; + repository.execSQL("delete from repetitions where habit = ?", + habit.getId()); } @Override public long getTotalCount() { - SQLiteDatabase db = Cache.openDatabase(); + loadRecords(); + return list.getTotalCount(); + } - return DatabaseUtils.queryNumEntries(db, "Repetitions", - "habit=?", new String[] { Long.toString(habit.getId()) }); + public void reload() + { + loaded = false; + } + + @Contract("null -> fail") + private void check(Long value) + { + if (value == null) throw new RuntimeException("null check failed"); } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteUtils.java b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteUtils.java deleted file mode 100644 index 616b0d12f..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * 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 . - */ - -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 -{ - private Class klass; - - public SQLiteUtils(Class klass) - { - this.klass = klass; - } - - @NonNull - public List 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 cursorToMultipleRecords(Cursor c) - { - List 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); - } - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java index 5731da245..9943941c9 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java @@ -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) + @Override + public int hashCode() { - try - { - Field f = (Model.class).getDeclaredField("mId"); - f.setAccessible(true); - f.set(this, id); - } - catch (Exception e) - { - throw new RuntimeException(e); - } + 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(); + } + + @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(); } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java index b10b7c73e..8cd871c78 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java @@ -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); diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/sync/Event.java b/uhabits-android/src/main/java/org/isoron/uhabits/sync/Event.java index fa20c1ac5..5f683928c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/sync/Event.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/sync/Event.java @@ -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 getAll() - { - return new Select().from(Event.class).orderBy("timestamp").execute(); - } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.java index e3dbf3a8d..761c8e7a9 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -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 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; } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java b/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java index 50a14edea..fd3f399c1 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java @@ -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 + try (SQLiteDatabase db = openDatabase()) { - callback.execute(); - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); + db.beginTransaction(); + try + { + callback.execute(); + db.setTransactionSuccessful(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + finally + { + db.endTransaction(); + } } } @@ -71,28 +78,24 @@ 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) { - if(e.getMessage().contains("downgrade")) + if (e.getMessage().contains("downgrade")) throw new InvalidDatabaseVersionException(); else throw e; } } @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; } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java index 6c3f5042a..31307bd8c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java @@ -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(); } } } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java index cf49b4ba0..34d5d1f6a 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java @@ -155,7 +155,7 @@ public abstract class HabitList implements Iterable * @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() { diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java index 790e9d945..094783ede 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java @@ -221,4 +221,6 @@ public abstract class RepetitionList add(new Repetition(timestamp, value)); habit.invalidateNewerThan(timestamp); } + + public abstract void removeAll(); } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryCheckmarkList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryCheckmarkList.java index 728ba3548..b6d3ee718 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryCheckmarkList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryCheckmarkList.java @@ -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 filtered = new LinkedList<>(); + List filtered = new ArrayList<>((int) days); for(long time = toTimestamp; time >= fromTimestamp; time -= millisecondsInOneDay) { if(time > newestTimestamp || time < oldestTimestamp) diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java index 58ec38fe0..f4cbbbf20 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java @@ -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 list; + private LinkedList list = new LinkedList<>(); private Comparator 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 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 getComparatorByOrder(Order order) + { + Comparator nameComparator = + (h1, h2) -> h1.getName().compareTo(h2.getName()); + + Comparator 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 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 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 getComparatorByOrder(Order order) + private void throwIfHasParent() { - Comparator nameComparator = - (h1, h2) -> h1.getName().compareTo(h2.getName()); - - Comparator 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 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); } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryRepetitionList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryRepetitionList.java index 2d7c81fea..0d63b0529 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryRepetitionList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryRepetitionList.java @@ -30,12 +30,12 @@ import java.util.*; */ public class MemoryRepetitionList extends RepetitionList { - LinkedList list; + ArrayList 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 getByInterval(long fromTimestamp, long toTimestamp) { - LinkedList filtered = new LinkedList<>(); + ArrayList 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(); + } } diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitListTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitListTest.java index 06a560763..f5cb6218e 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitListTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitListTest.java @@ -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 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); } } \ No newline at end of file