SyncManager: First version

pull/699/head
Alinson S. Xavier 5 years ago
parent b1560dd694
commit 659c528744

@ -34,6 +34,7 @@ import org.isoron.uhabits.core.ui.screens.habits.list.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.intents.*; import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.receivers.*; import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.sync.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.widgets.*; import org.isoron.uhabits.widgets.*;
@ -85,4 +86,6 @@ public interface HabitsApplicationComponent
WidgetPreferences getWidgetPreferences(); WidgetPreferences getWidgetPreferences();
WidgetUpdater getWidgetUpdater(); WidgetUpdater getWidgetUpdater();
SyncManager getSyncManager();
} }

@ -28,6 +28,7 @@ import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.ThemeSwitcher.* import org.isoron.uhabits.core.ui.ThemeSwitcher.*
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.database.* import org.isoron.uhabits.database.*
import org.isoron.uhabits.sync.*
class ListHabitsActivity : HabitsActivity() { class ListHabitsActivity : HabitsActivity() {
@ -38,10 +39,12 @@ class ListHabitsActivity : HabitsActivity() {
lateinit var screen: ListHabitsScreen lateinit var screen: ListHabitsScreen
lateinit var prefs: Preferences lateinit var prefs: Preferences
lateinit var midnightTimer: MidnightTimer lateinit var midnightTimer: MidnightTimer
lateinit var syncManager: SyncManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
prefs = appComponent.preferences prefs = appComponent.preferences
syncManager = appComponent.syncManager
pureBlack = prefs.isPureBlackEnabled pureBlack = prefs.isPureBlackEnabled
midnightTimer = appComponent.midnightTimer midnightTimer = appComponent.midnightTimer
rootView = component.listHabitsRootView rootView = component.listHabitsRootView
@ -58,6 +61,7 @@ class ListHabitsActivity : HabitsActivity() {
midnightTimer.onPause() midnightTimer.onPause()
screen.onDettached() screen.onDettached()
adapter.cancelRefresh() adapter.cancelRefresh()
syncManager.onPause()
super.onPause() super.onPause()
} }
@ -66,14 +70,13 @@ class ListHabitsActivity : HabitsActivity() {
screen.onAttached() screen.onAttached()
rootView.postInvalidate() rootView.postInvalidate()
midnightTimer.onResume() midnightTimer.onResume()
syncManager.onResume()
taskRunner.run { taskRunner.run {
AutoBackup(this@ListHabitsActivity).run() AutoBackup(this@ListHabitsActivity).run()
} }
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) { if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
restartWithFade(ListHabitsActivity::class.java) restartWithFade(ListHabitsActivity::class.java)
} }
super.onResume() super.onResume()
} }
} }

@ -85,9 +85,11 @@ class ListHabitsScreen
commandRunner.addListener(this) commandRunner.addListener(this)
if(activity.intent.action == "android.intent.action.VIEW") { if(activity.intent.action == "android.intent.action.VIEW") {
val uri = activity.intent.data!!.toString() val uri = activity.intent.data!!.toString()
val key = uri.replace(Regex("^.*sync/"), "") val parts = uri.replace(Regex("^.*sync/"), "").split("#")
Log.i("ListHabitsScreen", key) val syncKey = parts[0]
behavior.get().onSyncKeyOffer(key) 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 COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
SYNC_ENABLED -> R.string.sync_enabled SYNC_ENABLED -> R.string.sync_enabled
SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed
}) })
} }

@ -158,12 +158,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
private void updateSyncPreferences() 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()); findPreference("pref_sync_display").setVisible(prefs.isSyncEnabled());
} }
@ -190,11 +184,15 @@ public class SettingsFragment extends PreferenceFragmentCompat
} }
if (key.equals("pref_sync_enabled")) if (key.equals("pref_sync_enabled"))
{ {
Context context = getActivity();
if (prefs.isSyncEnabled()) if (prefs.isSyncEnabled())
{ {
Intent intent = new IntentFactory().startSyncActivity(context); Context context = getActivity();
context.startActivity(intent); context.startActivity(new IntentFactory().startSyncActivity(context));
}
else
{
prefs.setEncryptionKey("");
prefs.setSyncKey("");
} }
} }
BackupManager.dataChanged("org.isoron.uhabits"); BackupManager.dataChanged("org.isoron.uhabits");

