Merge branch 'feature/sync' into dev

pull/286/head
Alinson S. Xavier 9 years ago
commit b4e79c3f4b

@ -12,7 +12,7 @@ android {
minSdkVersion 15
targetSdkVersion 25
buildConfigField "Integer", "databaseVersion", "18"
buildConfigField "Integer", "databaseVersion", "19"
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -80,6 +80,10 @@ dependencies {
provided 'javax.annotation:jsr250-api:1.0'
compile ('io.socket:socket.io-client:0.7.0') {
exclude group: 'org.json', module: 'json'
}
testApt 'com.google.dagger:dagger-compiler:2.2'
testCompile 'junit:junit:4.12'

@ -36,6 +36,8 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="HabitsApplication"
android:allowBackup="true"

@ -0,0 +1,41 @@
-----BEGIN CERTIFICATE-----
MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO
BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi
MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ
8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6
zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y
fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7
w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc
G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k
epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q
laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ
QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU
fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826
YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY
gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe
MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0
IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy
dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw
czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0
dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl
aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC
AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg
b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB
ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc
nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg
18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c
gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl
Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY
sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T
SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF
CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk
zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW
omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD
-----END CERTIFICATE-----

@ -19,11 +19,16 @@
package org.isoron.uhabits.commands;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.json.*;
import java.util.*;
import static org.isoron.uhabits.commands.CommandParser.*;
/**
* Command to archive a list of habits.
*/
@ -33,12 +38,36 @@ public class ArchiveHabitsCommand extends Command
private final HabitList habitList;
public ArchiveHabitsCommand(HabitList habitList, List<Habit> selectedHabits)
public ArchiveHabitsCommand(@NonNull HabitList habitList,
@NonNull List<Habit> selectedHabits)
{
super();
this.habitList = habitList;
this.selectedHabits = selectedHabits;
}
public ArchiveHabitsCommand(@NonNull String id,
@NonNull HabitList habitList,
@NonNull List<Habit> selectedHabits)
{
super(id);
this.habitList = habitList;
this.selectedHabits = selectedHabits;
}
public static Command fromJSON(@NonNull JSONObject json,
@NonNull HabitList habitList)
throws JSONException
{
String id = json.getString("id");
JSONObject data = (JSONObject) json.get("data");
JSONArray habitIds = data.getJSONArray("ids");
LinkedList<Habit> selectedHabits =
habitListFromJSON(habitList, habitIds);
return new ArchiveHabitsCommand(id, habitList, selectedHabits);
}
@Override
public void execute()
{
@ -58,6 +87,24 @@ public class ArchiveHabitsCommand extends Command
return R.string.toast_habit_unarchived;
}
@Nullable
@Override
public JSONObject toJSON()
{
try
{
JSONObject root = super.toJSON();
JSONObject data = root.getJSONObject("data");
root.put("event", "ArchiveHabits");
data.put("ids", habitListToJSON(selectedHabits));
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@Override
public void undo()
{

@ -19,11 +19,16 @@
package org.isoron.uhabits.commands;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.json.*;
import java.util.*;
import static org.isoron.uhabits.commands.CommandParser.*;
/**
* Command to change the color of a list of habits.
*/
@ -37,16 +42,35 @@ public class ChangeHabitColorCommand extends Command
Integer newColor;
public ChangeHabitColorCommand(HabitList habitList,
List<Habit> selected,
Integer newColor)
public ChangeHabitColorCommand(@NonNull HabitList habitList,
@NonNull List<Habit> selected,
@NonNull Integer newColor)
{
this.habitList = habitList;
this.selected = selected;
this.newColor = newColor;
this.originalColors = new ArrayList<>(selected.size());
super();
init(habitList, selected, newColor);
}
for (Habit h : selected) originalColors.add(h.getColor());
public ChangeHabitColorCommand(@NonNull String id,
@NonNull HabitList habitList,
@NonNull List<Habit> selected,
@NonNull Integer newColor)
{
super(id);
init(habitList, selected, newColor);
}
@NonNull
public static Command fromJSON(@NonNull JSONObject json,
@NonNull HabitList habitList)
throws JSONException
{
String id = json.getString("id");
JSONObject data = (JSONObject) json.get("data");
JSONArray habitIds = data.getJSONArray("ids");
int newColor = data.getInt("color");
LinkedList<Habit> selected = habitListFromJSON(habitList, habitIds);
return new ChangeHabitColorCommand(id, habitList, selected, newColor);
}
@Override
@ -68,6 +92,25 @@ public class ChangeHabitColorCommand extends Command
return R.string.toast_habit_changed;
}
@Override
@NonNull
public JSONObject toJSON()
{
try
{
JSONObject root = super.toJSON();
JSONObject data = root.getJSONObject("data");
root.put("event", "ChangeHabitColor");
data.put("ids", habitListToJSON(selected));
data.put("color", newColor);
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@Override
public void undo()
{
@ -75,4 +118,16 @@ public class ChangeHabitColorCommand extends Command
for (Habit h : selected) h.setColor(originalColors.get(k++));
habitList.update(selected);
}
private void init(@NonNull HabitList habitList,
@NonNull List<Habit> selected,
@NonNull Integer newColor)
{
this.habitList = habitList;
this.selected = selected;
this.newColor = newColor;
this.originalColors = new ArrayList<>(selected.size());
for (Habit h : selected) originalColors.add(h.getColor());
}
}

@ -19,6 +19,9 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.utils.*;
import org.json.*;
/**
* A Command represents a desired set of changes that should be performed on the
* models.
@ -30,6 +33,18 @@ package org.isoron.uhabits.commands;
*/
public abstract class Command
{
private final String id;
public Command()
{
id = DatabaseUtils.getRandomId();
}
public Command(String id)
{
this.id = id;
}
public abstract void execute();
public Integer getExecuteStringId()
@ -43,4 +58,25 @@ public abstract class Command
}
public abstract void undo();
public JSONObject toJSON()
{
try
{
JSONObject root = new JSONObject();
JSONObject data = new JSONObject();
root.put("id", getId());
root.put("data", data);
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
public String getId()
{
return id;
}
}

@ -0,0 +1,105 @@
/*
* 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.commands;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import org.json.*;
import java.util.*;
public class CommandParser
{
private HabitList habitList;
private ModelFactory modelFactory;
public CommandParser(@NonNull HabitList habitList,
@NonNull ModelFactory modelFactory)
{
this.habitList = habitList;
this.modelFactory = modelFactory;
}
@NonNull
public static LinkedList<Habit> habitListFromJSON(
@NonNull HabitList habitList, @NonNull JSONArray habitIds)
throws JSONException
{
LinkedList<Habit> habits = new LinkedList<>();
for (int i = 0; i < habitIds.length(); i++)
{
Long hId = habitIds.getLong(i);
Habit h = habitList.getById(hId);
if (h == null) continue;
habits.add(h);
}
return habits;
}
@NonNull
protected static JSONArray habitListToJSON(List<Habit> habits)
{
JSONArray habitIds = new JSONArray();
for (Habit h : habits) habitIds.put(h.getId());
return habitIds;
}
@NonNull
public Command fromJSON(@NonNull JSONObject json) throws JSONException
{
switch (json.getString("event"))
{
case "ToggleRepetition":
return ToggleRepetitionCommand.fromJSON(json, habitList);
case "ArchiveHabits":
return ArchiveHabitsCommand.fromJSON(json, habitList);
case "UnarchiveHabits":
return UnarchiveHabitsCommand.fromJSON(json, habitList);
case "ChangeHabitColor":
return ChangeHabitColorCommand.fromJSON(json, habitList);
case "CreateHabit":
return CreateHabitCommand.fromJSON(json, habitList,
modelFactory);
case "DeleteHabits":
return DeleteHabitsCommand.fromJSON(json, habitList);
case "EditHabit":
return EditHabitCommand.fromJSON(json, habitList, modelFactory);
// TODO: Implement this
// case "ReorderHabit":
// return ReorderHabitCommand.fromJSON(json);
}
return null;
}
}

@ -25,6 +25,7 @@ import com.google.auto.factory.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.json.*;
/**
* Command to create a habit.
@ -36,19 +37,50 @@ public class CreateHabitCommand extends Command
HabitList habitList;
@NonNull
private Habit model;
@Nullable
private Long savedId;
public CreateHabitCommand(@Provided @NonNull ModelFactory modelFactory,
@NonNull HabitList habitList,
@NonNull Habit model)
{
super();
this.modelFactory = modelFactory;
this.habitList = habitList;
this.model = model;
}
public CreateHabitCommand(@Provided @NonNull ModelFactory modelFactory,
@NonNull String commandId,
@NonNull HabitList habitList,
@NonNull Habit model,
@Nullable Long savedId)
{
super(commandId);
this.modelFactory = modelFactory;
this.habitList = habitList;
this.model = model;
this.savedId = savedId;
}
@NonNull
public static Command fromJSON(@NonNull JSONObject root,
@NonNull HabitList habitList,
@NonNull ModelFactory modelFactory)
throws JSONException
{
String commandId = root.getString("id");
JSONObject data = (JSONObject) root.get("data");
Habit model = Habit.fromJSON(data.getJSONObject("habit"), modelFactory);
Long savedId = data.getLong("id");
return new CreateHabitCommand(modelFactory, commandId, habitList, model,
savedId);
}
@Override
public void execute()
{
@ -72,6 +104,24 @@ public class CreateHabitCommand extends Command
return R.string.toast_habit_deleted;
}
@Override
public JSONObject toJSON()
{
try
{
JSONObject root = super.toJSON();
JSONObject data = root.getJSONObject("data");
root.put("event", "CreateHabit");
data.put("habit", model.toJSON());
data.put("id", savedId);
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@Override
public void undo()
{
@ -80,5 +130,4 @@ public class CreateHabitCommand extends Command
habitList.remove(habit);
}
}

@ -19,11 +19,17 @@
package org.isoron.uhabits.commands;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.json.*;
import java.util.*;
import static org.isoron.uhabits.commands.CommandParser.habitListFromJSON;
import static org.isoron.uhabits.commands.CommandParser.habitListToJSON;
/**
* Command to delete a list of habits.
*/
@ -33,12 +39,36 @@ public class DeleteHabitsCommand extends Command
private List<Habit> habits;
public DeleteHabitsCommand(HabitList habitList, List<Habit> habits)
public DeleteHabitsCommand(@NonNull HabitList habitList,
@NonNull List<Habit> habits)
{
super();
this.habits = habits;
this.habitList = habitList;
}
public DeleteHabitsCommand(@NonNull String id,
@NonNull HabitList habitList,
@NonNull List<Habit> habits)
{
super(id);
this.habits = habits;
this.habitList = habitList;
}
@NonNull
public static Command fromJSON(@NonNull JSONObject json,
@NonNull HabitList habitList)
throws JSONException
{
String id = json.getString("id");
JSONObject data = (JSONObject) json.get("data");
JSONArray habitIds = data.getJSONArray("ids");
LinkedList<Habit> habits = habitListFromJSON(habitList, habitIds);
return new DeleteHabitsCommand(id, habitList, habits);
}
@Override
public void execute()
{
@ -63,6 +93,24 @@ public class DeleteHabitsCommand extends Command
return R.string.toast_habit_restored;
}
@Override
@NonNull
public JSONObject toJSON()
{
try
{
JSONObject root = super.toJSON();
JSONObject data = root.getJSONObject("data");
root.put("event", "DeleteHabits");
data.put("ids", habitListToJSON(habits));
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@Override
public void undo()
{

@ -25,6 +25,7 @@ import com.google.auto.factory.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.json.*;
/**
* Command to modify a habit.
@ -42,27 +43,42 @@ public class EditHabitCommand extends Command
private boolean hasFrequencyChanged;
private final boolean hasTargetChanged;
private boolean hasTargetChanged;
public EditHabitCommand(@Provided @NonNull ModelFactory modelFactory,
@NonNull HabitList habitList,
@NonNull Habit original,
@NonNull Habit modified)
{
this.habitList = habitList;
this.savedId = original.getId();
this.modified = modelFactory.buildHabit();
this.original = modelFactory.buildHabit();
super();
init(modelFactory, habitList, original, modified);
}
this.modified.copyFrom(modified);
this.original.copyFrom(original);
public EditHabitCommand(@Provided @NonNull ModelFactory modelFactory,
@NonNull String id,
@NonNull HabitList habitList,
@NonNull Habit original,
@NonNull Habit modified)
{
super(id);
init(modelFactory, habitList, original, modified);
}
Frequency originalFreq = this.original.getFrequency();
Frequency modifiedFreq = this.modified.getFrequency();
hasFrequencyChanged = (!originalFreq.equals(modifiedFreq));
hasTargetChanged =
(original.getTargetType() != modified.getTargetType() ||
original.getTargetValue() != modified.getTargetValue());
@NonNull
public static Command fromJSON(@NonNull JSONObject root,
@NonNull HabitList habitList,
@NonNull ModelFactory modelFactory)
throws JSONException
{
String commandId = root.getString("id");
JSONObject data = (JSONObject) root.get("data");
Habit original = habitList.getById(data.getLong("id"));
if (original == null) throw new HabitNotFoundException();
Habit modified =
Habit.fromJSON(data.getJSONObject("params"), modelFactory);
return new EditHabitCommand(modelFactory, commandId, habitList,
original, modified);
}
@Override
@ -83,6 +99,24 @@ public class EditHabitCommand extends Command
return R.string.toast_habit_changed_back;
}
@Override
public JSONObject toJSON()
{
try
{
JSONObject root = super.toJSON();
JSONObject data = root.getJSONObject("data");
root.put("event", "EditHabit");
data.put("id", savedId);
data.put("params", modified.toJSON());
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@Override
public void undo()
{
@ -100,6 +134,27 @@ public class EditHabitCommand extends Command
invalidateIfNeeded(habit);
}
private void init(@NonNull ModelFactory modelFactory,
@NonNull HabitList habitList,
@NonNull Habit original,
@NonNull Habit modified)
{
this.habitList = habitList;
this.savedId = original.getId();
this.modified = modelFactory.buildHabit();
this.original = modelFactory.buildHabit();
this.modified.copyFrom(modified);
this.original.copyFrom(original);
Frequency originalFreq = this.original.getFrequency();
Frequency modifiedFreq = this.modified.getFrequency();
hasFrequencyChanged = (!originalFreq.equals(modifiedFreq));
hasTargetChanged =
(original.getTargetType() != modified.getTargetType() ||
original.getTargetValue() != modified.getTargetValue());
}
private void invalidateIfNeeded(Habit habit)
{
if (hasFrequencyChanged || hasTargetChanged)

@ -19,36 +19,85 @@
package org.isoron.uhabits.commands;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import org.json.*;
/**
* Command to toggle a repetition.
*/
public class ToggleRepetitionCommand extends Command
{
private Long offset;
private Long timestamp;
private Habit habit;
public ToggleRepetitionCommand(Habit habit, long offset)
public ToggleRepetitionCommand(@NonNull Habit habit, long timestamp)
{
this.offset = offset;
super();
this.timestamp = timestamp;
this.habit = habit;
}
@Override
public void execute()
public ToggleRepetitionCommand(@NonNull String id,
@NonNull Habit habit,
long timestamp)
{
super(id);
this.timestamp = timestamp;
this.habit = habit;
}
@NonNull
public static Command fromJSON(@NonNull JSONObject json,
@NonNull HabitList habitList)
throws JSONException
{
habit.getRepetitions().toggleTimestamp(offset);
String id = json.getString("id");
JSONObject data = (JSONObject) json.get("data");
Long habitId = data.getLong("habit");
Long timestamp = data.getLong("timestamp");
Habit habit = habitList.getById(habitId);
if (habit == null) throw new HabitNotFoundException();
return new ToggleRepetitionCommand(id, habit, timestamp);
}
@Override
public void undo()
public void execute()
{
execute();
habit.getRepetitions().toggleTimestamp(timestamp);
}
public Habit getHabit()
{
return habit;
}
@Override
@NonNull
public JSONObject toJSON()
{
try
{
JSONObject root = super.toJSON();
JSONObject data = root.getJSONObject("data");
root.put("event", "ToggleRepetition");
data.put("habit", habit.getId());
data.put("timestamp", timestamp);
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@Override
public void undo()
{
execute();
}
}

@ -19,11 +19,16 @@
package org.isoron.uhabits.commands;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.json.*;
import java.util.*;
import static org.isoron.uhabits.commands.CommandParser.*;
/**
* Command to unarchive a list of habits.
*/
@ -33,23 +38,40 @@ public class UnarchiveHabitsCommand extends Command
private List<Habit> habits;
public UnarchiveHabitsCommand(HabitList habitList, List<Habit> selected)
public UnarchiveHabitsCommand(@NonNull HabitList habitList,
@NonNull List<Habit> selected)
{
super();
this.habits = selected;
this.habitList = habitList;
}
@Override
public void execute()
public UnarchiveHabitsCommand(@NonNull String id,
@NonNull HabitList habitList,
@NonNull List<Habit> selected)
{
for(Habit h : habits) h.setArchived(false);
habitList.update(habits);
super(id);
this.habits = selected;
this.habitList = habitList;
}
@NonNull
public static Command fromJSON(@NonNull JSONObject json,
@NonNull HabitList habitList)
throws JSONException
{
String id = json.getString("id");
JSONObject data = (JSONObject) json.get("data");
JSONArray habitIds = data.getJSONArray("ids");
LinkedList<Habit> selected = habitListFromJSON(habitList, habitIds);
return new UnarchiveHabitsCommand(id, habitList, selected);
}
@Override
public void undo()
public void execute()
{
for(Habit h : habits) h.setArchived(true);
for (Habit h : habits) h.setArchived(false);
habitList.update(habits);
}
@ -64,4 +86,30 @@ public class UnarchiveHabitsCommand extends Command
{
return R.string.toast_habit_archived;
}
@Override
@NonNull
public JSONObject toJSON()
{
try
{
JSONObject root = super.toJSON();
JSONObject data = root.getJSONObject("data");
root.put("event", "UnarchiveHabits");
data.put("ids", habitListToJSON(habits));
return root;
}
catch (JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@Override
public void undo()
{
for (Habit h : habits) h.setArchived(true);
habitList.update(habits);
}
}

@ -23,6 +23,7 @@ import android.net.*;
import android.support.annotation.*;
import org.apache.commons.lang3.builder.*;
import org.json.*;
import java.util.*;
@ -371,4 +372,64 @@ public class Habit
.append("unit", unit)
.toString();
}
@NonNull
public JSONObject toJSON()
{
try
{
JSONObject json = new JSONObject();
json.put("name", name);
json.put("description", description);
json.put("freqNum", frequency.getNumerator());
json.put("freqDen", frequency.getDenominator());
json.put("color", color);
json.put("type", type);
json.put("targetType", targetType);
json.put("targetValue", targetValue);
json.put("unit", unit);
json.put("archived", archived);
if(reminder != null)
{
json.put("reminderHour", reminder.getHour());
json.put("reminderMin", reminder.getMinute());
json.put("reminderDays", reminder.getDays().toInteger());
}
return json;
}
catch(JSONException e)
{
throw new RuntimeException(e.getMessage());
}
}
@NonNull
public static Habit fromJSON(@NonNull JSONObject json,
@NonNull ModelFactory modelFactory)
throws JSONException
{
Habit habit = modelFactory.buildHabit();
habit.name = json.getString("name");
habit.description = json.getString("description");
int freqNum = json.getInt("freqNum");
int freqDen = json.getInt("freqDen");
habit.frequency = new Frequency(freqNum, freqDen);
habit.color = json.getInt("color");
habit.archived = json.getBoolean("archived");
habit.targetValue = json.getInt("targetValue");
habit.targetType = json.getInt("targetType");
habit.unit = json.getString("unit");
habit.type = json.getInt("type");
if(json.has("reminderHour"))
{
int hour = json.getInt("reminderHour");
int min = json.getInt("reminderMin");
int days = json.getInt("reminderDays");
habit.reminder = new Reminder(hour, min, new WeekdayList(days));
}
return habit;
}
}

@ -77,11 +77,6 @@ public class Preferences
}
}
public boolean isNumericalHabitsFeatureEnabled()
{
return prefs.getBoolean("pref_feature_numerical_habits", false);
}
public void setDefaultOrder(HabitList.Order order)
{
prefs.edit().putString("pref_default_order", order.name()).apply();
@ -120,6 +115,16 @@ public class Preferences
return prefs.getLong("last_hint_timestamp", -1);
}
public long getLastSync()
{
return prefs.getLong("lastSync", 0);
}
public void setLastSync(long timestamp)
{
prefs.edit().putLong("last_sync", timestamp).apply();
}
public boolean getShowArchived()
{
return prefs.getBoolean("pref_show_archived", false);
@ -145,6 +150,11 @@ public class Preferences
return Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
}
public String getSyncKey()
{
return prefs.getString("pref_sync_key", "");
}
public int getTheme()
{
return prefs.getInt("pref_theme", ThemeSwitcher.THEME_LIGHT);
@ -186,6 +196,11 @@ public class Preferences
prefs.edit().putBoolean("pref_first_run", isFirstRun).apply();
}
public boolean isNumericalHabitsFeatureEnabled()
{
return prefs.getBoolean("pref_feature_numerical_habits", false);
}
public boolean isPureBlackEnabled()
{
return prefs.getBoolean("pref_pure_black", false);

@ -0,0 +1,65 @@
/*
* 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.sync;
import android.support.annotation.NonNull;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Select;
import java.util.List;
@Table(name = "Events")
public class Event extends Model
{
@NonNull
@Column(name = "timestamp")
public Long timestamp;
@NonNull
@Column(name = "message")
public String message;
@NonNull
@Column(name = "server_id")
public String serverId;
public Event()
{
timestamp = 0L;
message = "";
serverId = "";
}
public Event(@NonNull String serverId, long timestamp, @NonNull String message)
{
this.serverId = serverId;
this.timestamp = timestamp;
this.message = message;
}
@NonNull
public static List<Event> getAll()
{
return new Select().from(Event.class).orderBy("timestamp").execute();
}
}

@ -0,0 +1,334 @@
/*
* 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.sync;
import android.content.*;
import android.support.annotation.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import org.json.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.*;
import java.util.*;
import javax.net.ssl.*;
import io.socket.client.*;
import io.socket.client.Socket;
import io.socket.emitter.*;
import static io.socket.client.Socket.*;
public class SyncManager
{
public static final String EVENT_AUTH = "auth";
public static final String EVENT_AUTH_OK = "authOK";
public static final String EVENT_EXECUTE_EVENT = "execute";
public static final String EVENT_FETCH = "fetch";
public static final String EVENT_FETCH_OK = "fetchOK";
public static final String EVENT_POST_EVENT = "postEvent";
public static final String SYNC_SERVER_URL =
"https://sync.loophabits.org:4000";
private static String CLIENT_ID;
private static String GROUP_KEY;
@NonNull
private Socket socket;
@NonNull
private LinkedList<Event> pendingConfirmation;
@NonNull
private List<Event> pendingEmit;
private boolean readyToEmit = false;
private Context context;
private final Preferences prefs;
private CommandRunner commandRunner;
private CommandParser commandParser;
public SyncManager(@NonNull Context context,
@NonNull Preferences prefs,
@NonNull CommandRunner commandRunner,
@NonNull CommandParser commandParser)
{
this.context = context;
this.prefs = prefs;
this.commandRunner = commandRunner;
this.commandParser = commandParser;
pendingConfirmation = new LinkedList<>();
pendingEmit = Event.getAll();
GROUP_KEY = prefs.getSyncKey();
CLIENT_ID = DatabaseUtils.getRandomId();
Log.d("SyncManager", CLIENT_ID);
try
{
IO.setDefaultSSLContext(getCACertSSLContext());
socket = IO.socket(SYNC_SERVER_URL);
logSocketEvent(socket, EVENT_CONNECT, "Connected");
logSocketEvent(socket, EVENT_CONNECT_TIMEOUT, "Connect timeout");
logSocketEvent(socket, EVENT_CONNECTING, "Connecting...");
logSocketEvent(socket, EVENT_CONNECT_ERROR, "Connect error");
logSocketEvent(socket, EVENT_DISCONNECT, "Disconnected");
logSocketEvent(socket, EVENT_RECONNECT, "Reconnected");
logSocketEvent(socket, EVENT_RECONNECT_ATTEMPT, "Reconnecting...");
logSocketEvent(socket, EVENT_RECONNECT_ERROR, "Reconnect error");
logSocketEvent(socket, EVENT_RECONNECT_FAILED, "Reconnect failed");
logSocketEvent(socket, EVENT_DISCONNECT, "Disconnected");
logSocketEvent(socket, EVENT_PING, "Ping");
logSocketEvent(socket, EVENT_PONG, "Pong");
socket.on(EVENT_CONNECT, new OnConnectListener());
socket.on(EVENT_DISCONNECT, new OnDisconnectListener());
socket.on(EVENT_EXECUTE_EVENT, new OnExecuteCommandListener());
socket.on(EVENT_AUTH_OK, new OnAuthOKListener());
socket.on(EVENT_FETCH_OK, new OnFetchOKListener());
socket.connect();
}
catch (URISyntaxException e)
{
throw new RuntimeException(e);
}
}
public void close()
{
socket.off();
socket.close();
}
public void postCommand(Command command)
{
JSONObject msg = command.toJSON();
if (msg == null) return;
Long now = new Date().getTime();
Event e = new Event(command.getId(), now, msg.toString());
e.save();
Log.i("SyncManager", "Adding to outbox: " + msg.toString());
pendingEmit.add(e);
if (readyToEmit) emitPending();
}
private void emitPending()
{
try
{
for (Event e : pendingEmit)
{
Log.i("SyncManager", "Emitting: " + e.message);
socket.emit(EVENT_POST_EVENT, new JSONObject(e.message));
pendingConfirmation.add(e);
}
pendingEmit.clear();
}
catch (JSONException e)
{
throw new RuntimeException(e);
}
}
private SSLContext getCACertSSLContext()
{
try
{
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = context.getAssets().open("cacert.pem");
Certificate ca = cf.generateCertificate(caInput);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("ca", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
return ctx;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
private void logSocketEvent(Socket socket, String event, final String msg)
{
socket.on(event, args -> Log.i("SyncManager", msg));
}
private void updateLastSync(Long timestamp)
{
prefs.setLastSync(timestamp + 1);
}
private class OnAuthOKListener implements Emitter.Listener
{
@Override
public void call(Object... args)
{
Log.i("SyncManager", "Auth OK");
Log.i("SyncManager", "Requesting commands since last sync");
Long lastSync = prefs.getLastSync();
socket.emit(EVENT_FETCH, buildFetchMessage(lastSync));
}
private JSONObject buildFetchMessage(Long lastSync)
{
try
{
JSONObject json = new JSONObject();
json.put("since", lastSync);
return json;
}
catch (JSONException e)
{
throw new RuntimeException(e);
}
}
}
private class OnConnectListener implements Emitter.Listener
{
@Override
public void call(Object... args)
{
Log.i("SyncManager", "Sending auth message");
socket.emit(EVENT_AUTH, buildAuthMessage());
}
private JSONObject buildAuthMessage()
{
try
{
JSONObject json = new JSONObject();
json.put("groupKey", GROUP_KEY);
json.put("clientId", CLIENT_ID);
json.put("version", BuildConfig.VERSION_NAME);
return json;
}
catch (JSONException e)
{
throw new RuntimeException(e);
}
}
}
private class OnDisconnectListener implements Emitter.Listener
{
@Override
public void call(Object... args)
{
readyToEmit = false;
}
}
private class OnExecuteCommandListener implements Emitter.Listener
{
@Override
public void call(Object... args)
{
try
{
Log.d("SyncManager",
String.format("Received command: %s", args[0].toString()));
JSONObject root = new JSONObject(args[0].toString());
updateLastSync(root.getLong("timestamp"));
executeCommand(root);
}
catch (JSONException e)
{
throw new RuntimeException(e);
}
}
private void executeCommand(JSONObject root) throws JSONException
{
Command received = commandParser.fromJSON(root);
for (Event e : pendingConfirmation)
{
if (e.serverId.equals(received.getId()))
{
Log.i("SyncManager", "Pending command confirmed");
pendingConfirmation.remove(e);
e.delete();
return;
}
}
Log.d("SyncManager", "Executing received command");
commandRunner.execute(received, null);
}
}
private class OnFetchOKListener implements Emitter.Listener
{
@Override
public void call(Object... args)
{
try
{
Log.i("SyncManager", "Fetch OK");
JSONObject json = (JSONObject) args[0];
updateLastSync(json.getLong("timestamp"));
emitPending();
readyToEmit = true;
}
catch (JSONException e)
{
throw new RuntimeException(e);
}
}
}
}

@ -27,9 +27,12 @@ import com.activeandroid.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.sync.*;
import java.io.*;
import java.math.*;
import java.text.*;
import java.util.*;
public abstract class DatabaseUtils
{
@ -67,6 +70,11 @@ public abstract class DatabaseUtils
return databaseFilename;
}
public static String getRandomId()
{
return new BigInteger(260, new Random()).toString(32).substring(0, 32);
}
@SuppressWarnings("unchecked")
public static void initializeActiveAndroid(Context context)
{
@ -74,7 +82,8 @@ public abstract class DatabaseUtils
.setDatabaseName(getDatabaseFilename())
.setDatabaseVersion(BuildConfig.databaseVersion)
.addModelClasses(CheckmarkRecord.class, HabitRecord.class,
RepetitionRecord.class, ScoreRecord.class, StreakRecord.class)
RepetitionRecord.class, ScoreRecord.class, StreakRecord.class,
Event.class)
.create();
try

@ -147,6 +147,11 @@
android:key="pref_feature_numerical_habits"
android:title="Enable numerical habits"/>
<EditTextPreference
android:key="pref_sync_key"
android:title="Sync: group key"
android:summary="%s"/>
</PreferenceCategory>
</PreferenceScreen>

@ -32,6 +32,7 @@ import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.junit.*;
import org.junit.runner.*;
import org.junit.runners.*;
@ -77,6 +78,8 @@ public class ListHabitsScreenTest extends BaseUnitTest
private ListHabitsScreen baseScreen;
private Preferences prefs;
@Before
@Override
public void setUp()
@ -93,10 +96,12 @@ public class ListHabitsScreenTest extends BaseUnitTest
filePickerDialogFactory = mock(FilePickerDialogFactory.class);
colorPickerDialogFactory = mock(ColorPickerDialogFactory.class);
dialogFactory = mock(EditHabitDialogFactory.class);
prefs = mock(Preferences.class);
screen = spy(new ListHabitsScreen(activity, commandRunner, dirFinder,
rootView, intentFactory, themeSwitcher, confirmDeleteDialogFactory,
filePickerDialogFactory, colorPickerDialogFactory, dialogFactory));
filePickerDialogFactory, colorPickerDialogFactory, dialogFactory,
prefs));
doNothing().when(screen).showMessage(anyInt());

Loading…
Cancel
Save