First version of sync feature

pull/286/head
Alinson S. Xavier 10 years ago
parent 2c80544aaa
commit 1fcfb9b22e

@ -39,6 +39,7 @@ dependencies {
compile 'com.github.paolorotolo:appintro:3.4.0' compile 'com.github.paolorotolo:appintro:3.4.0'
compile 'org.apmem.tools:layouts:1.10@aar' compile 'org.apmem.tools:layouts:1.10@aar'
compile 'com.opencsv:opencsv:3.7' compile 'com.opencsv:opencsv:3.7'
compile 'com.github.nkzawa:socket.io-client:0.3.0'
compile project(':libs:drag-sort-listview:library') compile project(':libs:drag-sort-listview:library')
compile files('libs/ActiveAndroid.jar') compile files('libs/ActiveAndroid.jar')

@ -36,6 +36,8 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name="HabitsApplication" android:name="HabitsApplication"
android:allowBackup="true" android:allowBackup="true"

@ -20,30 +20,37 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.app.backup.BackupManager; import android.app.backup.BackupManager;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.helpers.ColorHelper; 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 java.util.LinkedList; 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;
abstract public class BaseActivity extends AppCompatActivity implements Thread.UncaughtExceptionHandler abstract public class BaseActivity extends AppCompatActivity implements Thread.UncaughtExceptionHandler
{ {
private static int MAX_UNDO_LEVEL = 15;
private LinkedList<Command> undoList;
private LinkedList<Command> redoList;
private Toast toast; private Toast toast;
private SyncManager sync;
Thread.UncaughtExceptionHandler androidExceptionHandler; Thread.UncaughtExceptionHandler androidExceptionHandler;
@ -57,38 +64,7 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U
androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this); Thread.setDefaultUncaughtExceptionHandler(this);
undoList = new LinkedList<>(); sync = new SyncManager(this);
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);
} }
public void showToast(Integer stringId) public void showToast(Integer stringId)
@ -99,27 +75,29 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U
toast.show(); 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); executeCommand(command, refreshKey, true);
}
if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast();
if (clearRedoStack) redoList.clear();
new AsyncTask<Void, Void, Void>() public void executeCommand(final Command command, final Long refreshKey,
final boolean shouldBroadcast)
{
new BaseTask()
{ {
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
Log.d("BaseActivity", "Executing command");
command.execute(); command.execute();
return null;
} }
@Override @Override
protected void onPostExecute(Void aVoid) protected void onPostExecute(Void aVoid)
{ {
BaseActivity.this.onPostExecuteCommand(refreshKey); BaseActivity.this.onPostExecuteCommand(command, refreshKey);
BackupManager.dataChanged("org.isoron.uhabits"); BackupManager.dataChanged("org.isoron.uhabits");
if(shouldBroadcast) sync.postCommand(command);
} }
}.execute(); }.execute();
@ -127,6 +105,19 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U
showToast(command.getExecuteStringId()); 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) protected void setupSupportActionBar(boolean homeButtonEnabled)
{ {
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
@ -144,10 +135,6 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
} }
public void onPostExecuteCommand(Long refreshKey)
{
}
@Override @Override
public void uncaughtException(Thread thread, Throwable ex) 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); view = findViewById(R.id.headerShadow);
if(view != null) view.setVisibility(View.GONE); 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);
}
} }

@ -19,9 +19,7 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
@ -40,18 +38,12 @@ import android.support.v7.app.ActionBar;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.fragments.ListHabitsFragment;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.helpers.UIHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit; 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.File;
import java.io.IOException; import java.io.IOException;
@ -122,7 +114,6 @@ public class MainActivity extends BaseActivity
}.execute(); }.execute();
} }
private void showTutorial() private void showTutorial()
{ {
Boolean firstRun = prefs.getBoolean("pref_first_run", true); Boolean firstRun = prefs.getBoolean("pref_first_run", true);
@ -263,47 +254,10 @@ public class MainActivity extends BaseActivity
} }
@Override @Override
public void onPostExecuteCommand(Long refreshKey) public void onPostExecuteCommand(Command command, Long refreshKey)
{ {
listHabitsFragment.onPostExecuteCommand(refreshKey); listHabitsFragment.onPostExecuteCommand(refreshKey);
super.onPostExecuteCommand(command, 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);
} }
@Override @Override
@ -331,4 +285,6 @@ public class MainActivity extends BaseActivity
listHabitsFragment.showImportDialog(); listHabitsFragment.showImportDialog();
} }
} }

@ -0,0 +1,151 @@
/*
* 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.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<Command> 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");
}
}
}

@ -19,8 +19,25 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import android.support.annotation.Nullable;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.json.JSONObject;
public abstract class Command 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 execute();
public abstract void undo(); public abstract void undo();
@ -34,4 +51,12 @@ public abstract class Command
{ {
return null; return null;
} }
@Nullable
public JSONObject toJSON() { return null; }
public String getId()
{
return id;
}
} }

@ -19,23 +19,35 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.json.JSONException;
import org.json.JSONObject;
public class ToggleRepetitionCommand extends Command public class ToggleRepetitionCommand extends Command
{ {
private Long offset; private final Long timestamp;
private Habit habit; private final Habit habit;
public ToggleRepetitionCommand(String id, Habit habit, long timestamp)
{
super(id);
this.timestamp = timestamp;
this.habit = habit;
}
public ToggleRepetitionCommand(Habit habit, long offset) public ToggleRepetitionCommand(Habit habit, long timestamp)
{ {
this.offset = offset; super();
this.timestamp = timestamp;
this.habit = habit; this.habit = habit;
} }
@Override @Override
public void execute() public void execute()
{ {
habit.repetitions.toggle(offset); habit.repetitions.toggle(timestamp);
} }
@Override @Override
@ -43,4 +55,39 @@ public class ToggleRepetitionCommand extends Command
{ {
execute(); 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);
}
} }

@ -45,7 +45,9 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Random;
public class DatabaseHelper public class DatabaseHelper
{ {
@ -71,6 +73,11 @@ public class DatabaseHelper
out.write(buffer, 0, numBytes); out.write(buffer, 0, numBytes);
} }
public static String getRandomId()
{
return new BigInteger(130, new Random()).toString(32);
}
public interface Command public interface Command
{ {
void execute(); void execute();

Loading…
Cancel
Save