From 659c528744b67eb3243ebbd201c9d0b566781723 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 24 Nov 2020 06:55:37 -0600 Subject: [PATCH] SyncManager: First version --- .../uhabits/HabitsApplicationComponent.java | 3 + .../habits/list/ListHabitsActivity.kt | 7 +- .../habits/list/ListHabitsScreen.kt | 9 +- .../activities/settings/SettingsFragment.java | 16 ++- .../uhabits/activities/sync/SyncActivity.kt | 17 +++- .../isoron/uhabits/sync/RemoteSyncServer.kt | 6 +- .../org/isoron/uhabits/sync/SyncManager.kt | 99 +++++++++++++++++++ .../src/main/res/values/strings.xml | 1 + .../src/main/res/xml/preferences.xml | 7 ++ .../uhabits/core/preferences/Preferences.java | 15 +++ .../habits/list/ListHabitsBehavior.java | 11 ++- 11 files changed, 166 insertions(+), 25 deletions(-) create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.kt diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplicationComponent.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplicationComponent.java index cc5e55b20..f20c9254f 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplicationComponent.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplicationComponent.java @@ -34,6 +34,7 @@ import org.isoron.uhabits.core.ui.screens.habits.list.*; import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.intents.*; import org.isoron.uhabits.receivers.*; +import org.isoron.uhabits.sync.*; import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.widgets.*; @@ -85,4 +86,6 @@ public interface HabitsApplicationComponent WidgetPreferences getWidgetPreferences(); WidgetUpdater getWidgetUpdater(); + + SyncManager getSyncManager(); } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt index 227aeb674..61179d467 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt @@ -28,6 +28,7 @@ import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.core.ui.ThemeSwitcher.* import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.database.* +import org.isoron.uhabits.sync.* class ListHabitsActivity : HabitsActivity() { @@ -38,10 +39,12 @@ class ListHabitsActivity : HabitsActivity() { lateinit var screen: ListHabitsScreen lateinit var prefs: Preferences lateinit var midnightTimer: MidnightTimer + lateinit var syncManager: SyncManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) prefs = appComponent.preferences + syncManager = appComponent.syncManager pureBlack = prefs.isPureBlackEnabled midnightTimer = appComponent.midnightTimer rootView = component.listHabitsRootView @@ -58,6 +61,7 @@ class ListHabitsActivity : HabitsActivity() { midnightTimer.onPause() screen.onDettached() adapter.cancelRefresh() + syncManager.onPause() super.onPause() } @@ -66,14 +70,13 @@ class ListHabitsActivity : HabitsActivity() { screen.onAttached() rootView.postInvalidate() midnightTimer.onResume() + syncManager.onResume() taskRunner.run { AutoBackup(this@ListHabitsActivity).run() } - if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) { restartWithFade(ListHabitsActivity::class.java) } - super.onResume() } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index 4d2d32602..eac956516 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -85,9 +85,11 @@ class ListHabitsScreen commandRunner.addListener(this) if(activity.intent.action == "android.intent.action.VIEW") { val uri = activity.intent.data!!.toString() - val key = uri.replace(Regex("^.*sync/"), "") - Log.i("ListHabitsScreen", key) - behavior.get().onSyncKeyOffer(key) + val parts = uri.replace(Regex("^.*sync/"), "").split("#") + val syncKey = parts[0] + val encKey = parts[1] + Log.i("ListHabitsScreen", "sync: $syncKey enc: $encKey") + behavior.get().onSyncKeyOffer(syncKey, encKey) } } @@ -185,6 +187,7 @@ class ListHabitsScreen COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed FILE_NOT_RECOGNIZED -> R.string.file_not_recognized SYNC_ENABLED -> R.string.sync_enabled + SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed }) } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java index 64153beda..5847f2395 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java @@ -158,12 +158,6 @@ public class SettingsFragment extends PreferenceFragmentCompat private void updateSyncPreferences() { - if(prefs.getSyncKey().isEmpty()) { - prefs.setSyncEnabled(false); - ((CheckBoxPreference) findPreference("pref_sync_enabled")).setChecked(false); - } - findPreference("pref_sync_base_url").setSummary(prefs.getSyncBaseURL()); - findPreference("pref_sync_key").setSummary(prefs.getSyncKey()); findPreference("pref_sync_display").setVisible(prefs.isSyncEnabled()); } @@ -190,11 +184,15 @@ public class SettingsFragment extends PreferenceFragmentCompat } if (key.equals("pref_sync_enabled")) { - Context context = getActivity(); if (prefs.isSyncEnabled()) { - Intent intent = new IntentFactory().startSyncActivity(context); - context.startActivity(intent); + Context context = getActivity(); + context.startActivity(new IntentFactory().startSyncActivity(context)); + } + else + { + prefs.setEncryptionKey(""); + prefs.setSyncKey(""); } } BackupManager.dataChanged("org.isoron.uhabits"); diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt index 7d04793be..3a86bae06 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt @@ -32,15 +32,16 @@ import org.isoron.androidbase.activities.* import org.isoron.androidbase.utils.* import org.isoron.androidbase.utils.InterfaceUtils.getFontAwesome import org.isoron.uhabits.* -import org.isoron.uhabits.activities.* import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.databinding.* import org.isoron.uhabits.sync.* +import org.isoron.uhabits.utils.* class SyncActivity : BaseActivity() { + private lateinit var syncManager: SyncManager private lateinit var preferences: Preferences private lateinit var taskRunner: TaskRunner private lateinit var baseScreen: BaseScreen @@ -56,6 +57,7 @@ class SyncActivity : BaseActivity() { val component = (application as HabitsApplication).component taskRunner = component.taskRunner preferences = component.preferences + syncManager = component.syncManager binding = ActivitySyncBinding.inflate(layoutInflater) setContentView(binding.root) @@ -84,20 +86,26 @@ class SyncActivity : BaseActivity() { } private fun displayCurrentKey() { - displayLink("https://loophabits.org/sync/${preferences.syncKey}") + displayLink("https://loophabits.org/sync/${preferences.syncKey}#${preferences.encryptionKey}") displayPassword("6B2W9F5X") } private fun register() { displayLoading() taskRunner.execute(object : Task { - private var key = "" + private lateinit var encKey: String + private lateinit var syncKey: String private var error = false override fun doInBackground() { runBlocking { val server = RemoteSyncServer(baseURL = preferences.syncBaseURL) try { - key = server.register() + syncKey = server.register() + encKey = generateEncryptionKey() + preferences.isSyncEnabled = true + preferences.encryptionKey = encKey + preferences.syncKey = syncKey; + syncManager.sync() } catch (e: ServiceUnavailable) { error = true } @@ -109,7 +117,6 @@ class SyncActivity : BaseActivity() { displayError() return; } - preferences.syncKey = key; displayCurrentKey() } }) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt index bfe1b5714..bc46d8189 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt @@ -46,7 +46,7 @@ class RemoteSyncServer( override suspend fun put(key: String, newData: SyncData) { try { - val response: String = httpClient.put("$baseURL/$key") { + val response: String = httpClient.put("$baseURL/db/$key") { header("Content-Type", "application/json") body = newData } @@ -59,7 +59,7 @@ class RemoteSyncServer( override suspend fun getData(key: String): SyncData { try { - return httpClient.get("$baseURL/$key") + return httpClient.get("$baseURL/db/$key") } catch (e: ServerResponseException) { throw ServiceUnavailable() } catch (e: ClientRequestException) { @@ -69,7 +69,7 @@ class RemoteSyncServer( override suspend fun getDataVersion(key: String): Long { try { - val response: GetDataVersionResponse = httpClient.get("$baseURL/$key/version") + val response: GetDataVersionResponse = httpClient.get("$baseURL/db/$key/version") return response.version } catch(e: ServerResponseException) { throw ServiceUnavailable() diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.kt new file mode 100644 index 000000000..b2744e94d --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016-2020 Á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.sync + +import android.content.* +import android.util.* +import kotlinx.coroutines.* +import org.isoron.androidbase.* +import org.isoron.uhabits.core.* +import org.isoron.uhabits.core.preferences.* +import org.isoron.uhabits.core.tasks.* +import org.isoron.uhabits.tasks.* +import org.isoron.uhabits.utils.* +import java.io.* +import javax.inject.* + +@AppScope +class SyncManager @Inject constructor( + val preferences: Preferences, + val taskRunner: TaskRunner, + val importDataTaskFactory: ImportDataTaskFactory, + @AppContext val context: Context +) : Preferences.Listener { + + private val server = RemoteSyncServer() + private val tmpFile = File.createTempFile("import", "", context.externalCacheDir) + private var currVersion = 0L + + init { + preferences.addListener(this) + } + + + fun sync() { + if(!preferences.isSyncEnabled) { + Log.i("SyncManager", "Device sync is disabled. Skipping sync") + return + } + taskRunner.execute { + runBlocking { + try { + Log.i("SyncManager", "Starting sync (key: ${preferences.syncKey})") + fetchAndMerge() + upload() + Log.i("SyncManager", "Sync finished") + } catch (e: Exception) { + Log.e("SyncManager", "Unexpected sync exception. Disabling sync", e) + preferences.isSyncEnabled = false + preferences.syncKey = "" + preferences.encryptionKey = "" + } + return@runBlocking + } + } + } + + suspend fun upload() { + Log.i("SyncManager", "Encrypting database...") + val db = DatabaseUtils.getDatabaseFile(context) + val encryptedDB = db.encryptToString(preferences.encryptionKey) + Log.i("SyncManager", "Uploading database (version ${currVersion}, ${encryptedDB.length / 1024} KB)") + server.put(preferences.syncKey, SyncData(currVersion, encryptedDB)) + } + + suspend fun fetchAndMerge() { + Log.i("SyncManager", "Fetching database from server...") + val data = server.getData(preferences.syncKey) + Log.i("SyncManager", "Fetched database (version ${data.version}, ${data.content.length / 1024} KB)") + if (data.version <= currVersion) { + Log.i("SyncManager", "Local version is up-to-date. Skipping merge.") + } else { + Log.i("SyncManager", "Decrypting and merging with local changes...") + data.content.decryptToFile(preferences.encryptionKey, tmpFile) + taskRunner.execute(importDataTaskFactory.create(tmpFile) { tmpFile.delete() }) + } + currVersion = data.version + 1 + } + + fun onResume() = sync() + fun onPause() = sync() + override fun onSyncEnabled() = sync() +} \ No newline at end of file diff --git a/android/uhabits-android/src/main/res/values/strings.xml b/android/uhabits-android/src/main/res/values/strings.xml index 31afe7709..f6e6e1426 100644 --- a/android/uhabits-android/src/main/res/values/strings.xml +++ b/android/uhabits-android/src/main/res/values/strings.xml @@ -214,4 +214,5 @@ Copied to the clipboard Device sync enabled + Sync key already installed \ No newline at end of file diff --git a/android/uhabits-android/src/main/res/xml/preferences.xml b/android/uhabits-android/src/main/res/xml/preferences.xml index e311649a6..e805fc985 100644 --- a/android/uhabits-android/src/main/res/xml/preferences.xml +++ b/android/uhabits-android/src/main/res/xml/preferences.xml @@ -235,6 +235,13 @@ app:iconSpaceReserved="false" /> + + \ No newline at end of file diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/preferences/Preferences.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/preferences/Preferences.java index af1a37bba..23ce47d5c 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/preferences/Preferences.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/preferences/Preferences.java @@ -324,6 +324,16 @@ public class Preferences storage.putString("pref_sync_key", key); } + public String getEncryptionKey() + { + return storage.getString("pref_encryption_key", ""); + } + + public void setEncryptionKey(String key) + { + storage.putString("pref_encryption_key", key); + } + public boolean isSyncEnabled() { return storage.getBoolean("pref_sync_enabled", false); @@ -332,6 +342,7 @@ public class Preferences public void setSyncEnabled(boolean enabled) { storage.putBoolean("pref_sync_enabled", enabled); + if(enabled) for (Listener l : listeners) l.onSyncEnabled(); } @@ -357,6 +368,10 @@ public class Preferences default void onNotificationsChanged() { } + + default void onSyncEnabled() + { + } } public interface Storage diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java index 6f6c3c5a2..fb52b006f 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java @@ -158,10 +158,15 @@ public class ListHabitsBehavior habit.getId()); } - public void onSyncKeyOffer(@NotNull String key) + public void onSyncKeyOffer(@NotNull String syncKey, @NotNull String encryptionKey) { + if(prefs.getSyncKey().equals(syncKey)) { + screen.showMessage(Message.SYNC_KEY_ALREADY_INSTALLED); + return; + } screen.showConfirmInstallSyncKey(() -> { - prefs.setSyncKey(key); + prefs.setSyncKey(syncKey); + prefs.setEncryptionKey(encryptionKey); prefs.setSyncEnabled(true); screen.showMessage(Message.SYNC_ENABLED); }); @@ -170,7 +175,7 @@ public class ListHabitsBehavior public enum Message { COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED, - COULD_NOT_GENERATE_BUG_REPORT, FILE_NOT_RECOGNIZED, SYNC_ENABLED + COULD_NOT_GENERATE_BUG_REPORT, FILE_NOT_RECOGNIZED, SYNC_ENABLED, SYNC_KEY_ALREADY_INSTALLED } public interface BugReporter