mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 01:08:50 -06:00
Merge branch 'feature/raw-sqlite' into dev
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
@@ -32,6 +31,7 @@ dependencies {
|
||||
implementation 'com.google.dagger:dagger:2.9'
|
||||
implementation 'com.android.support:design:25.3.1'
|
||||
implementation 'com.android.support:appcompat-v7:25.3.1'
|
||||
implementation 'org.apache.commons:commons-lang3:3.5'
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.9'
|
||||
androidTestAnnotationProcessor 'com.google.dagger:dagger-compiler:2.9'
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.storage;
|
||||
|
||||
import android.content.*;
|
||||
import android.database.sqlite.*;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
public class BaseSQLiteOpenHelper extends SQLiteOpenHelper
|
||||
{
|
||||
private final Context context;
|
||||
|
||||
private final int version;
|
||||
|
||||
public BaseSQLiteOpenHelper(@AppContext Context context,
|
||||
String databaseFilename,
|
||||
int version)
|
||||
{
|
||||
super(context, databaseFilename, null, version);
|
||||
this.context = context;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db)
|
||||
{
|
||||
executeMigrations(db, -1, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
||||
{
|
||||
executeMigrations(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
private void executeMigrations(SQLiteDatabase db,
|
||||
int oldVersion,
|
||||
int newVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (int v = oldVersion + 1; v <= newVersion; v++)
|
||||
{
|
||||
String fname = String.format(Locale.US, "migrations/%d.sql", v);
|
||||
InputStream stream = context.getAssets().open(fname);
|
||||
for (String command : SQLParser.parse(stream))
|
||||
db.execSQL(command);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
||||
{
|
||||
throw new UnsupportedDatabaseVersionException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.storage;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Column
|
||||
{
|
||||
String name() default "";
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Markus Pfeiffer
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.storage;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class Tokenizer {
|
||||
|
||||
private final InputStream mStream;
|
||||
|
||||
private boolean mIsNext;
|
||||
private int mCurrent;
|
||||
|
||||
public Tokenizer(final InputStream in) {
|
||||
this.mStream = in;
|
||||
}
|
||||
|
||||
public boolean hasNext() throws IOException {
|
||||
|
||||
if (!this.mIsNext) {
|
||||
this.mIsNext = true;
|
||||
this.mCurrent = this.mStream.read();
|
||||
}
|
||||
return this.mCurrent != -1;
|
||||
}
|
||||
|
||||
public int next() throws IOException {
|
||||
|
||||
if (!this.mIsNext) {
|
||||
this.mCurrent = this.mStream.read();
|
||||
}
|
||||
this.mIsNext = false;
|
||||
return this.mCurrent;
|
||||
}
|
||||
|
||||
public boolean skip(final String s) throws IOException {
|
||||
|
||||
if (s == null || s.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s.charAt(0) != this.mCurrent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int len = s.length();
|
||||
this.mStream.mark(len - 1);
|
||||
|
||||
for (int n = 1; n < len; n++) {
|
||||
final int value = this.mStream.read();
|
||||
|
||||
if (value != s.charAt(n)) {
|
||||
this.mStream.reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class SQLParser {
|
||||
|
||||
public final static int STATE_NONE = 0;
|
||||
public final static int STATE_STRING = 1;
|
||||
public final static int STATE_COMMENT = 2;
|
||||
public final static int STATE_COMMENT_BLOCK = 3;
|
||||
|
||||
public static List<String> parse(final InputStream stream) throws IOException {
|
||||
|
||||
final BufferedInputStream buffer = new BufferedInputStream(stream);
|
||||
final List<String> commands = new ArrayList<String>();
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
|
||||
try {
|
||||
final Tokenizer tokenizer = new Tokenizer(buffer);
|
||||
int state = STATE_NONE;
|
||||
|
||||
while (tokenizer.hasNext()) {
|
||||
final char c = (char) tokenizer.next();
|
||||
|
||||
if (state == STATE_COMMENT_BLOCK) {
|
||||
if (tokenizer.skip("*/")) {
|
||||
state = STATE_NONE;
|
||||
}
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_COMMENT) {
|
||||
if (isNewLine(c)) {
|
||||
state = STATE_NONE;
|
||||
}
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && tokenizer.skip("/*")) {
|
||||
state = STATE_COMMENT_BLOCK;
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && tokenizer.skip("--")) {
|
||||
state = STATE_COMMENT;
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && c == ';') {
|
||||
final String command = sb.toString().trim();
|
||||
commands.add(command);
|
||||
sb.setLength(0);
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && c == '\'') {
|
||||
state = STATE_STRING;
|
||||
|
||||
} else if (state == STATE_STRING && c == '\'') {
|
||||
state = STATE_NONE;
|
||||
|
||||
}
|
||||
|
||||
if (state == STATE_NONE || state == STATE_STRING) {
|
||||
if (state == STATE_NONE && isWhitespace(c)) {
|
||||
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
|
||||
sb.append(' ');
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
buffer.close();
|
||||
}
|
||||
|
||||
if (sb.length() > 0) {
|
||||
commands.add(sb.toString().trim());
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
private static boolean isNewLine(final char c) {
|
||||
return c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
private static boolean isWhitespace(final char c) {
|
||||
return c == '\r' || c == '\n' || c == '\t' || c == ' ';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.storage;
|
||||
|
||||
import android.content.*;
|
||||
import android.database.*;
|
||||
import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.apache.commons.lang3.*;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
|
||||
public class SQLiteRepository<T>
|
||||
{
|
||||
@NonNull
|
||||
private final Class klass;
|
||||
|
||||
@NonNull
|
||||
private final SQLiteDatabase db;
|
||||
|
||||
public SQLiteRepository(@NonNull Class<T> klass, @NonNull SQLiteDatabase db)
|
||||
{
|
||||
this.klass = klass;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T find(@NonNull Long id)
|
||||
{
|
||||
return findFirst(String.format("where %s=?", getIdName()),
|
||||
id.toString());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<T> findAll(String query, String... params)
|
||||
{
|
||||
try (Cursor c = db.rawQuery(buildSelectQuery() + query, params))
|
||||
{
|
||||
return cursorToMultipleRecords(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T findFirst(String query, String... params)
|
||||
{
|
||||
try (Cursor c = db.rawQuery(buildSelectQuery() + query, params))
|
||||
{
|
||||
if (!c.moveToNext()) return null;
|
||||
return cursorToSingleRecord(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void execSQL(String query, Object... params)
|
||||
{
|
||||
db.execSQL(query, params);
|
||||
}
|
||||
|
||||
public void save(T record)
|
||||
{
|
||||
try
|
||||
{
|
||||
Field fields[] = getFields();
|
||||
String columns[] = getColumnNames();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
for (int i = 0; i < fields.length; i++)
|
||||
fieldToContentValue(values, columns[i], fields[i], record);
|
||||
|
||||
Long id = (Long) getIdField().get(record);
|
||||
int affectedRows = 0;
|
||||
|
||||
if (id != null) affectedRows =
|
||||
db.update(getTableName(), values, getIdName() + "=?",
|
||||
new String[]{ id.toString() });
|
||||
|
||||
if (id == null || affectedRows == 0)
|
||||
{
|
||||
id = db.insertOrThrow(getTableName(), null, values);
|
||||
getIdField().set(record, id);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(T record)
|
||||
{
|
||||
try
|
||||
{
|
||||
Long id = (Long) getIdField().get(record);
|
||||
if (id == null) return;
|
||||
|
||||
db.delete(getTableName(), getIdName() + "=?",
|
||||
new String[]{ id.toString() });
|
||||
getIdField().set(record, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<T> cursorToMultipleRecords(Cursor c)
|
||||
{
|
||||
List<T> records = new LinkedList<>();
|
||||
while (c.moveToNext()) records.add(cursorToSingleRecord(c));
|
||||
return records;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private T cursorToSingleRecord(Cursor cursor)
|
||||
{
|
||||
try
|
||||
{
|
||||
Constructor constructor = klass.getDeclaredConstructors()[0];
|
||||
constructor.setAccessible(true);
|
||||
T record = (T) constructor.newInstance();
|
||||
|
||||
int index = 0;
|
||||
for (Field field : getFields())
|
||||
copyFieldFromCursor(record, field, cursor, index++);
|
||||
return record;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFieldFromCursor(T record, Field field, Cursor c, int index)
|
||||
throws IllegalAccessException
|
||||
{
|
||||
if (field.getType().isAssignableFrom(Integer.class))
|
||||
field.set(record, c.getInt(index));
|
||||
else if (field.getType().isAssignableFrom(Long.class))
|
||||
field.set(record, c.getLong(index));
|
||||
else if (field.getType().isAssignableFrom(Double.class))
|
||||
field.set(record, c.getDouble(index));
|
||||
else if (field.getType().isAssignableFrom(String.class))
|
||||
field.set(record, c.getString(index));
|
||||
else throw new RuntimeException(
|
||||
"Type not supported: " + field.getType().getName() + " " +
|
||||
field.getName());
|
||||
}
|
||||
|
||||
private void fieldToContentValue(ContentValues values,
|
||||
String columnName,
|
||||
Field field,
|
||||
T record)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (field.getType().isAssignableFrom(Integer.class))
|
||||
values.put(columnName, (Integer) field.get(record));
|
||||
else if (field.getType().isAssignableFrom(Long.class))
|
||||
values.put(columnName, (Long) field.get(record));
|
||||
else if (field.getType().isAssignableFrom(Double.class))
|
||||
values.put(columnName, (Double) field.get(record));
|
||||
else if (field.getType().isAssignableFrom(String.class))
|
||||
values.put(columnName, (String) field.get(record));
|
||||
else throw new RuntimeException(
|
||||
"Type not supported: " + field.getName());
|
||||
}
|
||||
catch (IllegalAccessException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSelectQuery()
|
||||
{
|
||||
return String.format("select %s from %s ",
|
||||
StringUtils.join(getColumnNames(), ", "), getTableName());
|
||||
}
|
||||
|
||||
private List<Pair<Field, Column>> getFieldColumnPairs()
|
||||
{
|
||||
List<Pair<Field, Column>> fields = new ArrayList<>();
|
||||
for (Field field : klass.getDeclaredFields())
|
||||
for (Annotation annotation : field.getAnnotations())
|
||||
{
|
||||
if (!(annotation instanceof Column)) continue;
|
||||
Column column = (Column) annotation;
|
||||
fields.add(new Pair<>(field, column));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Field[] getFields()
|
||||
{
|
||||
List<Field> fields = new ArrayList<>();
|
||||
List<Pair<Field, Column>> columns = getFieldColumnPairs();
|
||||
for (Pair<Field, Column> pair : columns) fields.add(pair.first);
|
||||
return fields.toArray(new Field[]{});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String[] getColumnNames()
|
||||
{
|
||||
List<String> names = new ArrayList<>();
|
||||
List<Pair<Field, Column>> columns = getFieldColumnPairs();
|
||||
for (Pair<Field, Column> pair : columns)
|
||||
{
|
||||
String cname = pair.second.name();
|
||||
if (cname.isEmpty()) cname = pair.first.getName();
|
||||
if (names.contains(cname))
|
||||
throw new RuntimeException("duplicated column : " + cname);
|
||||
names.add(cname);
|
||||
}
|
||||
|
||||
return names.toArray(new String[]{});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String getTableName()
|
||||
{
|
||||
String name = getTableAnnotation().name();
|
||||
if (name.isEmpty()) throw new RuntimeException("Table name is empty");
|
||||
return name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String getIdName()
|
||||
{
|
||||
String id = getTableAnnotation().id();
|
||||
if (id.isEmpty()) throw new RuntimeException("Table id is empty");
|
||||
return id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Field getIdField()
|
||||
{
|
||||
Field fields[] = getFields();
|
||||
String idName = getIdName();
|
||||
for (Field f : fields)
|
||||
if (f.getName().equals(idName)) return f;
|
||||
throw new RuntimeException("Field not found: " + idName);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Table getTableAnnotation()
|
||||
{
|
||||
Table t = null;
|
||||
for (Annotation annotation : klass.getAnnotations())
|
||||
{
|
||||
if (!(annotation instanceof Table)) continue;
|
||||
t = (Table) annotation;
|
||||
break;
|
||||
}
|
||||
|
||||
if (t == null) throw new RuntimeException("Table annotation not found");
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.storage;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Table
|
||||
{
|
||||
String name();
|
||||
String id() default "id";
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
@@ -15,13 +15,12 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.models.sqlite.records;
|
||||
package org.isoron.androidbase.storage;
|
||||
|
||||
import android.database.*;
|
||||
|
||||
public interface SQLiteRecord
|
||||
public class UnsupportedDatabaseVersionException extends RuntimeException
|
||||
{
|
||||
void copyFrom(Cursor c);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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'
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-4.0-20170417000025+0000-all.zip
|
||||
distributionUrl=http\://services.gradle.org/distributions/gradle-4.0-all.zip
|
||||
|
||||
@@ -64,7 +64,6 @@ dependencies {
|
||||
implementation 'com.github.paolorotolo:appintro:3.4.0'
|
||||
implementation 'com.google.dagger:dagger:2.9'
|
||||
implementation 'com.jakewharton:butterknife:8.6.1-SNAPSHOT'
|
||||
implementation 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
|
||||
implementation 'org.apmem.tools:layouts:1.10'
|
||||
implementation 'org.jetbrains:annotations-java5:15.0'
|
||||
implementation 'com.google.code.gson:gson:2.7'
|
||||
|
||||
@@ -115,9 +115,8 @@ public class HabitFixtures
|
||||
return habit;
|
||||
}
|
||||
|
||||
public void purgeHabits(HabitList habitList)
|
||||
public synchronized void purgeHabits(HabitList habitList)
|
||||
{
|
||||
for (Habit h : habitList)
|
||||
habitList.remove(h);
|
||||
habitList.removeAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,11 @@ package org.isoron.uhabits.models.sqlite;
|
||||
import android.support.test.runner.*;
|
||||
import android.test.suitebuilder.annotation.*;
|
||||
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.junit.*;
|
||||
import org.junit.runner.*;
|
||||
|
||||
@@ -35,25 +37,18 @@ import static org.hamcrest.core.IsEqual.*;
|
||||
@MediumTest
|
||||
public class HabitRecordTest extends BaseAndroidTest
|
||||
{
|
||||
private Habit habit;
|
||||
|
||||
private SQLiteRepository<HabitRecord> sqlite =
|
||||
new SQLiteRepository<>(HabitRecord.class, DatabaseUtils.openDatabase());
|
||||
|
||||
@Before
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
|
||||
Habit h = component.getModelFactory().buildHabit();
|
||||
h.setName("Hello world");
|
||||
h.setId(1000L);
|
||||
|
||||
HabitRecord record = new HabitRecord();
|
||||
record.copyFrom(h);
|
||||
record.position = 0;
|
||||
record.save(1000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyFrom()
|
||||
{
|
||||
Habit habit = component.getModelFactory().buildHabit();
|
||||
habit = component.getModelFactory().buildHabit();
|
||||
habit.setName("Hello world");
|
||||
habit.setDescription("Did you greet the world today?");
|
||||
habit.setColor(1);
|
||||
@@ -61,7 +56,11 @@ public class HabitRecordTest extends BaseAndroidTest
|
||||
habit.setFrequency(Frequency.THREE_TIMES_PER_WEEK);
|
||||
habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY));
|
||||
habit.setId(1000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyFrom()
|
||||
{
|
||||
HabitRecord rec = new HabitRecord();
|
||||
rec.copyFrom(habit);
|
||||
|
||||
|
||||
@@ -22,12 +22,13 @@ package org.isoron.uhabits.models.sqlite;
|
||||
import android.support.test.runner.*;
|
||||
import android.test.suitebuilder.annotation.*;
|
||||
|
||||
import com.activeandroid.query.*;
|
||||
import com.google.common.collect.*;
|
||||
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.models.sqlite.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.junit.*;
|
||||
import org.junit.rules.*;
|
||||
import org.junit.runner.*;
|
||||
@@ -36,6 +37,8 @@ import java.util.*;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.*;
|
||||
import static org.hamcrest.core.IsEqual.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@SuppressWarnings("JavaDoc")
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -49,6 +52,10 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
|
||||
private ModelFactory modelFactory;
|
||||
|
||||
private SQLiteRepository<HabitRecord> repository;
|
||||
|
||||
private ModelObservable.Listener listener;
|
||||
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
@@ -57,6 +64,9 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
fixtures.purgeHabits(habitList);
|
||||
|
||||
modelFactory = component.getModelFactory();
|
||||
repository =
|
||||
new SQLiteRepository<>(HabitRecord.class,
|
||||
DatabaseUtils.openDatabase());
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
@@ -68,8 +78,20 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
HabitRecord record = new HabitRecord();
|
||||
record.copyFrom(h);
|
||||
record.position = i;
|
||||
record.save(i);
|
||||
repository.save(record);
|
||||
}
|
||||
|
||||
habitList.reload();
|
||||
|
||||
listener = mock(ModelObservable.Listener.class);
|
||||
habitList.getObservable().addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception
|
||||
{
|
||||
habitList.getObservable().removeListener(listener);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,6 +99,8 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
{
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
habitList.add(habit);
|
||||
verify(listener).onModelChange();
|
||||
|
||||
exception.expect(IllegalArgumentException.class);
|
||||
habitList.add(habit);
|
||||
}
|
||||
@@ -91,7 +115,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
habitList.add(habit);
|
||||
assertThat(habit.getId(), equalTo(12300L));
|
||||
|
||||
HabitRecord record = getRecord(12300L);
|
||||
HabitRecord record = repository.find(12300L);
|
||||
assertNotNull(record);
|
||||
assertThat(record.name, equalTo(habit.getName()));
|
||||
}
|
||||
@@ -106,7 +130,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
habitList.add(habit);
|
||||
assertNotNull(habit.getId());
|
||||
|
||||
HabitRecord record = getRecord(habit.getId());
|
||||
HabitRecord record = repository.find(habit.getId());
|
||||
assertNotNull(record);
|
||||
assertThat(record.name, equalTo(habit.getName()));
|
||||
}
|
||||
@@ -120,7 +144,7 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
@Test
|
||||
public void testGetAll_withArchived()
|
||||
{
|
||||
List<Habit> habits = habitList.toList();
|
||||
List<Habit> habits = Lists.newArrayList(habitList.iterator());
|
||||
assertThat(habits.size(), equalTo(10));
|
||||
assertThat(habits.get(3).getName(), equalTo("habit 3"));
|
||||
}
|
||||
@@ -166,12 +190,4 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
h2.setId(1000L);
|
||||
assertThat(habitList.indexOf(h2), equalTo(-1));
|
||||
}
|
||||
|
||||
private HabitRecord getRecord(long id)
|
||||
{
|
||||
return new Select()
|
||||
.from(HabitRecord.class)
|
||||
.where("id = ?", id)
|
||||
.executeSingle();
|
||||
}
|
||||
}
|
||||
@@ -23,19 +23,19 @@ import android.support.annotation.*;
|
||||
import android.support.test.runner.*;
|
||||
import android.test.suitebuilder.annotation.*;
|
||||
|
||||
import com.activeandroid.query.*;
|
||||
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.junit.*;
|
||||
import org.junit.runner.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.hamcrest.core.IsEqual.*;
|
||||
import static org.isoron.uhabits.core.models.Checkmark.CHECKED_EXPLICITLY;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -50,6 +50,8 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
|
||||
|
||||
private long day;
|
||||
|
||||
private SQLiteRepository<RepetitionRecord> sqlite;
|
||||
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
@@ -59,6 +61,8 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
|
||||
repetitions = habit.getRepetitions();
|
||||
today = DateUtils.getStartOfToday();
|
||||
day = DateUtils.millisecondsInOneDay;
|
||||
sqlite = new SQLiteRepository<>(RepetitionRecord.class,
|
||||
DatabaseUtils.openDatabase());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -130,15 +134,13 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest
|
||||
@Nullable
|
||||
private RepetitionRecord getByTimestamp(long timestamp)
|
||||
{
|
||||
return selectByTimestamp(timestamp).executeSingle();
|
||||
}
|
||||
String query = "where habit = ? and timestamp = ?";
|
||||
|
||||
@NonNull
|
||||
private From selectByTimestamp(long timestamp)
|
||||
{
|
||||
return new Select()
|
||||
.from(RepetitionRecord.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp = ?", timestamp);
|
||||
String params[] = {
|
||||
Long.toString(habit.getId()),
|
||||
Long.toString(timestamp)
|
||||
};
|
||||
|
||||
return sqlite.findFirst(query, params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.models.sqlite;
|
||||
|
||||
import android.database.sqlite.*;
|
||||
import android.support.test.runner.*;
|
||||
import android.test.suitebuilder.annotation.*;
|
||||
|
||||
import org.apache.commons.lang3.builder.*;
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.junit.*;
|
||||
import org.junit.runner.*;
|
||||
|
||||
import static org.hamcrest.core.IsEqual.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@MediumTest
|
||||
public class SQLiteRepositoryTest extends BaseAndroidTest
|
||||
{
|
||||
private SQLiteRepository<ThingRecord> repository;
|
||||
|
||||
private SQLiteDatabase db;
|
||||
|
||||
@Before
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
this.db = DatabaseUtils.openDatabase();
|
||||
repository = new SQLiteRepository<>(ThingRecord.class, db);
|
||||
|
||||
db.execSQL("drop table if exists tests");
|
||||
db.execSQL("create table tests(" +
|
||||
"id integer not null primary key autoincrement, " +
|
||||
"color_number integer not null, score float not null, " +
|
||||
"name string not null)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFind() throws Exception
|
||||
{
|
||||
db.execSQL("insert into tests(id, color_number, name, score) " +
|
||||
"values (10, 20, 'hello', 8.0)");
|
||||
|
||||
ThingRecord record = repository.find(10L);
|
||||
|
||||
assertNotNull(record);
|
||||
assertThat(record.id, equalTo(10L));
|
||||
assertThat(record.color, equalTo(20));
|
||||
assertThat(record.name, equalTo("hello"));
|
||||
assertThat(record.score, equalTo(8.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSave_withId() throws Exception
|
||||
{
|
||||
ThingRecord record = new ThingRecord();
|
||||
record.id = 50L;
|
||||
record.color = 10;
|
||||
record.name = "hello";
|
||||
record.score = 5.0;
|
||||
repository.save(record);
|
||||
assertThat(record, equalTo(repository.find(50L)));
|
||||
|
||||
record.name = "world";
|
||||
record.score = 128.0;
|
||||
repository.save(record);
|
||||
assertThat(record, equalTo(repository.find(50L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSave_withoutId() throws Exception
|
||||
{
|
||||
ThingRecord r1 = new ThingRecord();
|
||||
r1.color = 10;
|
||||
r1.name = "hello";
|
||||
r1.score = 16.0;
|
||||
repository.save(r1);
|
||||
|
||||
ThingRecord r2 = new ThingRecord();
|
||||
r2.color = 20;
|
||||
r2.name = "world";
|
||||
r2.score = 2.0;
|
||||
repository.save(r2);
|
||||
|
||||
assertThat(r1.id, equalTo(1L));
|
||||
assertThat(r2.id, equalTo(2L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() throws Exception
|
||||
{
|
||||
ThingRecord rec1 = new ThingRecord();
|
||||
rec1.color = 10;
|
||||
rec1.name = "hello";
|
||||
rec1.score = 16.0;
|
||||
repository.save(rec1);
|
||||
|
||||
ThingRecord rec2 = new ThingRecord();
|
||||
rec2.color = 20;
|
||||
rec2.name = "world";
|
||||
rec2.score = 32.0;
|
||||
repository.save(rec2);
|
||||
|
||||
long id = rec1.id;
|
||||
assertThat(rec1, equalTo(repository.find(id)));
|
||||
assertThat(rec2, equalTo(repository.find(rec2.id)));
|
||||
|
||||
repository.remove(rec1);
|
||||
assertThat(rec1.id, equalTo(null));
|
||||
assertNull(repository.find(id));
|
||||
assertThat(rec2, equalTo(repository.find(rec2.id)));
|
||||
|
||||
repository.remove(rec1); // should have no effect
|
||||
assertNull(repository.find(id));
|
||||
}
|
||||
}
|
||||
|
||||
@Table(name = "tests")
|
||||
class ThingRecord
|
||||
{
|
||||
@Column
|
||||
public Long id;
|
||||
|
||||
@Column
|
||||
public String name;
|
||||
|
||||
@Column(name = "color_number")
|
||||
public Integer color;
|
||||
|
||||
@Column
|
||||
public Double score;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
ThingRecord record = (ThingRecord) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.append(id, record.id)
|
||||
.append(name, record.name)
|
||||
.append(color, record.color)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.append(id)
|
||||
.append(name)
|
||||
.append(color)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this)
|
||||
.append("id", id)
|
||||
.append("name", name)
|
||||
.append("color", color)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
delete from Score;
|
||||
delete from Streak;
|
||||
delete from Checkmarks;
|
||||
delete from Checkmarks;
|
||||
@@ -1 +1 @@
|
||||
alter table habits add column reminder_days integer not null default 127;
|
||||
alter table Habits add column reminder_days integer not null default 127;
|
||||
@@ -1,4 +1,4 @@
|
||||
create index idx_score_habit_timestamp on score(habit, timestamp);
|
||||
create index idx_checkmark_habit_timestamp on checkmarks(habit, timestamp);
|
||||
create index idx_repetitions_habit_timestamp on repetitions(habit, timestamp);
|
||||
create index idx_streak_habit_end on streak(habit, end);
|
||||
create index idx_score_habit_timestamp on Score(habit, timestamp);
|
||||
create index idx_checkmark_habit_timestamp on Checkmarks(habit, timestamp);
|
||||
create index idx_repetitions_habit_timestamp on Repetitions(habit, timestamp);
|
||||
create index idx_streak_habit_end on Streak(habit, end);
|
||||
@@ -1,5 +1,11 @@
|
||||
DROP TABLE Score;
|
||||
CREATE TABLE Score (Id INTEGER PRIMARY KEY AUTOINCREMENT, habit INTEGER REFERENCES Habits(Id), score REAL, timestamp INTEGER);
|
||||
CREATE INDEX idx_score_habit_timestamp on score(habit, timestamp);
|
||||
delete from Streak;
|
||||
delete from Checkmarks;
|
||||
drop table Score;
|
||||
create table Score (
|
||||
id integer primary key autoincrement,
|
||||
habit integer references habits(id),
|
||||
score real,
|
||||
timestamp integer);
|
||||
|
||||
create index idx_score_habit_timestamp on Score(habit, timestamp);
|
||||
|
||||
delete from streak;
|
||||
delete from checkmarks;
|
||||
6
uhabits-android/src/main/assets/migrations/19.sql
Normal file
6
uhabits-android/src/main/assets/migrations/19.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
create table Events (
|
||||
id integer primary key autoincrement,
|
||||
timestamp integer,
|
||||
message text,
|
||||
server_id integer
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
alter table habits add column reminder_hour integer;
|
||||
alter table habits add column reminder_min integer;
|
||||
@@ -1 +0,0 @@
|
||||
alter table habits add column highlight integer not null default 0;
|
||||
@@ -1 +0,0 @@
|
||||
alter table habits add column archived integer not null default 0;
|
||||
41
uhabits-android/src/main/assets/migrations/9.sql
Normal file
41
uhabits-android/src/main/assets/migrations/9.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
create table Habits (
|
||||
id integer primary key autoincrement,
|
||||
archived integer,
|
||||
color integer,
|
||||
description text,
|
||||
freq_den integer,
|
||||
freq_num integer,
|
||||
highlight integer,
|
||||
name text,
|
||||
position integer,
|
||||
reminder_hour integer,
|
||||
reminder_min integer
|
||||
);
|
||||
|
||||
create table Checkmarks (
|
||||
id integer primary key autoincrement,
|
||||
habit integer references habits(id),
|
||||
timestamp integer,
|
||||
value integer
|
||||
);
|
||||
|
||||
create table Repetitions (
|
||||
id integer primary key autoincrement,
|
||||
habit integer references habits(id),
|
||||
timestamp integer
|
||||
);
|
||||
|
||||
create table Streak (
|
||||
id integer primary key autoincrement,
|
||||
end integer,
|
||||
habit integer references habits(id),
|
||||
length integer,
|
||||
start integer
|
||||
);
|
||||
|
||||
create table Score (
|
||||
id integer primary key autoincrement,
|
||||
habit integer references habits(id),
|
||||
score integer,
|
||||
timestamp integer
|
||||
);
|
||||
@@ -22,8 +22,6 @@ package org.isoron.uhabits;
|
||||
import android.app.*;
|
||||
import android.content.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.reminders.*;
|
||||
@@ -92,13 +90,13 @@ public class HabitsApplication extends Application
|
||||
|
||||
try
|
||||
{
|
||||
DatabaseUtils.initializeActiveAndroid(context);
|
||||
DatabaseUtils.initializeDatabase(context);
|
||||
}
|
||||
catch (InvalidDatabaseVersionException e)
|
||||
{
|
||||
File db = DatabaseUtils.getDatabaseFile(context);
|
||||
db.renameTo(new File(db.getAbsolutePath() + ".invalid"));
|
||||
DatabaseUtils.initializeActiveAndroid(context);
|
||||
DatabaseUtils.initializeDatabase(context);
|
||||
}
|
||||
|
||||
widgetUpdater = component.getWidgetUpdater();
|
||||
@@ -124,8 +122,6 @@ public class HabitsApplication extends Application
|
||||
public void onTerminate()
|
||||
{
|
||||
context = null;
|
||||
ActiveAndroid.dispose();
|
||||
|
||||
reminderScheduler.stopListening();
|
||||
widgetUpdater.stopListening();
|
||||
notificationTray.stopListening();
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits;
|
||||
|
||||
import android.content.*;
|
||||
import android.database.sqlite.*;
|
||||
|
||||
import org.isoron.androidbase.storage.*;
|
||||
|
||||
|
||||
public class HabitsDatabaseOpener extends BaseSQLiteOpenHelper
|
||||
{
|
||||
private final int version;
|
||||
|
||||
public HabitsDatabaseOpener(Context context,
|
||||
String databaseFilename,
|
||||
int version)
|
||||
{
|
||||
super(context, databaseFilename, version);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db)
|
||||
{
|
||||
onUpgrade(db, 8, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
||||
{
|
||||
if(oldVersion < 8) throw new UnsupportedDatabaseVersionException();
|
||||
super.onUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ package org.isoron.uhabits.io;
|
||||
|
||||
import android.support.annotation.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
import com.opencsv.*;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
@@ -32,6 +31,8 @@ import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import static org.isoron.uhabits.utils.DatabaseUtils.executeAsTransaction;
|
||||
|
||||
/**
|
||||
* Class that imports data from HabitBull CSV files.
|
||||
*/
|
||||
@@ -59,16 +60,7 @@ public class HabitBullCSVImporter extends AbstractImporter
|
||||
@Override
|
||||
public void importHabitsFromFile(@NonNull final File file) throws IOException
|
||||
{
|
||||
ActiveAndroid.beginTransaction();
|
||||
try
|
||||
{
|
||||
parseFile(file);
|
||||
ActiveAndroid.setTransactionSuccessful();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
}
|
||||
executeAsTransaction(() -> parseFile(file));
|
||||
}
|
||||
|
||||
private void parseFile(@NonNull File file) throws IOException
|
||||
|
||||
@@ -25,8 +25,6 @@ import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.BuildConfig;
|
||||
@@ -89,9 +87,9 @@ public class LoopDBImporter extends AbstractImporter
|
||||
@Override
|
||||
public void importHabitsFromFile(@NonNull File file) throws IOException
|
||||
{
|
||||
ActiveAndroid.dispose();
|
||||
DatabaseUtils.dispose();
|
||||
File originalDB = DatabaseUtils.getDatabaseFile(context);
|
||||
FileUtils.copy(file, originalDB);
|
||||
DatabaseUtils.initializeActiveAndroid(context);
|
||||
DatabaseUtils.initializeDatabase(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ public class SQLModelFactory implements ModelFactory
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
public static HabitList provideHabitList()
|
||||
public static HabitList provideHabitList(ModelFactory modelFactory)
|
||||
{
|
||||
return SQLiteHabitList.getInstance(provideModelFactory());
|
||||
return new SQLiteHabitList(modelFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,55 +19,60 @@
|
||||
|
||||
package org.isoron.uhabits.models.sqlite;
|
||||
|
||||
import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import com.activeandroid.query.*;
|
||||
import com.activeandroid.util.*;
|
||||
|
||||
import org.apache.commons.lang3.*;
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.models.memory.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.uhabits.utils.DatabaseUtils.executeAsTransaction;
|
||||
|
||||
/**
|
||||
* Implementation of a {@link HabitList} that is backed by SQLite.
|
||||
*/
|
||||
public class SQLiteHabitList extends HabitList
|
||||
{
|
||||
private static HashMap<Long, Habit> cache;
|
||||
|
||||
private static SQLiteHabitList instance;
|
||||
|
||||
@NonNull
|
||||
private final SQLiteUtils<HabitRecord> sqlite;
|
||||
private final SQLiteRepository<HabitRecord> repository;
|
||||
|
||||
@NonNull
|
||||
private final ModelFactory modelFactory;
|
||||
|
||||
@NonNull
|
||||
private Order order;
|
||||
private final MemoryHabitList list;
|
||||
|
||||
private boolean loaded = false;
|
||||
|
||||
public SQLiteHabitList(@NonNull ModelFactory modelFactory)
|
||||
{
|
||||
super();
|
||||
this.modelFactory = modelFactory;
|
||||
this.list = new MemoryHabitList();
|
||||
|
||||
if (cache == null) cache = new HashMap<>();
|
||||
sqlite = new SQLiteUtils<>(HabitRecord.class);
|
||||
order = Order.BY_POSITION;
|
||||
repository =
|
||||
new SQLiteRepository<>(HabitRecord.class, DatabaseUtils.openDatabase());
|
||||
}
|
||||
|
||||
protected SQLiteHabitList(@NonNull ModelFactory modelFactory,
|
||||
@NonNull HabitMatcher filter,
|
||||
@NonNull Order order)
|
||||
private void loadRecords()
|
||||
{
|
||||
super(filter);
|
||||
this.modelFactory = modelFactory;
|
||||
if(loaded) return;
|
||||
loaded = true;
|
||||
|
||||
if (cache == null) cache = new HashMap<>();
|
||||
sqlite = new SQLiteUtils<>(HabitRecord.class);
|
||||
this.order = order;
|
||||
list.removeAll();
|
||||
List<HabitRecord> records = repository.findAll("order by position");
|
||||
for (HabitRecord rec : records)
|
||||
{
|
||||
Habit h = modelFactory.buildHabit();
|
||||
rec.copyTo(h);
|
||||
list.add(h);
|
||||
}
|
||||
}
|
||||
|
||||
public static SQLiteHabitList getInstance(
|
||||
@@ -78,127 +83,123 @@ public class SQLiteHabitList extends HabitList
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(@NonNull Habit habit)
|
||||
public synchronized void add(@NonNull Habit habit)
|
||||
{
|
||||
if (cache.containsValue(habit))
|
||||
throw new IllegalArgumentException("habit already added");
|
||||
loadRecords();
|
||||
list.add(habit);
|
||||
|
||||
HabitRecord record = new HabitRecord();
|
||||
record.copyFrom(habit);
|
||||
record.position = size();
|
||||
record.position = list.indexOf(habit);
|
||||
repository.save(record);
|
||||
|
||||
Long id = habit.getId();
|
||||
if (id == null) id = record.save();
|
||||
else record.save(id);
|
||||
|
||||
if (id < 0)
|
||||
throw new IllegalArgumentException("habit could not be saved");
|
||||
|
||||
habit.setId(id);
|
||||
cache.put(id, habit);
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Habit getById(long id)
|
||||
{
|
||||
if (!cache.containsKey(id))
|
||||
{
|
||||
HabitRecord record = HabitRecord.get(id);
|
||||
if (record == null) return null;
|
||||
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
record.copyTo(habit);
|
||||
cache.put(id, habit);
|
||||
}
|
||||
|
||||
return cache.get(id);
|
||||
loadRecords();
|
||||
return list.getById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Habit getByPosition(int position)
|
||||
{
|
||||
return toList().get(position);
|
||||
loadRecords();
|
||||
return list.getByPosition(position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public HabitList getFiltered(HabitMatcher filter)
|
||||
{
|
||||
return new SQLiteHabitList(modelFactory, filter, order);
|
||||
loadRecords();
|
||||
return list.getFiltered(filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Order getOrder()
|
||||
{
|
||||
return order;
|
||||
return list.getOrder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOrder(@NonNull Order order)
|
||||
{
|
||||
this.order = order;
|
||||
list.setOrder(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(@NonNull Habit h)
|
||||
{
|
||||
return toList().indexOf(h);
|
||||
loadRecords();
|
||||
return list.indexOf(h);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Habit> iterator()
|
||||
{
|
||||
return Collections.unmodifiableCollection(toList()).iterator();
|
||||
loadRecords();
|
||||
return list.iterator();
|
||||
}
|
||||
|
||||
public void rebuildOrder()
|
||||
private void rebuildOrder()
|
||||
{
|
||||
List<Habit> habits = toList();
|
||||
|
||||
int i = 0;
|
||||
for (Habit h : habits)
|
||||
{
|
||||
HabitRecord record = HabitRecord.get(h.getId());
|
||||
if (record == null)
|
||||
throw new RuntimeException("habit not in database");
|
||||
|
||||
record.position = i++;
|
||||
record.save();
|
||||
}
|
||||
|
||||
update(habits);
|
||||
// List<Habit> habits = toList();
|
||||
//
|
||||
// int i = 0;
|
||||
// for (Habit h : habits)
|
||||
// {
|
||||
// HabitRecord record = repository.find(h.getId());
|
||||
// if (record == null)
|
||||
// throw new RuntimeException("habit not in database");
|
||||
//
|
||||
// record.position = i++;
|
||||
// repository.save(record);
|
||||
// }
|
||||
//
|
||||
// update(habits);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NonNull Habit habit)
|
||||
public synchronized void remove(@NonNull Habit habit)
|
||||
{
|
||||
if (!cache.containsKey(habit.getId()))
|
||||
throw new RuntimeException("habit not in cache");
|
||||
loadRecords();
|
||||
list.remove(habit);
|
||||
|
||||
cache.remove(habit.getId());
|
||||
HabitRecord record = HabitRecord.get(habit.getId());
|
||||
HabitRecord record = repository.find(habit.getId());
|
||||
if (record == null) throw new RuntimeException("habit not in database");
|
||||
record.cascadeDelete();
|
||||
executeAsTransaction(() ->
|
||||
{
|
||||
((SQLiteRepetitionList) habit.getRepetitions()).removeAll();
|
||||
repository.remove(record);
|
||||
});
|
||||
rebuildOrder();
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll()
|
||||
public synchronized void removeAll()
|
||||
{
|
||||
sqlite.query("delete from repetitions", null);
|
||||
sqlite.query("delete from habits", null);
|
||||
list.removeAll();
|
||||
SQLiteDatabase db = DatabaseUtils.openDatabase();
|
||||
db.execSQL("delete from habits");
|
||||
db.execSQL("delete from repetitions");
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reorder(Habit from, Habit to)
|
||||
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
|
||||
{
|
||||
if (from == to) return;
|
||||
loadRecords();
|
||||
list.reorder(from, to);
|
||||
|
||||
HabitRecord fromRecord = HabitRecord.get(from.getId());
|
||||
HabitRecord toRecord = HabitRecord.get(to.getId());
|
||||
HabitRecord fromRecord = repository.find(from.getId());
|
||||
HabitRecord toRecord = repository.find(to.getId());
|
||||
|
||||
if (fromRecord == null)
|
||||
throw new RuntimeException("habit not in database");
|
||||
@@ -207,128 +208,59 @@ public class SQLiteHabitList extends HabitList
|
||||
|
||||
Integer fromPos = fromRecord.position;
|
||||
Integer toPos = toRecord.position;
|
||||
|
||||
Log.d("SQLiteHabitList",
|
||||
String.format("reorder: %d %d", fromPos, toPos));
|
||||
|
||||
SQLiteDatabase db = DatabaseUtils.openDatabase();
|
||||
if (toPos < fromPos)
|
||||
{
|
||||
new Update(HabitRecord.class)
|
||||
.set("position = position + 1")
|
||||
.where("position >= ? and position < ?", toPos, fromPos)
|
||||
.execute();
|
||||
db.execSQL("update habits set position = position + 1 " +
|
||||
"where position >= ? and position < ?",
|
||||
new String[]{ toPos.toString(), fromPos.toString() });
|
||||
}
|
||||
else
|
||||
{
|
||||
new Update(HabitRecord.class)
|
||||
.set("position = position - 1")
|
||||
.where("position > ? and position <= ?", fromPos, toPos)
|
||||
.execute();
|
||||
db.execSQL("update habits set position = position - 1 " +
|
||||
"where position > ? and position <= ?",
|
||||
new String[]{ fromPos.toString(), toPos.toString() });
|
||||
}
|
||||
|
||||
fromRecord.position = toPos;
|
||||
fromRecord.save();
|
||||
repository.save(fromRecord);
|
||||
update(from);
|
||||
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void repair()
|
||||
{
|
||||
super.repair();
|
||||
loadRecords();
|
||||
rebuildOrder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
return toList().size();
|
||||
loadRecords();
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(List<Habit> habits)
|
||||
public synchronized void update(List<Habit> habits)
|
||||
{
|
||||
loadRecords();
|
||||
for (Habit h : habits)
|
||||
{
|
||||
HabitRecord record = HabitRecord.get(h.getId());
|
||||
HabitRecord record = repository.find(h.getId());
|
||||
if (record == null)
|
||||
throw new RuntimeException("habit not in database");
|
||||
record.copyFrom(h);
|
||||
record.save();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized List<Habit> toList()
|
||||
{
|
||||
String query = buildSelectQuery();
|
||||
List<HabitRecord> recordList = sqlite.query(query, null);
|
||||
|
||||
List<Habit> habits = new LinkedList<>();
|
||||
for (HabitRecord record : recordList)
|
||||
{
|
||||
Habit habit = getById(record.getId());
|
||||
if (habit == null) continue;
|
||||
if (!filter.matches(habit)) continue;
|
||||
habits.add(habit);
|
||||
repository.save(record);
|
||||
}
|
||||
|
||||
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;
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
private void appendOrderBy(StringBuilder query)
|
||||
public void reload()
|
||||
{
|
||||
switch (order)
|
||||
{
|
||||
case BY_POSITION:
|
||||
query.append("order by position ");
|
||||
break;
|
||||
|
||||
case BY_NAME:
|
||||
case BY_SCORE:
|
||||
query.append("order by name ");
|
||||
break;
|
||||
|
||||
case BY_COLOR:
|
||||
query.append("order by color, name ");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSelect(StringBuilder query)
|
||||
{
|
||||
query.append(HabitRecord.SELECT);
|
||||
}
|
||||
|
||||
private void appendWhere(StringBuilder query)
|
||||
{
|
||||
ArrayList<Object> where = new ArrayList<>();
|
||||
if (filter.isReminderRequired()) where.add("reminder_hour is not null");
|
||||
if (!filter.isArchivedAllowed()) where.add("archived = 0");
|
||||
|
||||
if (where.isEmpty()) return;
|
||||
query.append("where ");
|
||||
query.append(StringUtils.join(where, " and "));
|
||||
query.append(" ");
|
||||
}
|
||||
|
||||
private String buildSelectQuery()
|
||||
{
|
||||
StringBuilder query = new StringBuilder();
|
||||
appendSelect(query);
|
||||
appendWhere(query);
|
||||
appendOrderBy(query);
|
||||
return query.toString();
|
||||
loaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,14 @@
|
||||
|
||||
package org.isoron.uhabits.models.sqlite;
|
||||
|
||||
import android.database.*;
|
||||
import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.activeandroid.*;
|
||||
import com.activeandroid.query.*;
|
||||
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.models.memory.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.util.*;
|
||||
@@ -38,165 +36,112 @@ import java.util.*;
|
||||
*/
|
||||
public class SQLiteRepetitionList extends RepetitionList
|
||||
{
|
||||
private final SQLiteRepository<RepetitionRecord> repository;
|
||||
|
||||
private final SQLiteUtils<RepetitionRecord> sqlite;
|
||||
private final MemoryRepetitionList list;
|
||||
|
||||
@Nullable
|
||||
private HabitRecord habitRecord;
|
||||
|
||||
private SQLiteStatement addStatement;
|
||||
|
||||
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);
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
addStatement = db.compileStatement(ADD_QUERY);
|
||||
repository = new SQLiteRepository<>(RepetitionRecord.class,
|
||||
DatabaseUtils.openDatabase());
|
||||
list = new MemoryRepetitionList(habit);
|
||||
}
|
||||
|
||||
private void loadRecords()
|
||||
{
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
|
||||
check(habit.getId());
|
||||
List<RepetitionRecord> records =
|
||||
repository.findAll("where habit = ? order by timestamp",
|
||||
habit.getId().toString());
|
||||
|
||||
for (RepetitionRecord rec : records)
|
||||
list.add(rec.toRepetition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a repetition to the global SQLite database.
|
||||
* <p>
|
||||
* Given a repetition, this creates and saves the corresponding
|
||||
* RepetitionRecord to the database.
|
||||
*
|
||||
* @param rep the repetition to be added
|
||||
*/
|
||||
@Override
|
||||
public void add(Repetition rep)
|
||||
{
|
||||
loadRecords();
|
||||
list.add(rep);
|
||||
check(habit.getId());
|
||||
addStatement.bindLong(1, habit.getId());
|
||||
addStatement.bindLong(2, rep.getTimestamp());
|
||||
addStatement.bindLong(3, rep.getValue());
|
||||
addStatement.execute();
|
||||
RepetitionRecord record = new RepetitionRecord();
|
||||
record.habit_id = habit.getId();
|
||||
record.copyFrom(rep);
|
||||
repository.save(record);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Repetition> getByInterval(long timeFrom, long timeTo)
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, value " +
|
||||
"from Repetitions " +
|
||||
"where habit = ? and timestamp >= ? and timestamp <= ? " +
|
||||
"order by timestamp";
|
||||
|
||||
String params[] = {
|
||||
Long.toString(habit.getId()),
|
||||
Long.toString(timeFrom),
|
||||
Long.toString(timeTo)
|
||||
};
|
||||
|
||||
List<RepetitionRecord> records = sqlite.query(query, params);
|
||||
return toRepetitions(records);
|
||||
loadRecords();
|
||||
return list.getByInterval(timeFrom, timeTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Repetition getByTimestamp(long timestamp)
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, value " +
|
||||
"from Repetitions " +
|
||||
"where habit = ? and timestamp = ? " +
|
||||
"limit 1";
|
||||
|
||||
String params[] =
|
||||
{ Long.toString(habit.getId()), Long.toString(timestamp) };
|
||||
|
||||
RepetitionRecord record = sqlite.querySingle(query, params);
|
||||
if (record == null) return null;
|
||||
record.habit = habitRecord;
|
||||
return record.toRepetition();
|
||||
loadRecords();
|
||||
return list.getByTimestamp(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repetition getOldest()
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, value " +
|
||||
"from Repetitions " +
|
||||
"where habit = ? " +
|
||||
"order by timestamp asc " +
|
||||
"limit 1";
|
||||
|
||||
String params[] = { Long.toString(habit.getId()) };
|
||||
|
||||
RepetitionRecord record = sqlite.querySingle(query, params);
|
||||
if (record == null) return null;
|
||||
record.habit = habitRecord;
|
||||
return record.toRepetition();
|
||||
loadRecords();
|
||||
return list.getOldest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repetition getNewest()
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, value " +
|
||||
"from Repetitions " +
|
||||
"where habit = ? " +
|
||||
"order by timestamp desc " +
|
||||
"limit 1";
|
||||
|
||||
String params[] = { Long.toString(habit.getId()) };
|
||||
|
||||
RepetitionRecord record = sqlite.querySingle(query, params);
|
||||
if (record == null) return null;
|
||||
record.habit = habitRecord;
|
||||
return record.toRepetition();
|
||||
loadRecords();
|
||||
return list.getNewest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NonNull Repetition repetition)
|
||||
{
|
||||
new Delete()
|
||||
.from(RepetitionRecord.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp = ?", repetition.getTimestamp())
|
||||
.execute();
|
||||
|
||||
loadRecords();
|
||||
list.remove(repetition);
|
||||
check(habit.getId());
|
||||
repository.execSQL(
|
||||
"delete from repetitions where habit = ? and timestamp = ?",
|
||||
habit.getId());
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Contract("null -> fail")
|
||||
private void check(Long id)
|
||||
{
|
||||
if (id == null) throw new RuntimeException("habit is not saved");
|
||||
|
||||
if (habitRecord != null) return;
|
||||
|
||||
habitRecord = HabitRecord.get(id);
|
||||
if (habitRecord == null) throw new RuntimeException("habit not found");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<Repetition> toRepetitions(
|
||||
@NonNull List<RepetitionRecord> records)
|
||||
public void removeAll()
|
||||
{
|
||||
loadRecords();
|
||||
list.removeAll();
|
||||
check(habit.getId());
|
||||
|
||||
List<Repetition> 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.models.sqlite;
|
||||
|
||||
import android.database.*;
|
||||
import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class SQLiteUtils<T extends SQLiteRecord>
|
||||
{
|
||||
private Class klass;
|
||||
|
||||
public SQLiteUtils(Class klass)
|
||||
{
|
||||
this.klass = klass;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<T> query(String query, String params[])
|
||||
{
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
try (Cursor c = db.rawQuery(query, params))
|
||||
{
|
||||
return cursorToMultipleRecords(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T querySingle(String query, String params[])
|
||||
{
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
try(Cursor c = db.rawQuery(query, params))
|
||||
{
|
||||
if (!c.moveToNext()) return null;
|
||||
return cursorToSingleRecord(c);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<T> cursorToMultipleRecords(Cursor c)
|
||||
{
|
||||
List<T> records = new LinkedList<>();
|
||||
while (c.moveToNext()) records.add(cursorToSingleRecord(c));
|
||||
return records;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private T cursorToSingleRecord(Cursor c)
|
||||
{
|
||||
try
|
||||
{
|
||||
T record = (T) klass.newInstance();
|
||||
record.copyFrom(c);
|
||||
return record;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,122 +19,67 @@
|
||||
|
||||
package org.isoron.uhabits.models.sqlite.records;
|
||||
|
||||
import android.annotation.*;
|
||||
import android.database.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
import com.activeandroid.annotation.*;
|
||||
import com.activeandroid.query.*;
|
||||
import com.activeandroid.util.*;
|
||||
|
||||
import org.apache.commons.lang3.builder.*;
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.utils.DatabaseUtils;
|
||||
|
||||
import java.lang.reflect.*;
|
||||
|
||||
/**
|
||||
* The SQLite database record corresponding to a {@link Habit}.
|
||||
*/
|
||||
@Table(name = "Habits")
|
||||
public class HabitRecord extends Model implements SQLiteRecord
|
||||
@Table(name = "habits")
|
||||
public class HabitRecord
|
||||
{
|
||||
public static String SELECT =
|
||||
"select id, color, description, freq_den, freq_num, " +
|
||||
"name, position, reminder_hour, reminder_min, " +
|
||||
"highlight, archived, reminder_days, type, target_type, " +
|
||||
"target_value, unit from habits ";
|
||||
|
||||
@Column(name = "name")
|
||||
public String name;
|
||||
|
||||
@Column(name = "description")
|
||||
@Column
|
||||
public String description;
|
||||
|
||||
@Column
|
||||
public String name;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,32 +19,28 @@
|
||||
|
||||
package org.isoron.uhabits.models.sqlite.records;
|
||||
|
||||
import android.database.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
import com.activeandroid.annotation.*;
|
||||
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
|
||||
/**
|
||||
* The SQLite database record corresponding to a {@link Repetition}.
|
||||
*/
|
||||
@Table(name = "Repetitions")
|
||||
public class RepetitionRecord extends Model implements SQLiteRecord
|
||||
public class RepetitionRecord
|
||||
{
|
||||
@Column(name = "habit")
|
||||
public HabitRecord habit;
|
||||
|
||||
@Column(name = "timestamp")
|
||||
@Column(name = "habit")
|
||||
public Long habit_id;
|
||||
|
||||
@Column
|
||||
public Long timestamp;
|
||||
|
||||
@Column(name = "value")
|
||||
public int value;
|
||||
@Column
|
||||
public Integer value;
|
||||
|
||||
public static RepetitionRecord get(Long id)
|
||||
{
|
||||
return RepetitionRecord.load(RepetitionRecord.class, id);
|
||||
}
|
||||
@Column
|
||||
public Long id;
|
||||
|
||||
public void copyFrom(Repetition repetition)
|
||||
{
|
||||
@@ -52,13 +48,6 @@ public class RepetitionRecord extends Model implements SQLiteRecord
|
||||
value = repetition.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyFrom(Cursor c)
|
||||
{
|
||||
timestamp = c.getLong(1);
|
||||
value = c.getInt(2);
|
||||
}
|
||||
|
||||
public Repetition toRepetition()
|
||||
{
|
||||
return new Repetition(timestamp, value);
|
||||
|
||||
@@ -19,18 +19,17 @@
|
||||
|
||||
package org.isoron.uhabits.sync;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
|
||||
import java.util.List;
|
||||
import org.isoron.androidbase.storage.*;
|
||||
|
||||
@Table(name = "Events")
|
||||
public class Event extends Model
|
||||
public class Event
|
||||
{
|
||||
@Nullable
|
||||
@Column
|
||||
public Long id;
|
||||
|
||||
@NonNull
|
||||
@Column(name = "timestamp")
|
||||
public Long timestamp;
|
||||
@@ -56,10 +55,4 @@ public class Event extends Model
|
||||
this.timestamp = timestamp;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static List<Event> getAll()
|
||||
{
|
||||
return new Select().from(Event.class).orderBy("timestamp").execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
import org.isoron.androidbase.storage.*;
|
||||
import org.isoron.uhabits.BuildConfig;
|
||||
import org.isoron.uhabits.core.*;
|
||||
import org.isoron.uhabits.core.commands.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.json.*;
|
||||
|
||||
import java.net.*;
|
||||
@@ -95,6 +97,8 @@ public class SyncManager implements CommandRunner.Listener
|
||||
|
||||
private SSLContextProvider sslProvider;
|
||||
|
||||
private final SQLiteRepository<Event> repository;
|
||||
|
||||
@Inject
|
||||
public SyncManager(@NonNull SSLContextProvider sslProvider,
|
||||
@NonNull Preferences prefs,
|
||||
@@ -109,8 +113,10 @@ public class SyncManager implements CommandRunner.Listener
|
||||
this.commandParser = commandParser;
|
||||
this.isListening = false;
|
||||
|
||||
repository =
|
||||
new SQLiteRepository<>(Event.class, DatabaseUtils.openDatabase());
|
||||
pendingConfirmation = new LinkedList<>();
|
||||
pendingEmit = new LinkedList<>(Event.getAll());
|
||||
pendingEmit = new LinkedList<>(repository.findAll("order by timestamp"));
|
||||
|
||||
groupKey = prefs.getSyncKey();
|
||||
clientId = prefs.getSyncClientId();
|
||||
@@ -129,7 +135,7 @@ public class SyncManager implements CommandRunner.Listener
|
||||
JSONObject msg = toJSONObject(command.toJson());
|
||||
Long now = new Date().getTime();
|
||||
Event e = new Event(command.getId(), now, msg.toString());
|
||||
e.save();
|
||||
repository.save(e);
|
||||
|
||||
Log.i("SyncManager", "Adding to outbox: " + msg.toString());
|
||||
|
||||
@@ -337,7 +343,7 @@ public class SyncManager implements CommandRunner.Listener
|
||||
{
|
||||
Log.i("SyncManager", "Pending command confirmed");
|
||||
pendingConfirmation.remove(e);
|
||||
e.delete();
|
||||
repository.remove(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,33 +20,40 @@
|
||||
package org.isoron.uhabits.utils;
|
||||
|
||||
import android.content.*;
|
||||
import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.models.sqlite.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
import org.isoron.uhabits.sync.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.*;
|
||||
|
||||
public abstract class DatabaseUtils
|
||||
{
|
||||
@Nullable
|
||||
private static HabitsDatabaseOpener opener = null;
|
||||
|
||||
public static void executeAsTransaction(Callback callback)
|
||||
{
|
||||
ActiveAndroid.beginTransaction();
|
||||
try
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,15 @@ import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.widget.*;
|
||||
|
||||
import com.activeandroid.util.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
|
||||
import static android.appwidget.AppWidgetManager.*;
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.*;
|
||||
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
|
||||
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
|
||||
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
|
||||
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.dpToPixels;
|
||||
|
||||
public abstract class BaseWidgetProvider extends AppWidgetProvider
|
||||
{
|
||||
@@ -109,7 +110,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
|
||||
}
|
||||
catch (HabitNotFoundException e)
|
||||
{
|
||||
Log.e("BaseWidgetProvider", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public abstract class HabitList implements Iterable<Habit>
|
||||
* @param from the habit that should be moved
|
||||
* @param to the habit that currently occupies the desired position
|
||||
*/
|
||||
public abstract void reorder(Habit from, Habit to);
|
||||
public abstract void reorder(@NonNull Habit from, @NonNull Habit to);
|
||||
|
||||
public void repair()
|
||||
{
|
||||
|
||||
@@ -221,4 +221,6 @@ public abstract class RepetitionList
|
||||
add(new Repetition(timestamp, value));
|
||||
habit.invalidateNewerThan(timestamp);
|
||||
}
|
||||
|
||||
public abstract void removeAll();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package org.isoron.uhabits.core.models.memory;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -60,8 +61,10 @@ public class MemoryCheckmarkList extends CheckmarkList
|
||||
Checkmark oldest = getOldestComputed();
|
||||
if(newest != null) newestTimestamp = newest.getTimestamp();
|
||||
if(oldest != null) oldestTimestamp = oldest.getTimestamp();
|
||||
long days = (newestTimestamp - oldestTimestamp) /
|
||||
DateUtils.millisecondsInOneDay;
|
||||
|
||||
List<Checkmark> filtered = new LinkedList<>();
|
||||
List<Checkmark> filtered = new ArrayList<>((int) days);
|
||||
for(long time = toTimestamp; time >= fromTimestamp; time -= millisecondsInOneDay)
|
||||
{
|
||||
if(time > newestTimestamp || time < oldestTimestamp)
|
||||
|
||||
@@ -25,7 +25,10 @@ import org.isoron.uhabits.core.models.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.*;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.BY_COLOR;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.BY_NAME;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.BY_POSITION;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.BY_SCORE;
|
||||
|
||||
/**
|
||||
* In-memory implementation of {@link HabitList}.
|
||||
@@ -33,30 +36,37 @@ import static org.isoron.uhabits.core.models.HabitList.Order.*;
|
||||
public class MemoryHabitList extends HabitList
|
||||
{
|
||||
@NonNull
|
||||
private LinkedList<Habit> list;
|
||||
private LinkedList<Habit> list = new LinkedList<>();
|
||||
|
||||
private Comparator<Habit> comparator = null;
|
||||
|
||||
@NonNull
|
||||
private Order order;
|
||||
private Order order = Order.BY_POSITION;
|
||||
|
||||
@Nullable
|
||||
private MemoryHabitList parent = null;
|
||||
|
||||
public MemoryHabitList()
|
||||
{
|
||||
super();
|
||||
list = new LinkedList<>();
|
||||
order = Order.BY_POSITION;
|
||||
}
|
||||
|
||||
protected MemoryHabitList(@NonNull HabitMatcher matcher)
|
||||
protected MemoryHabitList(@NonNull HabitMatcher matcher,
|
||||
Comparator<Habit> comparator,
|
||||
@NonNull MemoryHabitList parent)
|
||||
{
|
||||
super(matcher);
|
||||
list = new LinkedList<>();
|
||||
order = Order.BY_POSITION;
|
||||
this.parent = parent;
|
||||
this.comparator = comparator;
|
||||
parent.getObservable().addListener(this::loadFromParent);
|
||||
loadFromParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(@NonNull Habit habit) throws IllegalArgumentException
|
||||
public synchronized void add(@NonNull Habit habit)
|
||||
throws IllegalArgumentException
|
||||
{
|
||||
throwIfHasParent();
|
||||
if (list.contains(habit))
|
||||
throw new IllegalArgumentException("habit already added");
|
||||
|
||||
@@ -67,14 +77,16 @@ public class MemoryHabitList extends HabitList
|
||||
if (id == null) habit.setId((long) list.size());
|
||||
list.addLast(habit);
|
||||
resort();
|
||||
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Habit getById(long id)
|
||||
public synchronized Habit getById(long id)
|
||||
{
|
||||
for (Habit h : list)
|
||||
{
|
||||
if (h.getId() == null) continue;
|
||||
if (h.getId() == null) throw new IllegalStateException();
|
||||
if (h.getId() == id) return h;
|
||||
}
|
||||
return null;
|
||||
@@ -82,33 +94,66 @@ public class MemoryHabitList extends HabitList
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Habit getByPosition(int position)
|
||||
public synchronized Habit getByPosition(int position)
|
||||
{
|
||||
return list.get(position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public HabitList getFiltered(HabitMatcher matcher)
|
||||
public synchronized HabitList getFiltered(HabitMatcher matcher)
|
||||
{
|
||||
MemoryHabitList habits = new MemoryHabitList(matcher);
|
||||
habits.comparator = comparator;
|
||||
for (Habit h : this) if (matcher.matches(h)) habits.add(h);
|
||||
return habits;
|
||||
return new MemoryHabitList(matcher, comparator, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Order getOrder()
|
||||
public synchronized Order getOrder()
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setOrder(@NonNull Order order)
|
||||
{
|
||||
this.order = order;
|
||||
this.comparator = getComparatorByOrder(order);
|
||||
resort();
|
||||
}
|
||||
|
||||
private Comparator<Habit> getComparatorByOrder(Order order)
|
||||
{
|
||||
Comparator<Habit> nameComparator =
|
||||
(h1, h2) -> h1.getName().compareTo(h2.getName());
|
||||
|
||||
Comparator<Habit> colorComparator = (h1, h2) ->
|
||||
{
|
||||
Integer c1 = h1.getColor();
|
||||
Integer c2 = h2.getColor();
|
||||
if (c1.equals(c2)) return nameComparator.compare(h1, h2);
|
||||
else return c1.compareTo(c2);
|
||||
};
|
||||
|
||||
Comparator<Habit> scoreComparator = (h1, h2) ->
|
||||
{
|
||||
double s1 = h1.getScores().getTodayValue();
|
||||
double s2 = h2.getScores().getTodayValue();
|
||||
return Double.compare(s2, s1);
|
||||
};
|
||||
|
||||
if (order == BY_POSITION) return null;
|
||||
if (order == BY_NAME) return nameComparator;
|
||||
if (order == BY_COLOR) return colorComparator;
|
||||
if (order == BY_SCORE) return scoreComparator;
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(@NonNull Habit h)
|
||||
{
|
||||
return list.indexOf(h);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<Habit> iterator()
|
||||
{
|
||||
@@ -116,27 +161,29 @@ public class MemoryHabitList extends HabitList
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NonNull Habit habit)
|
||||
public synchronized void remove(@NonNull Habit habit)
|
||||
{
|
||||
throwIfHasParent();
|
||||
list.remove(habit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reorder(Habit from, Habit to)
|
||||
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
|
||||
{
|
||||
throwIfHasParent();
|
||||
if (indexOf(from) < 0)
|
||||
throw new IllegalArgumentException(
|
||||
"list does not contain (from) habit");
|
||||
|
||||
int toPos = indexOf(to);
|
||||
if (toPos < 0)
|
||||
throw new IllegalArgumentException(
|
||||
"list does not contain (to) habit");
|
||||
|
||||
list.remove(from);
|
||||
list.add(toPos, from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOrder(@NonNull Order order)
|
||||
{
|
||||
this.order = order;
|
||||
this.comparator = getComparatorByOrder(order);
|
||||
resort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
@@ -149,32 +196,23 @@ public class MemoryHabitList extends HabitList
|
||||
// NOP
|
||||
}
|
||||
|
||||
private Comparator<Habit> getComparatorByOrder(Order order)
|
||||
private void throwIfHasParent()
|
||||
{
|
||||
Comparator<Habit> nameComparator =
|
||||
(h1, h2) -> h1.getName().compareTo(h2.getName());
|
||||
|
||||
Comparator<Habit> colorComparator = (h1, h2) -> {
|
||||
Integer c1 = h1.getColor();
|
||||
Integer c2 = h2.getColor();
|
||||
if (c1.equals(c2)) return nameComparator.compare(h1, h2);
|
||||
else return c1.compareTo(c2);
|
||||
};
|
||||
|
||||
Comparator<Habit> scoreComparator = (h1, h2) -> {
|
||||
double s1 = h1.getScores().getTodayValue();
|
||||
double s2 = h2.getScores().getTodayValue();
|
||||
return Double.compare(s2, s1);
|
||||
};
|
||||
|
||||
if (order == BY_POSITION) return null;
|
||||
if (order == BY_NAME) return nameComparator;
|
||||
if (order == BY_COLOR) return colorComparator;
|
||||
if (order == BY_SCORE) return scoreComparator;
|
||||
throw new IllegalStateException();
|
||||
if (parent != null) throw new IllegalStateException(
|
||||
"Filtered lists cannot be modified directly. " +
|
||||
"You should modify the parent list instead.");
|
||||
}
|
||||
|
||||
private void resort()
|
||||
private synchronized void loadFromParent()
|
||||
{
|
||||
if (parent == null) throw new IllegalStateException();
|
||||
|
||||
list.clear();
|
||||
for (Habit h : parent) if (filter.matches(h)) list.add(h);
|
||||
resort();
|
||||
}
|
||||
|
||||
private synchronized void resort()
|
||||
{
|
||||
if (comparator != null) Collections.sort(list, comparator);
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ import java.util.*;
|
||||
*/
|
||||
public class MemoryRepetitionList extends RepetitionList
|
||||
{
|
||||
LinkedList<Repetition> list;
|
||||
ArrayList<Repetition> list;
|
||||
|
||||
public MemoryRepetitionList(Habit habit)
|
||||
{
|
||||
super(habit);
|
||||
list = new LinkedList<>();
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,7 +48,7 @@ public class MemoryRepetitionList extends RepetitionList
|
||||
@Override
|
||||
public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp)
|
||||
{
|
||||
LinkedList<Repetition> filtered = new LinkedList<>();
|
||||
ArrayList<Repetition> filtered = new ArrayList<>();
|
||||
|
||||
for (Repetition r : list)
|
||||
{
|
||||
@@ -57,7 +57,7 @@ public class MemoryRepetitionList extends RepetitionList
|
||||
}
|
||||
|
||||
Collections.sort(filtered,
|
||||
(r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp()));
|
||||
(r1, r2) -> Long.compare(r1.getTimestamp(), r2.getTimestamp()));
|
||||
|
||||
return filtered;
|
||||
}
|
||||
@@ -122,4 +122,10 @@ public class MemoryRepetitionList extends RepetitionList
|
||||
{
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll()
|
||||
{
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,20 +19,29 @@
|
||||
|
||||
package org.isoron.uhabits.core.models;
|
||||
|
||||
import org.hamcrest.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.junit.*;
|
||||
import org.junit.rules.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.fail;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.hamcrest.MatcherAssert.*;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.BY_COLOR;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.BY_NAME;
|
||||
import static org.isoron.uhabits.core.models.HabitList.Order.BY_POSITION;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
@SuppressWarnings("JavaDoc")
|
||||
public class HabitListTest extends BaseUnitTest
|
||||
{
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private ArrayList<Habit> habitsArray;
|
||||
|
||||
private HabitList activeHabits;
|
||||
@@ -69,37 +78,27 @@ 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()
|
||||
{
|
||||
assertThat(habitList.getById(100L), is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_get_withValidId()
|
||||
public void testGetById()
|
||||
{
|
||||
Habit habit1 = habitsArray.get(0);
|
||||
Habit habit2 = habitList.getById(habit1.getId());
|
||||
@@ -107,7 +106,13 @@ public class HabitListTest extends BaseUnitTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_ordering()
|
||||
public void testGetById_withInvalidId()
|
||||
{
|
||||
assertNull(habitList.getById(100L));
|
||||
}
|
||||
|
||||
@Test
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user