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; + } + } +}