@ -32,15 +32,16 @@ import org.isoron.androidbase.activities.*
import org.isoron.androidbase.utils.* import org.isoron.androidbase.utils.*
import org.isoron.androidbase.utils.InterfaceUtils.getFontAwesome import org.isoron.androidbase.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.* import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.sync.* import org.isoron.uhabits.sync.*
import org.isoron.uhabits.utils.*
class SyncActivity : BaseActivity() { class SyncActivity : BaseActivity() {
private lateinit var syncManager: SyncManager
private lateinit var preferences: Preferences private lateinit var preferences: Preferences
private lateinit var taskRunner: TaskRunner private lateinit var taskRunner: TaskRunner
private lateinit var baseScreen: BaseScreen private lateinit var baseScreen: BaseScreen
@ -56,6 +57,7 @@ class SyncActivity : BaseActivity() {
val component = (application as HabitsApplication).component val component = (application as HabitsApplication).component
taskRunner = component.taskRunner taskRunner = component.taskRunner
preferences = component.preferences preferences = component.preferences
syncManager = component.syncManager
binding = ActivitySyncBinding.inflate(layoutInflater) binding = ActivitySyncBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -84,20 +86,26 @@ class SyncActivity : BaseActivity() {
} }
private fun displayCurrentKey() { private fun displayCurrentKey() {
displayLink("https://loophabits.org/sync/${preferences.syncKey}") displayLink("https://loophabits.org/sync/${preferences.syncKey}#${preferences.encryptionKey}")
displayPassword("6B2W9F5X") displayPassword("6B2W9F5X")
} }
private fun register() { private fun register() {
displayLoading() displayLoading()
taskRunner.execute(object : Task { taskRunner.execute(object : Task {
private var key = "" private lateinit var encKey: String
private lateinit var syncKey: String
private var error = false private var error = false
override fun doInBackground() { override fun doInBackground() {
runBlocking { runBlocking {
val server = RemoteSyncServer(baseURL = preferences.syncBaseURL) val server = RemoteSyncServer(baseURL = preferences.syncBaseURL)
try { try {
key = server.register() syncKey = server.register()
encKey = generateEncryptionKey()
preferences.isSyncEnabled = true
preferences.encryptionKey = encKey
preferences.syncKey = syncKey;
syncManager.sync()
} catch (e: ServiceUnavailable) { } catch (e: ServiceUnavailable) {
error = true error = true
} }
@ -109,7 +117,6 @@ class SyncActivity : BaseActivity() {
displayError() displayError()
return; return;
} }
preferences.syncKey = key;
displayCurrentKey() displayCurrentKey()
} }
}) })

@ -46,7 +46,7 @@ class RemoteSyncServer(
override suspend fun put(key: String, newData: SyncData) { override suspend fun put(key: String, newData: SyncData) {
try { try {
val response: String = httpClient.put("$baseURL/$key") { val response: String = httpClient.put("$baseURL/db/$key") {
header("Content-Type", "application/json") header("Content-Type", "application/json")
body = newData body = newData
} }
@ -59,7 +59,7 @@ class RemoteSyncServer(
override suspend fun getData(key: String): SyncData { override suspend fun getData(key: String): SyncData {
try { try {
return httpClient.get("$baseURL/$key") return httpClient.get("$baseURL/db/$key")
} catch (e: ServerResponseException) { } catch (e: ServerResponseException) {
throw ServiceUnavailable() throw ServiceUnavailable()
} catch (e: ClientRequestException) { } catch (e: ClientRequestException) {
@ -69,7 +69,7 @@ class RemoteSyncServer(
override suspend fun getDataVersion(key: String): Long { override suspend fun getDataVersion(key: String): Long {
try { try {
val response: GetDataVersionResponse = httpClient.get("$baseURL/$key/version") val response: GetDataVersionResponse = httpClient.get("$baseURL/db/$key/version")
return response.version return response.version
} catch(e: ServerResponseException) { } catch(e: ServerResponseException) {
throw ServiceUnavailable() throw ServiceUnavailable()

@ -0,0 +1,99 @@
/*
* Copyright (C) 2016-2020 Á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.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()
}

@ -214,4 +214,5 @@
<string name="copied_to_the_clipboard">Copied to the clipboard</string> <string name="copied_to_the_clipboard">Copied to the clipboard</string>
<string name="sync_confirm"><![CDATA[Are you trying to enable device sync?\n\nThis feature allows you to sync your data across multiple devices. When enabled, an encrypted copy of your data will be uploaded to our servers.]]></string> <string name="sync_confirm"><![CDATA[Are you trying to enable device sync?\n\nThis feature allows you to sync your data across multiple devices. When enabled, an encrypted copy of your data will be uploaded to our servers.]]></string>
<string name="sync_enabled">Device sync enabled</string> <string name="sync_enabled">Device sync enabled</string>
<string name="sync_key_already_installed">Sync key already installed</string>
</resources> </resources>

@ -235,6 +235,13 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
/> />
<EditTextPreference
android:defaultValue=""
android:key="pref_encryption_key"
android:title="Encryption key"
app:iconSpaceReserved="false"
/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

@ -324,6 +324,16 @@ public class Preferences
storage.putString("pref_sync_key", key); 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() public boolean isSyncEnabled()
{ {
return storage.getBoolean("pref_sync_enabled", false); return storage.getBoolean("pref_sync_enabled", false);
@ -332,6 +342,7 @@ public class Preferences
public void setSyncEnabled(boolean enabled) public void setSyncEnabled(boolean enabled)
{ {
storage.putBoolean("pref_sync_enabled", 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 onNotificationsChanged()
{ {
} }
default void onSyncEnabled()
{
}
} }
public interface Storage public interface Storage

@ -158,10 +158,15 @@ public class ListHabitsBehavior
habit.getId()); 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(() -> { screen.showConfirmInstallSyncKey(() -> {
prefs.setSyncKey(key); prefs.setSyncKey(syncKey);
prefs.setEncryptionKey(encryptionKey);
prefs.setSyncEnabled(true); prefs.setSyncEnabled(true);
screen.showMessage(Message.SYNC_ENABLED); screen.showMessage(Message.SYNC_ENABLED);
}); });
@ -170,7 +175,7 @@ public class ListHabitsBehavior
public enum Message public enum Message
{ {
COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED, 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 public interface BugReporter

Loading…
Cancel
Save