mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
First version of sync feature
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name="HabitsApplication"
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -20,30 +20,37 @@
|
||||
package org.isoron.uhabits;
|
||||
|
||||
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.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.isoron.uhabits.commands.Command;
|
||||
import org.isoron.uhabits.helpers.ColorHelper;
|
||||
import org.isoron.uhabits.helpers.UIHelper;
|
||||
|
||||
import java.util.LinkedList;
|
||||
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;
|
||||
|
||||
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 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);
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
151
app/src/main/java/org/isoron/uhabits/SyncManager.java
Normal file
151
app/src/main/java/org/isoron/uhabits/SyncManager.java
Normal file
@@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user