From 1fcfb9b22edc8f185195c3d6c86c86a09a2f7cc2 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 13:41:45 -0400 Subject: [PATCH] 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();