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.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();
}

@ -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()
}
}

@ -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
})
}

@ -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");

@ -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()
}
})

@ -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()

@ -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="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_key_already_installed">Sync key already installed</string>
</resources>

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

@ -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

@ -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

Loading…
Cancel
Save