mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-07 01:28:52 -06:00
Move SyncManager to uhabits-core
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.isoron.uhabits.core.database;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
public interface Database
|
||||
@@ -54,6 +55,8 @@ public interface Database
|
||||
|
||||
int getVersion();
|
||||
|
||||
File getFile();
|
||||
|
||||
interface ProcessCallback
|
||||
{
|
||||
void process(Cursor cursor);
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.database;
|
||||
|
||||
import org.apache.commons.lang3.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
|
||||
@@ -203,4 +204,10 @@ public class JdbcDatabase implements Database
|
||||
return c.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.core.io
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
interface Logging {
|
||||
fun getLogger(name: String): Logger
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
fun info(msg: String)
|
||||
fun debug(msg: String)
|
||||
fun error(msg: String)
|
||||
fun error(exception: Exception)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.io;
|
||||
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.*;
|
||||
import org.isoron.uhabits.core.commands.*;
|
||||
import org.isoron.uhabits.core.database.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
@@ -47,10 +48,10 @@ public class LoopDBImporter extends AbstractImporter
|
||||
private final CommandRunner runner;
|
||||
|
||||
@Inject
|
||||
public LoopDBImporter(@NonNull HabitList habitList,
|
||||
@NonNull ModelFactory modelFactory,
|
||||
@NonNull DatabaseOpener opener,
|
||||
@NonNull CommandRunner runner)
|
||||
public LoopDBImporter(@AppScope @NonNull HabitList habitList,
|
||||
@AppScope @NonNull ModelFactory modelFactory,
|
||||
@AppScope @NonNull DatabaseOpener opener,
|
||||
@AppScope @NonNull CommandRunner runner)
|
||||
{
|
||||
super(habitList);
|
||||
this.modelFactory = modelFactory;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.core.sync
|
||||
|
||||
interface NetworkManager {
|
||||
fun addListener(listener: Listener)
|
||||
fun remoteListener(listener: Listener)
|
||||
interface Listener {
|
||||
fun onNetworkAvailable()
|
||||
fun onNetworkLost()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.core.sync
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.commands.*
|
||||
import org.isoron.uhabits.core.database.*
|
||||
import org.isoron.uhabits.core.io.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import java.io.*
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
class SyncManager @Inject constructor(
|
||||
val preferences: Preferences,
|
||||
val commandRunner: CommandRunner,
|
||||
val logging: Logging,
|
||||
val networkManager: NetworkManager,
|
||||
val server: AbstractSyncServer,
|
||||
val db: Database,
|
||||
val dbImporter: LoopDBImporter,
|
||||
) : Preferences.Listener, CommandRunner.Listener, NetworkManager.Listener {
|
||||
|
||||
private var logger = logging.getLogger("SyncManager")
|
||||
private var connected = false
|
||||
private val tmpFile = File.createTempFile("import", "")
|
||||
private var currVersion = 1L
|
||||
private var dirty = true
|
||||
private lateinit var encryptionKey: EncryptionKey
|
||||
private lateinit var syncKey: String
|
||||
|
||||
init {
|
||||
preferences.addListener(this)
|
||||
commandRunner.addListener(this)
|
||||
networkManager.addListener(this)
|
||||
}
|
||||
|
||||
fun sync() = CoroutineScope(Dispatchers.Main).launch {
|
||||
if (!preferences.isSyncEnabled) {
|
||||
logger.info("Device sync is disabled. Skipping sync.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
encryptionKey = EncryptionKey.fromBase64(preferences.encryptionKey)
|
||||
syncKey = preferences.syncKey
|
||||
logger.info("Starting sync (key: $syncKey)")
|
||||
|
||||
try {
|
||||
pull()
|
||||
push()
|
||||
logger.info("Sync finished successfully.")
|
||||
} catch (e: ConnectionLostException) {
|
||||
logger.info("Network unavailable. Aborting sync.")
|
||||
} catch (e: ServiceUnavailable) {
|
||||
logger.info("Sync service unavailable. Aborting sync.")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Unexpected sync exception. Disabling sync.")
|
||||
logger.error(e)
|
||||
preferences.disableSync()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun push(depth: Int = 0) {
|
||||
if (depth >= 5) {
|
||||
throw RuntimeException()
|
||||
}
|
||||
|
||||
if (!dirty) {
|
||||
logger.info("Local database not modified. Skipping push.")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Encrypting local database...")
|
||||
val encryptedDB = db.file.encryptToString(encryptionKey)
|
||||
val size = encryptedDB.length / 1024
|
||||
|
||||
try {
|
||||
logger.info("Pushing local database (version $currVersion, $size KB)")
|
||||
assertConnected()
|
||||
server.put(preferences.syncKey, SyncData(currVersion, encryptedDB))
|
||||
dirty = false
|
||||
} catch (e: EditConflictException) {
|
||||
logger.info("Sync conflict detected while pushing.")
|
||||
setCurrentVersion(0)
|
||||
pull()
|
||||
push(depth = depth + 1)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pull() = Dispatchers.IO {
|
||||
logger.info("Querying remote database version...")
|
||||
assertConnected()
|
||||
val remoteVersion = server.getDataVersion(syncKey)
|
||||
logger.info("Remote database version: $remoteVersion")
|
||||
|
||||
if (remoteVersion <= currVersion) {
|
||||
logger.info("Local database is up-to-date. Skipping merge.")
|
||||
} else {
|
||||
logger.info("Pulling remote database...")
|
||||
assertConnected()
|
||||
val data = server.getData(syncKey)
|
||||
val size = data.content.length / 1024
|
||||
logger.info("Pulled remote database (version ${data.version}, $size KB)")
|
||||
logger.info("Decrypting remote database and merging with local changes...")
|
||||
data.content.decryptToFile(encryptionKey, tmpFile)
|
||||
|
||||
try {
|
||||
db.beginTransaction()
|
||||
dbImporter.importHabitsFromFile(tmpFile)
|
||||
db.setTransactionSuccessful()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to import database")
|
||||
logger.error(e)
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
dirty = true
|
||||
setCurrentVersion(data.version + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume() = sync()
|
||||
|
||||
fun onPause() = sync()
|
||||
|
||||
override fun onSyncEnabled() {
|
||||
logger.info("Sync enabled.")
|
||||
setCurrentVersion(1)
|
||||
dirty = true
|
||||
sync()
|
||||
}
|
||||
|
||||
override fun onNetworkAvailable() {
|
||||
logger.info("Network available.")
|
||||
connected = true
|
||||
sync()
|
||||
}
|
||||
|
||||
override fun onNetworkLost() {
|
||||
logger.info("Network unavailable.")
|
||||
connected = false
|
||||
}
|
||||
|
||||
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
|
||||
if (!dirty) setCurrentVersion(currVersion + 1)
|
||||
dirty = true
|
||||
}
|
||||
|
||||
private fun assertConnected() {
|
||||
if (!connected) throw ConnectionLostException()
|
||||
}
|
||||
|
||||
private fun setCurrentVersion(v: Long) {
|
||||
currVersion = v
|
||||
logger.info("Setting local database version: $currVersion")
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionLostException : RuntimeException()
|
||||
Reference in New Issue
Block a user