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();