From 1fcfb9b22edc8f185195c3d6c86c86a09a2f7cc2 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 13:41:45 -0400 Subject: [PATCH 1/8] First version of sync feature --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 + .../java/org/isoron/uhabits/BaseActivity.java | 126 +++++++++------ .../java/org/isoron/uhabits/MainActivity.java | 54 +------ .../java/org/isoron/uhabits/SyncManager.java | 151 ++++++++++++++++++ .../org/isoron/uhabits/commands/Command.java | 25 +++ .../commands/ToggleRepetitionCommand.java | 57 ++++++- .../uhabits/helpers/DatabaseHelper.java | 7 + 8 files changed, 317 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/SyncManager.java diff --git a/app/build.gradle b/app/build.gradle index 35eaaa0dc..c7dc17682 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,7 @@ dependencies { compile 'com.github.paolorotolo:appintro:3.4.0' compile 'org.apmem.tools:layouts:1.10@aar' compile 'com.opencsv:opencsv:3.7' + compile 'com.github.nkzawa:socket.io-client:0.3.0' compile project(':libs:drag-sort-listview:library') compile files('libs/ActiveAndroid.jar') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3bac419f5..a19760a1d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,8 @@ + + undoList; - private LinkedList redoList; private Toast toast; + private SyncManager sync; Thread.UncaughtExceptionHandler androidExceptionHandler; @@ -57,38 +64,7 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); - undoList = new LinkedList<>(); - redoList = new LinkedList<>(); - } - - public void executeCommand(Command command, Long refreshKey) - { - executeCommand(command, false, refreshKey); - } - - protected void undo() - { - if (undoList.isEmpty()) - { - showToast(R.string.toast_nothing_to_undo); - return; - } - - Command last = undoList.pop(); - redoList.push(last); - last.undo(); - showToast(last.getUndoStringId()); - } - - protected void redo() - { - if (redoList.isEmpty()) - { - showToast(R.string.toast_nothing_to_redo); - return; - } - Command last = redoList.pop(); - executeCommand(last, false, null); + sync = new SyncManager(this); } public void showToast(Integer stringId) @@ -99,27 +75,29 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U toast.show(); } - public void executeCommand(final Command command, Boolean clearRedoStack, final Long refreshKey) + public void executeCommand(final Command command, final Long refreshKey) { - undoList.push(command); - - if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast(); - if (clearRedoStack) redoList.clear(); + executeCommand(command, refreshKey, true); + } - new AsyncTask() + public void executeCommand(final Command command, final Long refreshKey, + final boolean shouldBroadcast) + { + new BaseTask() { @Override - protected Void doInBackground(Void... params) + protected void doInBackground() { + Log.d("BaseActivity", "Executing command"); command.execute(); - return null; } @Override protected void onPostExecute(Void aVoid) { - BaseActivity.this.onPostExecuteCommand(refreshKey); + BaseActivity.this.onPostExecuteCommand(command, refreshKey); BackupManager.dataChanged("org.isoron.uhabits"); + if(shouldBroadcast) sync.postCommand(command); } }.execute(); @@ -127,6 +105,19 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U showToast(command.getExecuteStringId()); } + public void onPostExecuteCommand(Command command, Long refreshKey) + { + new BaseTask() + { + @Override + protected void doInBackground() + { + dismissNotifications(BaseActivity.this); + updateWidgets(BaseActivity.this); + } + }.execute(); + } + protected void setupSupportActionBar(boolean homeButtonEnabled) { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); @@ -144,10 +135,6 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U actionBar.setDisplayHomeAsUpEnabled(true); } - public void onPostExecuteCommand(Long refreshKey) - { - } - @Override public void uncaughtException(Thread thread, Throwable ex) { @@ -201,4 +188,39 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U view = findViewById(R.id.headerShadow); if(view != null) view.setVisibility(View.GONE); } + + @Override + protected void onDestroy() + { + sync.close(); + super.onDestroy(); + } + + private void dismissNotifications(Context context) + { + for(Habit h : Habit.getHabitsWithReminder()) + { + if(h.checkmarks.getTodayValue() != Checkmark.UNCHECKED) + HabitBroadcastReceiver.dismissNotification(context, h); + } + } + + public static void updateWidgets(Context context) + { + updateWidgets(context, CheckmarkWidgetProvider.class); + updateWidgets(context, HistoryWidgetProvider.class); + updateWidgets(context, ScoreWidgetProvider.class); + updateWidgets(context, StreakWidgetProvider.class); + updateWidgets(context, FrequencyWidgetProvider.class); + } + + private static void updateWidgets(Context context, Class providerClass) + { + ComponentName provider = new ComponentName(context, providerClass); + Intent intent = new Intent(context, providerClass); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); + context.sendBroadcast(intent); + } } diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index a8dad23b8..907a28b7e 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -19,9 +19,7 @@ package org.isoron.uhabits; -import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -40,18 +38,12 @@ import android.support.v7.app.ActionBar; import android.view.Menu; import android.view.MenuItem; +import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.widgets.CheckmarkWidgetProvider; -import org.isoron.uhabits.widgets.FrequencyWidgetProvider; -import org.isoron.uhabits.widgets.HistoryWidgetProvider; -import org.isoron.uhabits.widgets.ScoreWidgetProvider; -import org.isoron.uhabits.widgets.StreakWidgetProvider; import java.io.File; import java.io.IOException; @@ -122,7 +114,6 @@ public class MainActivity extends BaseActivity }.execute(); } - private void showTutorial() { Boolean firstRun = prefs.getBoolean("pref_first_run", true); @@ -263,47 +254,10 @@ public class MainActivity extends BaseActivity } @Override - public void onPostExecuteCommand(Long refreshKey) + public void onPostExecuteCommand(Command command, Long refreshKey) { listHabitsFragment.onPostExecuteCommand(refreshKey); - - new BaseTask() - { - @Override - protected void doInBackground() - { - dismissNotifications(MainActivity.this); - updateWidgets(MainActivity.this); - } - }.execute(); - } - - private void dismissNotifications(Context context) - { - for(Habit h : Habit.getHabitsWithReminder()) - { - if(h.checkmarks.getTodayValue() != Checkmark.UNCHECKED) - HabitBroadcastReceiver.dismissNotification(context, h); - } - } - - public static void updateWidgets(Context context) - { - updateWidgets(context, CheckmarkWidgetProvider.class); - updateWidgets(context, HistoryWidgetProvider.class); - updateWidgets(context, ScoreWidgetProvider.class); - updateWidgets(context, StreakWidgetProvider.class); - updateWidgets(context, FrequencyWidgetProvider.class); - } - - private static void updateWidgets(Context context, Class providerClass) - { - ComponentName provider = new ComponentName(context, providerClass); - Intent intent = new Intent(context, providerClass); - intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - context.sendBroadcast(intent); + super.onPostExecuteCommand(command, refreshKey); } @Override @@ -331,4 +285,6 @@ public class MainActivity extends BaseActivity listHabitsFragment.showImportDialog(); } + + } diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java new file mode 100644 index 000000000..93ed0a3e0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.github.nkzawa.emitter.Emitter; +import com.github.nkzawa.socketio.client.IO; +import com.github.nkzawa.socketio.client.Socket; + +import org.isoron.uhabits.commands.Command; +import org.isoron.uhabits.commands.ToggleRepetitionCommand; +import org.isoron.uhabits.helpers.DatabaseHelper; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URISyntaxException; +import java.util.LinkedList; + +public class SyncManager +{ + public static final String EXECUTE_COMMAND = "executeCommand"; + public static final String POST_COMMAND = "postCommand"; + public static final String SYNC_SERVER_URL = "http://10.0.2.2:4000"; + + private static String GROUP_KEY = "sEBY3poXHFH7EyB43V2JoQUNEtBjMgdD"; + private static String CLIENT_KEY; + + @NonNull + private Socket socket; + private BaseActivity activity; + private LinkedList outbox; + + public SyncManager(BaseActivity activity) + { + this.activity = activity; + outbox = new LinkedList<>(); + CLIENT_KEY = DatabaseHelper.getRandomId(); + + try + { + socket = IO.socket(SYNC_SERVER_URL); + socket.connect(); + socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); + socket.on(EXECUTE_COMMAND, new OnExecuteCommandListener()); + } + catch (URISyntaxException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public void postCommand(Command command) + { + JSONObject msg = command.toJSON(); + if(msg != null) + { + socket.emit(POST_COMMAND, msg.toString()); + outbox.add(command); + } + } + + public void close() + { + socket.close(); + } + + private class OnConnectListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + JSONObject authMsg = buildAuthMessage(); + socket.emit("auth", authMsg.toString()); + } + + private JSONObject buildAuthMessage() + { + try + { + JSONObject json = new JSONObject(); + json.put("group_key", GROUP_KEY); + json.put("client_key", CLIENT_KEY); + json.put("version", BuildConfig.VERSION_NAME); + return json; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + } + + private class OnExecuteCommandListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + try + { + executeCommand(args[0]); + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + } + + private void executeCommand(Object arg) throws JSONException + { + Log.d("SyncManager", String.format("Received command: %s", arg.toString())); + JSONObject root = new JSONObject(arg.toString()); + if(root.getString("command").equals("ToggleRepetition")) + { + Command received = ToggleRepetitionCommand.fromJSON(root); + if(received == null) throw new RuntimeException("received is null"); + + for(Command pending : outbox) + { + if(pending.getId().equals(received.getId())) + { + outbox.remove(pending); + Log.d("SyncManager", "Received command discarded"); + return; + } + } + + activity.executeCommand(received, null, false); + Log.d("SyncManager", "Received command executed"); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/commands/Command.java b/app/src/main/java/org/isoron/uhabits/commands/Command.java index b9427e38a..ec809bf03 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/Command.java +++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java @@ -19,8 +19,25 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + +import org.isoron.uhabits.helpers.DatabaseHelper; +import org.json.JSONObject; + public abstract class Command { + private final String id; + + public Command() + { + id = DatabaseHelper.getRandomId(); + } + + public Command(String id) + { + this.id = id; + } + public abstract void execute(); public abstract void undo(); @@ -34,4 +51,12 @@ public abstract class Command { return null; } + + @Nullable + public JSONObject toJSON() { return null; } + + public String getId() + { + return id; + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index 451908433..df18f654b 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -19,23 +19,35 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.models.Habit; +import org.json.JSONException; +import org.json.JSONObject; public class ToggleRepetitionCommand extends Command { - private Long offset; - private Habit habit; + private final Long timestamp; + private final Habit habit; - public ToggleRepetitionCommand(Habit habit, long offset) + public ToggleRepetitionCommand(String id, Habit habit, long timestamp) { - this.offset = offset; + super(id); + this.timestamp = timestamp; + this.habit = habit; + } + + public ToggleRepetitionCommand(Habit habit, long timestamp) + { + super(); + this.timestamp = timestamp; this.habit = habit; } @Override public void execute() { - habit.repetitions.toggle(offset); + habit.repetitions.toggle(timestamp); } @Override @@ -43,4 +55,39 @@ public class ToggleRepetitionCommand extends Command { execute(); } + + @Nullable + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = new JSONObject(); + JSONObject data = new JSONObject(); + root.put("id", getId()); + root.put("command", "ToggleRepetition"); + data.put("habit", habit.getId()); + data.put("timestamp", timestamp); + root.put("data", data); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + @Nullable + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + Long habitId = data.getLong("habit"); + Long timestamp = data.getLong("timestamp"); + + Habit habit = Habit.get(habitId); + if(habit == null) return null; + + return new ToggleRepetitionCommand(id, habit, timestamp); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index d3c3d21e5..f6db065f3 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -45,7 +45,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.math.BigInteger; import java.text.SimpleDateFormat; +import java.util.Random; public class DatabaseHelper { @@ -71,6 +73,11 @@ public class DatabaseHelper out.write(buffer, 0, numBytes); } + public static String getRandomId() + { + return new BigInteger(130, new Random()).toString(32); + } + public interface Command { void execute(); From 41d9e2f0f5ba9dba63e3d3425b27c04694e11431 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 14:10:26 -0400 Subject: [PATCH 2/8] Sync archive/unarchive commands --- .../java/org/isoron/uhabits/BaseActivity.java | 9 ++- .../java/org/isoron/uhabits/SyncManager.java | 28 ++++--- .../commands/ArchiveHabitsCommand.java | 41 ++++++++++ .../org/isoron/uhabits/commands/Command.java | 24 +++++- .../uhabits/commands/CommandParser.java | 75 +++++++++++++++++++ .../commands/ToggleRepetitionCommand.java | 7 +- .../commands/UnarchiveHabitsCommand.java | 38 ++++++++++ 7 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/commands/CommandParser.java diff --git a/app/src/main/java/org/isoron/uhabits/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/BaseActivity.java index ec3fea9dc..455e51d60 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseActivity.java +++ b/app/src/main/java/org/isoron/uhabits/BaseActivity.java @@ -97,12 +97,13 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U { BaseActivity.this.onPostExecuteCommand(command, refreshKey); BackupManager.dataChanged("org.isoron.uhabits"); - if(shouldBroadcast) sync.postCommand(command); + if(shouldBroadcast) + { + sync.postCommand(command); + showToast(command.getExecuteStringId()); + } } }.execute(); - - - showToast(command.getExecuteStringId()); } public void onPostExecuteCommand(Command command, Long refreshKey) diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java index 93ed0a3e0..1130fef5d 100644 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -27,7 +27,7 @@ import com.github.nkzawa.socketio.client.IO; import com.github.nkzawa.socketio.client.Socket; import org.isoron.uhabits.commands.Command; -import org.isoron.uhabits.commands.ToggleRepetitionCommand; +import org.isoron.uhabits.commands.CommandParser; import org.isoron.uhabits.helpers.DatabaseHelper; import org.json.JSONException; import org.json.JSONObject; @@ -129,23 +129,21 @@ public class SyncManager { Log.d("SyncManager", String.format("Received command: %s", arg.toString())); JSONObject root = new JSONObject(arg.toString()); - if(root.getString("command").equals("ToggleRepetition")) - { - Command received = ToggleRepetitionCommand.fromJSON(root); - if(received == null) throw new RuntimeException("received is null"); - for(Command pending : outbox) + Command received = CommandParser.fromJSON(root); + if(received == null) throw new RuntimeException("received is null"); + + for(Command pending : outbox) + { + if(pending.getId().equals(received.getId())) { - if(pending.getId().equals(received.getId())) - { - outbox.remove(pending); - Log.d("SyncManager", "Received command discarded"); - return; - } + outbox.remove(pending); + Log.d("SyncManager", "Received command discarded"); + return; } - - activity.executeCommand(received, null, false); - Log.d("SyncManager", "Received command executed"); } + + activity.executeCommand(received, null, false); + Log.d("SyncManager", "Received command executed"); } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java index 25e998b7b..72086f183 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -19,9 +19,15 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.LinkedList; import java.util.List; public class ArchiveHabitsCommand extends Command @@ -29,6 +35,12 @@ public class ArchiveHabitsCommand extends Command private List habits; + public ArchiveHabitsCommand(String id, List habits) + { + super(id); + this.habits = habits; + } + public ArchiveHabitsCommand(List habits) { this.habits = habits; @@ -55,4 +67,33 @@ 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("command", "ArchiveHabits"); + data.put("habits", CommandParser.habitsToJSON(habits)); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("habits"); + + LinkedList habits = CommandParser.habitsFromJSON(habitIds); + return new ArchiveHabitsCommand(id, habits); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/Command.java b/app/src/main/java/org/isoron/uhabits/commands/Command.java index ec809bf03..375749622 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/Command.java +++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java @@ -19,11 +19,16 @@ package org.isoron.uhabits.commands; -import android.support.annotation.Nullable; +import android.support.annotation.NonNull; import org.isoron.uhabits.helpers.DatabaseHelper; +import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; +import java.util.List; + public abstract class Command { private final String id; @@ -52,8 +57,21 @@ public abstract class Command return null; } - @Nullable - public JSONObject toJSON() { return null; } + 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() { diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java new file mode 100644 index 000000000..9267740f8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.commands; + +import android.support.annotation.NonNull; + +import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.LinkedList; +import java.util.List; + +public class CommandParser +{ + public static Command fromJSON(JSONObject json) throws JSONException + { + switch(json.getString("command")) + { + case "ToggleRepetition": + return ToggleRepetitionCommand.fromJSON(json); + + case "ArchiveHabits": + return ArchiveHabitsCommand.fromJSON(json); + + case "UnarchiveHabits": + return UnarchiveHabitsCommand.fromJSON(json); + } + + return null; + } + + @NonNull + public static LinkedList habitsFromJSON(JSONArray habitIds) throws JSONException + { + LinkedList habits = new LinkedList<>(); + + for (int i = 0; i < habitIds.length(); i++) + { + Long hId = habitIds.getLong(i); + Habit h = Habit.get(hId); + if(h == null) continue; + + habits.add(h); + } + + return habits; + } + + @NonNull + protected static JSONArray habitsToJSON(List habits) + { + JSONArray habitIds = new JSONArray(); + for(Habit h : habits) habitIds.put(h.getId()); + return habitIds; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index df18f654b..e77c895db 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -56,19 +56,16 @@ public class ToggleRepetitionCommand extends Command execute(); } - @Nullable @Override public JSONObject toJSON() { try { - JSONObject root = new JSONObject(); - JSONObject data = new JSONObject(); - root.put("id", getId()); + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); root.put("command", "ToggleRepetition"); data.put("habit", habit.getId()); data.put("timestamp", timestamp); - root.put("data", data); return root; } catch (JSONException e) diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java index 612481fa7..3af0b7aeb 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -21,7 +21,11 @@ package org.isoron.uhabits.commands; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.LinkedList; import java.util.List; public class UnarchiveHabitsCommand extends Command @@ -29,6 +33,12 @@ public class UnarchiveHabitsCommand extends Command private List habits; + public UnarchiveHabitsCommand(String id, List habits) + { + super(id); + this.habits = habits; + } + public UnarchiveHabitsCommand(List habits) { this.habits = habits; @@ -55,4 +65,32 @@ public class UnarchiveHabitsCommand extends Command { return R.string.toast_habit_archived; } + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "UnarchiveHabits"); + data.put("habits", CommandParser.habitsToJSON(habits)); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("habits"); + + LinkedList habits = CommandParser.habitsFromJSON(habitIds); + return new UnarchiveHabitsCommand(id, habits); + } + } \ No newline at end of file From 56e1268f8569e149d750e0ba2640387f66faf0cc Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 17:41:03 -0400 Subject: [PATCH 3/8] Sync remaining commands --- .../java/org/isoron/uhabits/BaseActivity.java | 13 ++++-- .../java/org/isoron/uhabits/SyncManager.java | 7 +-- .../commands/ArchiveHabitsCommand.java | 6 +-- .../commands/ChangeHabitColorCommand.java | 46 ++++++++++++++++++- .../uhabits/commands/CommandParser.java | 21 ++++++++- .../uhabits/commands/CreateHabitCommand.java | 39 ++++++++++++++++ .../uhabits/commands/DeleteHabitsCommand.java | 37 +++++++++++++++ .../uhabits/commands/EditHabitCommand.java | 46 +++++++++++++++++++ .../commands/UnarchiveHabitsCommand.java | 6 +-- .../java/org/isoron/uhabits/models/Habit.java | 44 ++++++++++++++++++ 10 files changed, 249 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/BaseActivity.java index 455e51d60..be9ac786d 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseActivity.java +++ b/app/src/main/java/org/isoron/uhabits/BaseActivity.java @@ -63,7 +63,12 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); + } + @Override + protected void onResume() + { + super.onResume(); sync = new SyncManager(this); } @@ -99,7 +104,7 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U BackupManager.dataChanged("org.isoron.uhabits"); if(shouldBroadcast) { - sync.postCommand(command); + if(sync != null) sync.postCommand(command); showToast(command.getExecuteStringId()); } } @@ -191,10 +196,12 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U } @Override - protected void onDestroy() + protected void onPause() { sync.close(); - super.onDestroy(); + sync = null; + + super.onPause(); } private void dismissNotifications(Context context) diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java index 1130fef5d..055e2b4e1 100644 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -42,7 +42,7 @@ public class SyncManager public static final String SYNC_SERVER_URL = "http://10.0.2.2:4000"; private static String GROUP_KEY = "sEBY3poXHFH7EyB43V2JoQUNEtBjMgdD"; - private static String CLIENT_KEY; + private static String CLIENT_ID; @NonNull private Socket socket; @@ -53,7 +53,7 @@ public class SyncManager { this.activity = activity; outbox = new LinkedList<>(); - CLIENT_KEY = DatabaseHelper.getRandomId(); + CLIENT_ID = DatabaseHelper.getRandomId(); try { @@ -80,6 +80,7 @@ public class SyncManager public void close() { + socket.off(); socket.close(); } @@ -98,7 +99,7 @@ public class SyncManager { JSONObject json = new JSONObject(); json.put("group_key", GROUP_KEY); - json.put("client_key", CLIENT_KEY); + json.put("client_id", CLIENT_ID); json.put("version", BuildConfig.VERSION_NAME); return json; } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java index 72086f183..7f0bdea7c 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -78,7 +78,7 @@ public class ArchiveHabitsCommand extends Command JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); root.put("command", "ArchiveHabits"); - data.put("habits", CommandParser.habitsToJSON(habits)); + data.put("ids", CommandParser.habitListToJSON(habits)); return root; } catch (JSONException e) @@ -91,9 +91,9 @@ public class ArchiveHabitsCommand extends Command { String id = json.getString("id"); JSONObject data = (JSONObject) json.get("data"); - JSONArray habitIds = data.getJSONArray("habits"); + JSONArray habitIds = data.getJSONArray("ids"); - LinkedList habits = CommandParser.habitsFromJSON(habitIds); + LinkedList habits = CommandParser.habitListFromJSON(habitIds); return new ArchiveHabitsCommand(id, habits); } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java index 04ba83d7d..6d8a336ae 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java @@ -19,13 +19,15 @@ package org.isoron.uhabits.commands; -import com.activeandroid.ActiveAndroid; - import org.isoron.uhabits.R; import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; public class ChangeHabitColorCommand extends Command @@ -34,7 +36,18 @@ public class ChangeHabitColorCommand extends Command List originalColors; Integer newColor; + public ChangeHabitColorCommand(String id, List habits, Integer newColor) + { + super(id); + init(habits, newColor); + } + public ChangeHabitColorCommand(List habits, Integer newColor) + { + init(habits, newColor); + } + + private void init(List habits, Integer newColor) { this.habits = habits; this.newColor = newColor; @@ -77,4 +90,33 @@ public class ChangeHabitColorCommand extends Command { return R.string.toast_habit_changed; } + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "ChangeHabitColor"); + data.put("ids", CommandParser.habitListToJSON(habits)); + data.put("color", newColor); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("ids"); + int newColor = data.getInt("color"); + + LinkedList habits = CommandParser.habitListFromJSON(habitIds); + return new ChangeHabitColorCommand(id, habits, newColor); + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java index 9267740f8..c39bb9e10 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java @@ -43,13 +43,30 @@ public class CommandParser case "UnarchiveHabits": return UnarchiveHabitsCommand.fromJSON(json); + + case "ChangeHabitColor": + return ChangeHabitColorCommand.fromJSON(json); + + case "CreateHabit": + return CreateHabitCommand.fromJSON(json); + + case "DeleteHabits": + return DeleteHabitsCommand.fromJSON(json); + + case "EditHabit": + return EditHabitCommand.fromJSON(json); + +// TODO: Implement this +// case "ReorderHabit": +// return ReorderHabitCommand.fromJSON(json); + } return null; } @NonNull - public static LinkedList habitsFromJSON(JSONArray habitIds) throws JSONException + public static LinkedList habitListFromJSON(JSONArray habitIds) throws JSONException { LinkedList habits = new LinkedList<>(); @@ -66,7 +83,7 @@ public class CommandParser } @NonNull - protected static JSONArray habitsToJSON(List habits) + protected static JSONArray habitListToJSON(List habits) { JSONArray habitIds = new JSONArray(); for(Habit h : habits) habitIds.put(h.getId()); diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java index 7cc9ad51c..762ffbbd5 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java @@ -19,14 +19,25 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONException; +import org.json.JSONObject; public class CreateHabitCommand extends Command { private Habit model; private Long savedId; + public CreateHabitCommand(String id, Habit model, Long savedId) + { + super(id); + this.model = model; + this.savedId = savedId; + } + public CreateHabitCommand(Habit model) { this.model = model; @@ -68,4 +79,32 @@ 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("command", "CreateHabit"); + data.put("habit", model.toJSON()); + data.put("id", savedId); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + @Nullable + public static Command fromJSON(JSONObject root) throws JSONException + { + String commandId = root.getString("id"); + JSONObject data = (JSONObject) root.get("data"); + Habit model = Habit.fromJSON(data.getJSONObject("habit")); + Long savedId = data.getLong("id"); + + return new CreateHabitCommand(commandId, model, savedId); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java index 34e26c50c..ca8ed0a78 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java @@ -21,13 +21,23 @@ package org.isoron.uhabits.commands; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.LinkedList; import java.util.List; public class DeleteHabitsCommand extends Command { private List habits; + public DeleteHabitsCommand(String id, List habits) + { + super(id); + this.habits = habits; + } + public DeleteHabitsCommand(List habits) { this.habits = habits; @@ -57,4 +67,31 @@ public class DeleteHabitsCommand extends Command { return R.string.toast_habit_restored; } + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "DeleteHabits"); + data.put("ids", CommandParser.habitListToJSON(habits)); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("ids"); + + LinkedList habits = CommandParser.habitListFromJSON(habitIds); + return new DeleteHabitsCommand(id, habits); + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java index 7a7787d6a..59e4a1506 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -19,8 +19,12 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONException; +import org.json.JSONObject; public class EditHabitCommand extends Command { @@ -29,7 +33,18 @@ public class EditHabitCommand extends Command private long savedId; private boolean hasIntervalChanged; + public EditHabitCommand(String id, Habit original, Habit modified) + { + super(id); + init(original, modified); + } + public EditHabitCommand(Habit original, Habit modified) + { + init(original, modified); + } + + private void init(Habit original, Habit modified) { this.savedId = original.getId(); this.modified = new Habit(modified); @@ -81,4 +96,35 @@ 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("command", "EditHabit"); + data.put("id", savedId); + data.put("params", modified.toJSON()); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + @Nullable + public static Command fromJSON(JSONObject root) throws JSONException + { + String commandId = root.getString("id"); + JSONObject data = (JSONObject) root.get("data"); + Habit original = Habit.get(data.getLong("id")); + if(original == null) return null; + + Habit modified = Habit.fromJSON(data.getJSONObject("params")); + + return new EditHabitCommand(commandId, original, modified); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java index 3af0b7aeb..31fd680d5 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -74,7 +74,7 @@ public class UnarchiveHabitsCommand extends Command JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); root.put("command", "UnarchiveHabits"); - data.put("habits", CommandParser.habitsToJSON(habits)); + data.put("ids", CommandParser.habitListToJSON(habits)); return root; } catch (JSONException e) @@ -87,9 +87,9 @@ public class UnarchiveHabitsCommand extends Command { String id = json.getString("id"); JSONObject data = (JSONObject) json.get("data"); - JSONArray habitIds = data.getJSONArray("habits"); + JSONArray habitIds = data.getJSONArray("ids"); - LinkedList habits = CommandParser.habitsFromJSON(habitIds); + LinkedList habits = CommandParser.habitListFromJSON(habitIds); return new UnarchiveHabitsCommand(id, habits); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 1fdb982ad..e97540331 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -37,6 +37,8 @@ import com.opencsv.CSVWriter; import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.DateHelper; +import org.json.JSONException; +import org.json.JSONObject; import java.io.IOException; import java.io.Writer; @@ -510,4 +512,46 @@ public class Habit extends Model csv.close(); } + + public JSONObject toJSON() + { + try + { + JSONObject json = new JSONObject(); + json.put("name", name); + json.put("description", description); + json.put("freqNum", freqNum); + json.put("freqDen", freqDen); + json.put("color", color); + json.put("position", position); + json.put("reminderHour", reminderHour); + json.put("reminderMin", reminderMin); + json.put("reminderDays", reminderDays); + json.put("archived", archived); + return json; + } + catch(JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Habit fromJSON(JSONObject json) throws JSONException + { + Habit habit = new Habit(); + habit.name = json.getString("name"); + habit.description = json.getString("description"); + habit.freqNum = json.getInt("freqNum"); + habit.freqDen = json.getInt("freqDen"); + habit.color = json.getInt("color"); + habit.position = json.getInt("position"); + habit.archived = json.getInt("archived"); + if(json.has("reminderHour")) + { + habit.reminderHour = json.getInt("reminderHour"); + habit.reminderMin = json.getInt("reminderMin"); + habit.reminderDays = json.getInt("reminderDays"); + } + return habit; + } } From b0040bd83c72ace15f4371941458ae4fea082936 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 23:19:14 -0400 Subject: [PATCH 4/8] Fetch commands since last sync --- app/build.gradle | 6 ++- .../java/org/isoron/uhabits/SyncManager.java | 47 +++++++++++++++---- .../uhabits/fragments/SettingsFragment.java | 10 ++++ app/src/main/res/xml/preferences.xml | 11 +++++ 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c7dc17682..47131224d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,11 @@ dependencies { compile 'com.github.paolorotolo:appintro:3.4.0' compile 'org.apmem.tools:layouts:1.10@aar' compile 'com.opencsv:opencsv:3.7' - compile 'com.github.nkzawa:socket.io-client:0.3.0' + + compile ('io.socket:socket.io-client:0.7.0') { + exclude group: 'org.json', module: 'json' + } + compile project(':libs:drag-sort-listview:library') compile files('libs/ActiveAndroid.jar') diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java index 055e2b4e1..bbfbf73b6 100644 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -19,13 +19,11 @@ package org.isoron.uhabits; +import android.content.SharedPreferences; import android.support.annotation.NonNull; +import android.support.v7.preference.PreferenceManager; import android.util.Log; -import com.github.nkzawa.emitter.Emitter; -import com.github.nkzawa.socketio.client.IO; -import com.github.nkzawa.socketio.client.Socket; - import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.CommandParser; import org.isoron.uhabits.helpers.DatabaseHelper; @@ -35,14 +33,19 @@ import org.json.JSONObject; import java.net.URISyntaxException; import java.util.LinkedList; +import io.socket.client.IO; +import io.socket.client.Socket; +import io.socket.emitter.Emitter; + public class SyncManager { public static final String EXECUTE_COMMAND = "executeCommand"; public static final String POST_COMMAND = "postCommand"; - public static final String SYNC_SERVER_URL = "http://10.0.2.2:4000"; + public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; - private static String GROUP_KEY = "sEBY3poXHFH7EyB43V2JoQUNEtBjMgdD"; + private static String GROUP_KEY; private static String CLIENT_ID; + private final SharedPreferences prefs; @NonNull private Socket socket; @@ -53,6 +56,9 @@ public class SyncManager { this.activity = activity; outbox = new LinkedList<>(); + + prefs = PreferenceManager.getDefaultSharedPreferences(activity); + GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); CLIENT_ID = DatabaseHelper.getRandomId(); try @@ -91,6 +97,24 @@ public class SyncManager { JSONObject authMsg = buildAuthMessage(); socket.emit("auth", authMsg.toString()); + + Long lastSync = prefs.getLong("lastSync", 0); + JSONObject fetchMsg = buildFetchMessage(lastSync); + socket.emit("fetchCommands", fetchMsg.toString()); + } + + private JSONObject buildFetchMessage(Long lastSync) + { + try + { + JSONObject json = new JSONObject(); + json.put("since", lastSync); + return json; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } } private JSONObject buildAuthMessage() @@ -98,8 +122,8 @@ public class SyncManager try { JSONObject json = new JSONObject(); - json.put("group_key", GROUP_KEY); - json.put("client_id", CLIENT_ID); + json.put("groupKey", GROUP_KEY); + json.put("clientId", CLIENT_ID); json.put("version", BuildConfig.VERSION_NAME); return json; } @@ -117,6 +141,8 @@ public class SyncManager { try { + JSONObject root = new JSONObject(args[0].toString()); + updateLastSync(root.getLong("timestamp")); executeCommand(args[0]); } catch (JSONException e) @@ -147,4 +173,9 @@ public class SyncManager activity.executeCommand(received, null, false); Log.d("SyncManager", "Received command executed"); } + + private void updateLastSync(Long timestamp) + { + prefs.edit().putLong("lastSync", timestamp).apply(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java index 838527589..cbc56333b 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java @@ -49,6 +49,7 @@ public class SettingsFragment extends PreferenceFragmentCompat setResultOnPreferenceClick("bugReport", MainActivity.RESULT_BUG_REPORT); updateRingtoneDescription(); + updateSyncDescription(); if(UIHelper.isLocaleFullyTranslated()) removePreference("translate", "linksCategory"); @@ -101,9 +102,18 @@ public class SettingsFragment extends PreferenceFragmentCompat @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals("syncKey")) + updateSyncDescription(); + BackupManager.dataChanged("org.isoron.uhabits"); } + private void updateSyncDescription() + { + SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); + findPreference("syncKey").setSummary(preferences.getString("syncKey", "")); + } + @Override public boolean onPreferenceTreeClick(Preference preference) { diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 1339b2d05..5a474d829 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -88,6 +88,17 @@ + + + + + + From e3b7e9f60f37f24362b84f9cba51f0cdad516472 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 16 May 2016 08:20:08 -0400 Subject: [PATCH 5/8] Persist pending events to database --- app/build.gradle | 2 +- .../java/org/isoron/uhabits/BaseActivity.java | 1 + .../java/org/isoron/uhabits/SyncManager.java | 181 ------------ .../uhabits/helpers/DatabaseHelper.java | 3 +- .../java/org/isoron/uhabits/sync/Event.java | 65 +++++ .../org/isoron/uhabits/sync/SyncManager.java | 270 ++++++++++++++++++ 6 files changed, 339 insertions(+), 183 deletions(-) delete mode 100644 app/src/main/java/org/isoron/uhabits/SyncManager.java create mode 100644 app/src/main/java/org/isoron/uhabits/sync/Event.java create mode 100644 app/src/main/java/org/isoron/uhabits/sync/SyncManager.java diff --git a/app/build.gradle b/app/build.gradle index 47131224d..848e68e00 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 15 targetSdkVersion 23 - buildConfigField "Integer", "databaseVersion", "14" + buildConfigField "Integer", "databaseVersion", "16" buildConfigField "String", "databaseFilename", "\"uhabits.db\"" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/org/isoron/uhabits/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/BaseActivity.java index be9ac786d..4b0398495 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseActivity.java +++ b/app/src/main/java/org/isoron/uhabits/BaseActivity.java @@ -40,6 +40,7 @@ import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.UIHelper; import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.sync.SyncManager; import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.widgets.CheckmarkWidgetProvider; import org.isoron.uhabits.widgets.FrequencyWidgetProvider; diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java deleted file mode 100644 index bbfbf73b6..000000000 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits; - -import android.content.SharedPreferences; -import android.support.annotation.NonNull; -import android.support.v7.preference.PreferenceManager; -import android.util.Log; - -import org.isoron.uhabits.commands.Command; -import org.isoron.uhabits.commands.CommandParser; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.json.JSONException; -import org.json.JSONObject; - -import java.net.URISyntaxException; -import java.util.LinkedList; - -import io.socket.client.IO; -import io.socket.client.Socket; -import io.socket.emitter.Emitter; - -public class SyncManager -{ - public static final String EXECUTE_COMMAND = "executeCommand"; - public static final String POST_COMMAND = "postCommand"; - public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; - - private static String GROUP_KEY; - private static String CLIENT_ID; - private final SharedPreferences prefs; - - @NonNull - private Socket socket; - private BaseActivity activity; - private LinkedList outbox; - - public SyncManager(BaseActivity activity) - { - this.activity = activity; - outbox = new LinkedList<>(); - - prefs = PreferenceManager.getDefaultSharedPreferences(activity); - GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); - CLIENT_ID = DatabaseHelper.getRandomId(); - - try - { - socket = IO.socket(SYNC_SERVER_URL); - socket.connect(); - socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); - socket.on(EXECUTE_COMMAND, new OnExecuteCommandListener()); - } - catch (URISyntaxException e) - { - throw new RuntimeException(e.getMessage()); - } - } - - public void postCommand(Command command) - { - JSONObject msg = command.toJSON(); - if(msg != null) - { - socket.emit(POST_COMMAND, msg.toString()); - outbox.add(command); - } - } - - public void close() - { - socket.off(); - socket.close(); - } - - private class OnConnectListener implements Emitter.Listener - { - @Override - public void call(Object... args) - { - JSONObject authMsg = buildAuthMessage(); - socket.emit("auth", authMsg.toString()); - - Long lastSync = prefs.getLong("lastSync", 0); - JSONObject fetchMsg = buildFetchMessage(lastSync); - socket.emit("fetchCommands", fetchMsg.toString()); - } - - private JSONObject buildFetchMessage(Long lastSync) - { - try - { - JSONObject json = new JSONObject(); - json.put("since", lastSync); - return json; - } - catch (JSONException e) - { - throw new RuntimeException(e.getMessage()); - } - } - - 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.getMessage()); - } - } - } - - private class OnExecuteCommandListener implements Emitter.Listener - { - @Override - public void call(Object... args) - { - try - { - JSONObject root = new JSONObject(args[0].toString()); - updateLastSync(root.getLong("timestamp")); - executeCommand(args[0]); - } - catch (JSONException e) - { - throw new RuntimeException(e.getMessage()); - } - } - } - - private void executeCommand(Object arg) throws JSONException - { - Log.d("SyncManager", String.format("Received command: %s", arg.toString())); - JSONObject root = new JSONObject(arg.toString()); - - Command received = CommandParser.fromJSON(root); - if(received == null) throw new RuntimeException("received is null"); - - for(Command pending : outbox) - { - if(pending.getId().equals(received.getId())) - { - outbox.remove(pending); - Log.d("SyncManager", "Received command discarded"); - return; - } - } - - activity.executeCommand(received, null, false); - Log.d("SyncManager", "Received command executed"); - } - - private void updateLastSync(Long timestamp) - { - prefs.edit().putLong("lastSync", timestamp).apply(); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index f6db065f3..a453a5543 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -38,6 +38,7 @@ import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Repetition; import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.Streak; +import org.isoron.uhabits.sync.Event; import java.io.File; import java.io.FileInputStream; @@ -201,7 +202,7 @@ public class DatabaseHelper .setDatabaseName(getDatabaseFilename()) .setDatabaseVersion(BuildConfig.databaseVersion) .addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class, - Streak.class) + Streak.class, Event.class) .create(); ActiveAndroid.initialize(dbConfig); diff --git a/app/src/main/java/org/isoron/uhabits/sync/Event.java b/app/src/main/java/org/isoron/uhabits/sync/Event.java new file mode 100644 index 000000000..67308dc72 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/sync/Event.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 = "serverId") + 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 getAll() + { + return new Select().from(Event.class).orderBy("timestamp").execute(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java new file mode 100644 index 000000000..dfddc89c2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.sync; + +import android.content.SharedPreferences; +import android.support.annotation.NonNull; +import android.support.v7.preference.PreferenceManager; +import android.util.Log; + +import org.isoron.uhabits.BaseActivity; +import org.isoron.uhabits.BuildConfig; +import org.isoron.uhabits.commands.Command; +import org.isoron.uhabits.commands.CommandParser; +import org.isoron.uhabits.helpers.DatabaseHelper; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URISyntaxException; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import io.socket.client.IO; +import io.socket.client.Socket; +import io.socket.emitter.Emitter; + +public class SyncManager +{ + public static final String EVENT_AUTH = "auth"; + public static final String EVENT_AUTH_OK = "authOK"; + public static final String EVENT_EXECUTE_COMMAND = "execute"; + public static final String EVENT_POST_COMMAND = "post"; + public static final String EVENT_FETCH = "fetch"; + public static final String EVENT_FETCH_OK = "fetchOK"; + + public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; + + private static String GROUP_KEY; + private static String CLIENT_ID; + private final SharedPreferences prefs; + + @NonNull + private Socket socket; + private BaseActivity activity; + private LinkedList pendingConfirmation; + private List pendingEmit; + private boolean readyToEmit = false; + + public SyncManager(final BaseActivity activity) + { + this.activity = activity; + pendingConfirmation = new LinkedList<>(); + pendingEmit = Event.getAll(); + + prefs = PreferenceManager.getDefaultSharedPreferences(activity); + GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); + CLIENT_ID = DatabaseHelper.getRandomId(); + + try + { + socket = IO.socket(SYNC_SERVER_URL); + + logSocketEvent(socket, Socket.EVENT_CONNECT, "Connected"); + logSocketEvent(socket, Socket.EVENT_CONNECT_TIMEOUT, "Connect timeout"); + logSocketEvent(socket, Socket.EVENT_CONNECTING, "Connecting..."); + logSocketEvent(socket, Socket.EVENT_CONNECT_ERROR, "Connect error"); + logSocketEvent(socket, Socket.EVENT_DISCONNECT, "Disconnected"); + logSocketEvent(socket, Socket.EVENT_RECONNECT, "Reconnected"); + logSocketEvent(socket, Socket.EVENT_RECONNECT_ATTEMPT, "Reconnecting..."); + logSocketEvent(socket, Socket.EVENT_RECONNECT_ERROR, "Reconnect error"); + logSocketEvent(socket, Socket.EVENT_RECONNECT_FAILED, "Reconnect failed"); + logSocketEvent(socket, Socket.EVENT_DISCONNECT, "Disconnected"); + logSocketEvent(socket, Socket.EVENT_PING, "Ping"); + logSocketEvent(socket, Socket.EVENT_PONG, "Pong"); + + socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); + socket.on(Socket.EVENT_DISCONNECT, new OnDisconnectListener()); + socket.on(EVENT_EXECUTE_COMMAND, 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.getMessage()); + } + } + + private void logSocketEvent(Socket socket, String event, final String msg) + { + socket.on(event, new Emitter.Listener() + { + @Override + public void call(Object... args) + { + Log.i("SyncManager", msg); + } + }); + } + + 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() + { + for (Event e : pendingEmit) + { + Log.i("SyncManager", "Emitting: " + e.message); + socket.emit(EVENT_POST_COMMAND, e.message); + pendingConfirmation.add(e); + } + + pendingEmit.clear(); + } + + public void close() + { + socket.off(); + socket.close(); + } + + private class OnConnectListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + Log.i("SyncManager", "Sending auth message"); + JSONObject authMsg = buildAuthMessage(); + socket.emit(EVENT_AUTH, authMsg.toString()); + } + + 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.getMessage()); + } + } + } + + 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.getMessage()); + } + } + + private void updateLastSync(Long timestamp) + { + prefs.edit().putLong("lastSync", timestamp).apply(); + } + + private void executeCommand(JSONObject root) throws JSONException + { + Command received = CommandParser.fromJSON(root); + if(received == null) throw new RuntimeException("received is null"); + + 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"); + activity.executeCommand(received, null, false); + } + } + + 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.getLong("lastSync", 0); + JSONObject fetchMsg = buildFetchMessage(lastSync); + socket.emit(EVENT_FETCH, fetchMsg.toString()); + } + + private JSONObject buildFetchMessage(Long lastSync) + { + try + { + JSONObject json = new JSONObject(); + json.put("since", lastSync); + return json; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + } + + private class OnFetchOKListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + Log.i("SyncManager", "Fetch OK"); + emitPending(); + readyToEmit = true; + } + } + + private class OnDisconnectListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + readyToEmit = false; + } + } +} From 5b402478e986a0883dee433ca2fe5f23e6365193 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 16 May 2016 22:29:10 -0400 Subject: [PATCH 6/8] Use encrypted connection to server (TLS) --- app/src/main/assets/cacert.pem | 41 +++++++++++ .../org/isoron/uhabits/sync/SyncManager.java | 73 +++++++++++++++---- 2 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 app/src/main/assets/cacert.pem diff --git a/app/src/main/assets/cacert.pem b/app/src/main/assets/cacert.pem new file mode 100644 index 000000000..e7dfc8294 --- /dev/null +++ b/app/src/main/assets/cacert.pem @@ -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----- diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java index dfddc89c2..c6c42ce40 100644 --- a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -32,11 +32,18 @@ import org.isoron.uhabits.helpers.DatabaseHelper; import org.json.JSONException; import org.json.JSONObject; +import java.io.InputStream; import java.net.URISyntaxException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; import java.util.Date; import java.util.LinkedList; import java.util.List; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + import io.socket.client.IO; import io.socket.client.Socket; import io.socket.emitter.Emitter; @@ -50,7 +57,7 @@ public class SyncManager public static final String EVENT_FETCH = "fetch"; public static final String EVENT_FETCH_OK = "fetchOK"; - public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; + public static final String SYNC_SERVER_URL = "https://sync.loophabits.org:4000"; private static String GROUP_KEY; private static String CLIENT_ID; @@ -75,6 +82,8 @@ public class SyncManager try { + IO.setDefaultSSLContext(getCACertSSLContext()); + socket = IO.socket(SYNC_SERVER_URL); logSocketEvent(socket, Socket.EVENT_CONNECT, "Connected"); @@ -100,7 +109,34 @@ public class SyncManager } catch (URISyntaxException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); + } + } + + private SSLContext getCACertSSLContext() + { + try + { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + InputStream caInput = activity.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); } } @@ -170,7 +206,7 @@ public class SyncManager } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); } } } @@ -190,15 +226,10 @@ public class SyncManager } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); } } - private void updateLastSync(Long timestamp) - { - prefs.edit().putLong("lastSync", timestamp).apply(); - } - private void executeCommand(JSONObject root) throws JSONException { Command received = CommandParser.fromJSON(root); @@ -243,7 +274,7 @@ public class SyncManager } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); } } } @@ -253,9 +284,20 @@ public class SyncManager @Override public void call(Object... args) { - Log.i("SyncManager", "Fetch OK"); - emitPending(); - readyToEmit = true; + try + { + Log.i("SyncManager", "Fetch OK"); + + JSONObject json = new JSONObject((String) args[0]); + updateLastSync(json.getLong("timestamp")); + + emitPending(); + readyToEmit = true; + } + catch (JSONException e) + { + throw new RuntimeException(e); + } } } @@ -267,4 +309,9 @@ public class SyncManager readyToEmit = false; } } + + private void updateLastSync(Long timestamp) + { + prefs.edit().putLong("lastSync", timestamp).apply(); + } } From 2399dccddc912186b20c1a459285e0b5ba830710 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 19 May 2016 10:35:44 -0400 Subject: [PATCH 7/8] Emit JSON object instead of string --- .../org/isoron/uhabits/sync/SyncManager.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java index c6c42ce40..92f8f98c7 100644 --- a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -162,20 +162,28 @@ public class SyncManager e.save(); Log.i("SyncManager", "Adding to outbox: " + msg.toString()); + pendingEmit.add(e); if(readyToEmit) emitPending(); } private void emitPending() { - for (Event e : pendingEmit) + try { - Log.i("SyncManager", "Emitting: " + e.message); - socket.emit(EVENT_POST_COMMAND, e.message); - pendingConfirmation.add(e); - } + for (Event e : pendingEmit) + { + Log.i("SyncManager", "Emitting: " + e.message); + socket.emit(EVENT_POST_COMMAND, new JSONObject(e.message)); + pendingConfirmation.add(e); + } - pendingEmit.clear(); + pendingEmit.clear(); + } + catch (JSONException e) + { + throw new RuntimeException(e); + } } public void close() @@ -190,8 +198,7 @@ public class SyncManager public void call(Object... args) { Log.i("SyncManager", "Sending auth message"); - JSONObject authMsg = buildAuthMessage(); - socket.emit(EVENT_AUTH, authMsg.toString()); + socket.emit(EVENT_AUTH, buildAuthMessage()); } private JSONObject buildAuthMessage() @@ -219,7 +226,6 @@ public class SyncManager 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); @@ -260,8 +266,7 @@ public class SyncManager Log.i("SyncManager", "Requesting commands since last sync"); Long lastSync = prefs.getLong("lastSync", 0); - JSONObject fetchMsg = buildFetchMessage(lastSync); - socket.emit(EVENT_FETCH, fetchMsg.toString()); + socket.emit(EVENT_FETCH, buildFetchMessage(lastSync)); } private JSONObject buildFetchMessage(Long lastSync) From e6e80b98419d4ae67865697eec69da97f6c7fa2e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 22 May 2016 18:28:06 -0400 Subject: [PATCH 8/8] Update sync protocol --- .../uhabits/commands/ArchiveHabitsCommand.java | 2 +- .../uhabits/commands/ChangeHabitColorCommand.java | 2 +- .../org/isoron/uhabits/commands/CommandParser.java | 2 +- .../uhabits/commands/CreateHabitCommand.java | 2 +- .../uhabits/commands/DeleteHabitsCommand.java | 2 +- .../isoron/uhabits/commands/EditHabitCommand.java | 2 +- .../uhabits/commands/ToggleRepetitionCommand.java | 2 +- .../uhabits/commands/UnarchiveHabitsCommand.java | 2 +- .../org/isoron/uhabits/helpers/DatabaseHelper.java | 2 +- .../java/org/isoron/uhabits/sync/SyncManager.java | 14 ++++++++------ 10 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java index 7f0bdea7c..697b3cc10 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -77,7 +77,7 @@ public class ArchiveHabitsCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "ArchiveHabits"); + root.put("event", "ArchiveHabits"); data.put("ids", CommandParser.habitListToJSON(habits)); return root; } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java index 6d8a336ae..437594ea5 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java @@ -98,7 +98,7 @@ public class ChangeHabitColorCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "ChangeHabitColor"); + root.put("event", "ChangeHabitColor"); data.put("ids", CommandParser.habitListToJSON(habits)); data.put("color", newColor); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java index c39bb9e10..34a02f981 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java @@ -33,7 +33,7 @@ public class CommandParser { public static Command fromJSON(JSONObject json) throws JSONException { - switch(json.getString("command")) + switch(json.getString("event")) { case "ToggleRepetition": return ToggleRepetitionCommand.fromJSON(json); diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java index 762ffbbd5..c2d23cf66 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java @@ -86,7 +86,7 @@ public class CreateHabitCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "CreateHabit"); + root.put("event", "CreateHabit"); data.put("habit", model.toJSON()); data.put("id", savedId); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java index ca8ed0a78..1a851e94b 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java @@ -75,7 +75,7 @@ public class DeleteHabitsCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "DeleteHabits"); + root.put("event", "DeleteHabits"); data.put("ids", CommandParser.habitListToJSON(habits)); return root; } diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java index 59e4a1506..5c8dcc4d4 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -104,7 +104,7 @@ public class EditHabitCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "EditHabit"); + root.put("event", "EditHabit"); data.put("id", savedId); data.put("params", modified.toJSON()); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index e77c895db..6dffa4781 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -63,7 +63,7 @@ public class ToggleRepetitionCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "ToggleRepetition"); + root.put("event", "ToggleRepetition"); data.put("habit", habit.getId()); data.put("timestamp", timestamp); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java index 31fd680d5..d15a32810 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -73,7 +73,7 @@ public class UnarchiveHabitsCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "UnarchiveHabits"); + root.put("event", "UnarchiveHabits"); data.put("ids", CommandParser.habitListToJSON(habits)); return root; } diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index a453a5543..b94a6902c 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -76,7 +76,7 @@ public class DatabaseHelper public static String getRandomId() { - return new BigInteger(130, new Random()).toString(32); + return new BigInteger(260, new Random()).toString(32).substring(0, 32); } public interface Command diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java index 92f8f98c7..e8c05e8b6 100644 --- a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -52,8 +52,8 @@ public class SyncManager { public static final String EVENT_AUTH = "auth"; public static final String EVENT_AUTH_OK = "authOK"; - public static final String EVENT_EXECUTE_COMMAND = "execute"; - public static final String EVENT_POST_COMMAND = "post"; + public static final String EVENT_EXECUTE_EVENT = "execute"; + public static final String EVENT_POST_EVENT = "postEvent"; public static final String EVENT_FETCH = "fetch"; public static final String EVENT_FETCH_OK = "fetchOK"; @@ -80,6 +80,8 @@ public class SyncManager GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); CLIENT_ID = DatabaseHelper.getRandomId(); + Log.d("SyncManager", DatabaseHelper.getRandomId()); + try { IO.setDefaultSSLContext(getCACertSSLContext()); @@ -101,7 +103,7 @@ public class SyncManager socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); socket.on(Socket.EVENT_DISCONNECT, new OnDisconnectListener()); - socket.on(EVENT_EXECUTE_COMMAND, new OnExecuteCommandListener()); + socket.on(EVENT_EXECUTE_EVENT, new OnExecuteCommandListener()); socket.on(EVENT_AUTH_OK, new OnAuthOKListener()); socket.on(EVENT_FETCH_OK, new OnFetchOKListener()); @@ -174,7 +176,7 @@ public class SyncManager for (Event e : pendingEmit) { Log.i("SyncManager", "Emitting: " + e.message); - socket.emit(EVENT_POST_COMMAND, new JSONObject(e.message)); + socket.emit(EVENT_POST_EVENT, new JSONObject(e.message)); pendingConfirmation.add(e); } @@ -293,7 +295,7 @@ public class SyncManager { Log.i("SyncManager", "Fetch OK"); - JSONObject json = new JSONObject((String) args[0]); + JSONObject json = (JSONObject) args[0]; updateLastSync(json.getLong("timestamp")); emitPending(); @@ -317,6 +319,6 @@ public class SyncManager private void updateLastSync(Long timestamp) { - prefs.edit().putLong("lastSync", timestamp).apply(); + prefs.edit().putLong("lastSync", timestamp + 1).apply(); } }