Persist pending events to database

pull/286/head
Alinson S. Xavier 10 years ago
parent b0040bd83c
commit e3b7e9f60f

@ -9,7 +9,7 @@ android {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 23 targetSdkVersion 23
buildConfigField "Integer", "databaseVersion", "14" buildConfigField "Integer", "databaseVersion", "16"
buildConfigField "String", "databaseFilename", "\"uhabits.db\"" buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

@ -40,6 +40,7 @@ import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.UIHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.sync.SyncManager;
import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.widgets.CheckmarkWidgetProvider; import org.isoron.uhabits.widgets.CheckmarkWidgetProvider;
import org.isoron.uhabits.widgets.FrequencyWidgetProvider; import org.isoron.uhabits.widgets.FrequencyWidgetProvider;

@ -1,181 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
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<Command> 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();
}
}

@ -38,6 +38,7 @@ import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition; import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.Streak; import org.isoron.uhabits.models.Streak;
import org.isoron.uhabits.sync.Event;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -201,7 +202,7 @@ public class DatabaseHelper
.setDatabaseName(getDatabaseFilename()) .setDatabaseName(getDatabaseFilename())
.setDatabaseVersion(BuildConfig.databaseVersion) .setDatabaseVersion(BuildConfig.databaseVersion)
.addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class, .addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class,
Streak.class) Streak.class, Event.class)
.create(); .create();
ActiveAndroid.initialize(dbConfig); ActiveAndroid.initialize(dbConfig);

@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.sync;
import android.support.annotation.NonNull;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Select;
import java.util.List;
@Table(name = "Events")
public class Event extends Model
{
@NonNull
@Column(name = "timestamp")
public Long timestamp;
@NonNull
@Column(name = "message")
public String message;
@NonNull
@Column(name = "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<Event> getAll()
{
return new Select().from(Event.class).orderBy("timestamp").execute();
}
}

@ -0,0 +1,270 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.sync;
import android.content.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<Event> pendingConfirmation;
private List<Event> 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;
}
}
}
Loading…
Cancel
Save