diff --git a/app/build.gradle b/app/build.gradle index 0ee5a61f5..75e13f84f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } debug { - testCoverageEnabled = true + testCoverageEnabled = false } } diff --git a/app/src/main/java/org/isoron/uhabits/AppComponent.java b/app/src/main/java/org/isoron/uhabits/AppComponent.java index 8c07b93ea..adf4937ea 100644 --- a/app/src/main/java/org/isoron/uhabits/AppComponent.java +++ b/app/src/main/java/org/isoron/uhabits/AppComponent.java @@ -29,6 +29,7 @@ import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.sqlite.*; import org.isoron.uhabits.notifications.*; import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.sync.*; import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.utils.*; import org.isoron.uhabits.widgets.*; @@ -74,6 +75,8 @@ public interface AppComponent ReminderScheduler getReminderScheduler(); + SyncManager getSyncManager(); + TaskRunner getTaskRunner(); WidgetPreferences getWidgetPreferences(); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java index deb1b3649..6101b60e5 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java @@ -25,6 +25,7 @@ import org.isoron.uhabits.*; import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.sync.*; import org.isoron.uhabits.utils.*; /** @@ -46,6 +47,8 @@ public class ListHabitsActivity extends BaseActivity private MidnightTimer midnightTimer; + private SyncManager syncManager; + public ListHabitsComponent getListHabitsComponent() { return component; @@ -81,6 +84,7 @@ public class ListHabitsActivity extends BaseActivity rootView.setController(controller, selectionMenu); midnightTimer = component.getMidnightTimer(); + syncManager = app.getComponent().getSyncManager(); setScreen(screen); controller.onStartup(); @@ -89,6 +93,7 @@ public class ListHabitsActivity extends BaseActivity @Override protected void onPause() { + syncManager.stopListening(); midnightTimer.onPause(); screen.onDettached(); adapter.cancelRefresh(); @@ -102,6 +107,7 @@ public class ListHabitsActivity extends BaseActivity screen.onAttached(); rootView.postInvalidate(); midnightTimer.onResume(); + syncManager.startListening(); if (prefs.getTheme() == ThemeSwitcher.THEME_DARK && prefs.isPureBlackEnabled() != pureBlack) diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java index 1796d1a90..aeda45bcf 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java @@ -133,6 +133,7 @@ public class ListHabitsScreen extends BaseScreen public void onCommandExecuted(@NonNull Command command, @Nullable Long refreshKey) { + if(command.isRemote()) return; showMessage(command.getExecuteStringId()); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java index 36dadf340..0f9ee800d 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java @@ -61,6 +61,13 @@ public class SettingsFragment extends PreferenceFragmentCompat super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); + Context appContext = getContext().getApplicationContext(); + if(appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + prefs = app.getComponent().getPreferences(); + } + setResultOnPreferenceClick("importData", RESULT_IMPORT_DATA); setResultOnPreferenceClick("exportCSV", RESULT_EXPORT_CSV); setResultOnPreferenceClick("exportDB", RESULT_EXPORT_DB); @@ -68,13 +75,21 @@ public class SettingsFragment extends PreferenceFragmentCompat setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT); updateRingtoneDescription(); + updateSync(); + } - Context appContext = getContext().getApplicationContext(); - if(appContext instanceof HabitsApplication) - { - HabitsApplication app = (HabitsApplication) appContext; - prefs = app.getComponent().getPreferences(); - } + private void updateSync() + { + if(prefs == null) return; + boolean enabled = prefs.isSyncFeatureEnabled(); + + Preference syncKey = findPreference("pref_sync_key"); + syncKey.setSummary(prefs.getSyncKey()); + syncKey.setVisible(enabled); + + Preference syncAddress = findPreference("pref_sync_address"); + syncAddress.setSummary(prefs.getSyncAddress()); + syncAddress.setVisible(enabled); } @Override @@ -127,6 +142,7 @@ public class SettingsFragment extends PreferenceFragmentCompat String key) { BackupManager.dataChanged("org.isoron.uhabits"); + updateSync(); } private void setResultOnPreferenceClick(String key, final int result) 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 27794cccc..6059845e9 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/Command.java +++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java @@ -39,14 +39,18 @@ public abstract class Command { private String id; + private boolean isRemote; + public Command() { id = DatabaseUtils.getRandomId(); + isRemote = false; } public Command(String id) { this.id = id; + isRemote = false; } public abstract void execute(); @@ -71,6 +75,16 @@ public abstract class Command return null; } + public boolean isRemote() + { + return isRemote; + } + + public void setRemote(boolean remote) + { + isRemote = remote; + } + @NonNull public JSONObject toJson() { diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java index 104e308a4..1adf190a4 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java @@ -26,12 +26,15 @@ import com.google.gson.*; import org.isoron.uhabits.models.*; import org.json.*; +import javax.inject.*; + public class CommandParser { private HabitList habitList; private ModelFactory modelFactory; + @Inject public CommandParser(@NonNull HabitList habitList, @NonNull ModelFactory modelFactory) { diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java index eff7a4149..ddebd1073 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java @@ -95,9 +95,9 @@ public class CreateRepetitionCommand extends Command @NonNull public String event = "CreateRep"; - public long habitId; + public long habit; - public long timestamp; + public long repTimestamp; public int value; @@ -107,18 +107,18 @@ public class CreateRepetitionCommand extends Command Long habitId = command.habit.getId(); if(habitId == null) throw new RuntimeException("Habit not saved"); - this.habitId = habitId; - this.timestamp = command.timestamp; + this.habit = habitId; + this.repTimestamp = command.timestamp; this.value = command.value; } public CreateRepetitionCommand toCommand(@NonNull HabitList habitList) { - Habit h = habitList.getById(habitId); + Habit h = habitList.getById(habit); if(h == null) throw new HabitNotFoundException(); CreateRepetitionCommand command; - command = new CreateRepetitionCommand(h, timestamp, value); + command = new CreateRepetitionCommand(h, repTimestamp, value); command.setId(id); return command; } 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 996a406fd..556bf3e14 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -73,9 +73,9 @@ public class ToggleRepetitionCommand extends Command @NonNull public String event = "Toggle"; - public long habitId; + public long habit; - public long timestamp; + public long repTimestamp; public Record(@NonNull ToggleRepetitionCommand command) { @@ -83,17 +83,17 @@ public class ToggleRepetitionCommand extends Command Long habitId = command.habit.getId(); if(habitId == null) throw new RuntimeException("Habit not saved"); - this.timestamp = command.timestamp; - this.habitId = habitId; + this.repTimestamp = command.timestamp; + this.habit = habitId; } public ToggleRepetitionCommand toCommand(@NonNull HabitList habitList) { - Habit h = habitList.getById(habitId); + Habit h = habitList.getById(habit); if(h == null) throw new HabitNotFoundException(); ToggleRepetitionCommand command; - command = new ToggleRepetitionCommand(h, timestamp); + command = new ToggleRepetitionCommand(h, repTimestamp); command.setId(id); return command; } diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 5500f588d..bac4902c7 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -28,7 +28,6 @@ import java.util.*; import javax.inject.*; -import static android.R.attr.*; import static org.isoron.uhabits.models.Checkmark.*; /** @@ -328,7 +327,7 @@ public class Habit public boolean isNumerical() { - return type == NUMBER_HABIT; + return data.type == NUMBER_HABIT; } public HabitData getData() diff --git a/app/src/main/java/org/isoron/uhabits/preferences/Preferences.java b/app/src/main/java/org/isoron/uhabits/preferences/Preferences.java index 3ff1b2906..560b7647a 100644 --- a/app/src/main/java/org/isoron/uhabits/preferences/Preferences.java +++ b/app/src/main/java/org/isoron/uhabits/preferences/Preferences.java @@ -77,6 +77,26 @@ public class Preferences } } + public String getSyncAddress() + { + return prefs.getString("pref_sync_address", "https://sync.loophabits.org:4000"); + } + + public String getSyncClientId() + { + String id = prefs.getString("pref_sync_client_id", ""); + if(!id.isEmpty()) return id; + + id = UUID.randomUUID().toString(); + prefs.edit().putString("pref_sync_client_id", id).apply(); + return id; + } + + public boolean isSyncFeatureEnabled() + { + return prefs.getBoolean("pref_feature_sync", false); + } + public void setDefaultOrder(HabitList.Order order) { prefs.edit().putString("pref_default_order", order.name()).apply(); @@ -117,7 +137,7 @@ public class Preferences public long getLastSync() { - return prefs.getLong("lastSync", 0); + return prefs.getLong("last_sync", 0); } public void setLastSync(long timestamp) diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java index 35d536db7..f5320d430 100644 --- a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -26,7 +26,6 @@ import android.util.*; import org.isoron.uhabits.*; import org.isoron.uhabits.commands.*; import org.isoron.uhabits.preferences.*; -import org.isoron.uhabits.utils.*; import org.json.*; import java.io.*; @@ -36,6 +35,7 @@ import java.security.cert.Certificate; import java.security.cert.*; import java.util.*; +import javax.inject.*; import javax.net.ssl.*; import io.socket.client.*; @@ -44,7 +44,7 @@ import io.socket.emitter.*; import static io.socket.client.Socket.*; -public class SyncManager +public class SyncManager implements CommandRunner.Listener { public static final String EVENT_AUTH = "auth"; @@ -58,12 +58,9 @@ public class SyncManager public static final String EVENT_POST_EVENT = "postEvent"; - public static final String SYNC_SERVER_URL = - "https://sync.loophabits.org:4000"; + private String clientId; - private static String CLIENT_ID; - - private static String GROUP_KEY; + private String groupKey; @NonNull private Socket socket; @@ -84,7 +81,8 @@ public class SyncManager private CommandParser commandParser; - public SyncManager(@NonNull Context context, + @Inject + public SyncManager(@AppContext @NonNull Context context, @NonNull Preferences prefs, @NonNull CommandRunner commandRunner, @NonNull CommandParser commandParser) @@ -97,15 +95,16 @@ public class SyncManager pendingConfirmation = new LinkedList<>(); pendingEmit = Event.getAll(); - GROUP_KEY = prefs.getSyncKey(); - CLIENT_ID = DatabaseUtils.getRandomId(); + groupKey = prefs.getSyncKey(); + clientId = prefs.getSyncClientId(); + String serverURL = prefs.getSyncAddress(); - Log.d("SyncManager", CLIENT_ID); + Log.d("SyncManager", clientId); try { IO.setDefaultSSLContext(getCACertSSLContext()); - socket = IO.socket(SYNC_SERVER_URL); + socket = IO.socket(serverURL); logSocketEvent(socket, EVENT_CONNECT, "Connected"); logSocketEvent(socket, EVENT_CONNECT_TIMEOUT, "Connect timeout"); logSocketEvent(socket, EVENT_CONNECTING, "Connecting..."); @@ -124,8 +123,6 @@ public class SyncManager socket.on(EVENT_EXECUTE_EVENT, new OnExecuteCommandListener()); socket.on(EVENT_AUTH_OK, new OnAuthOKListener()); socket.on(EVENT_FETCH_OK, new OnFetchOKListener()); - - socket.connect(); } catch (URISyntaxException e) { @@ -133,14 +130,12 @@ public class SyncManager } } - public void close() + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) { - socket.off(); - socket.close(); - } + if(command.isRemote()) return; - public void postCommand(Command command) - { JSONObject msg = command.toJson(); Long now = new Date().getTime(); Event e = new Event(command.getId(), now, msg.toString()); @@ -152,6 +147,21 @@ public class SyncManager if (readyToEmit) emitPending(); } + public void startListening() + { + if(!prefs.isSyncFeatureEnabled()) return; + if(groupKey.isEmpty()) return; + + socket.connect(); + commandRunner.addListener(this); + } + + public void stopListening() + { + commandRunner.removeListener(this); + socket.close(); + } + private void emitPending() { try @@ -200,7 +210,13 @@ public class SyncManager private void logSocketEvent(Socket socket, String event, final String msg) { - socket.on(event, args -> Log.i("SyncManager", msg)); + socket.on(event, args -> + { + Log.i("SyncManager", msg); + for (Object o : args) + if (o instanceof SocketIOException) + ((SocketIOException) o).printStackTrace(); + }); } private void updateLastSync(Long timestamp) @@ -249,8 +265,8 @@ public class SyncManager try { JSONObject json = new JSONObject(); - json.put("groupKey", GROUP_KEY); - json.put("clientId", CLIENT_ID); + json.put("groupKey", groupKey); + json.put("clientId", clientId); json.put("version", BuildConfig.VERSION_NAME); return json; } @@ -292,6 +308,8 @@ public class SyncManager private void executeCommand(JSONObject root) throws JSONException { Command received = commandParser.parse(root); + received.setRemote(true); + for (Event e : pendingConfirmation) { if (e.serverId.equals(received.getId())) diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 7acafd361..62a20f148 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -147,10 +147,18 @@ android:key="pref_feature_numerical_habits" android:title="Enable numerical habits"/> + + + + + android:title="Sync key"/>