Move SyncManager to uhabits-core

pull/699/head
Alinson S. Xavier 5 years ago
parent 9743b05a78
commit f6699fbfda

@ -35,10 +35,12 @@ import junit.framework.*;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.isoron.androidbase.activities.*; import org.isoron.androidbase.activities.*;
import org.isoron.androidbase.utils.*; import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
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.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.utils.*;
import org.junit.*; import org.junit.*;
import java.io.*; import java.io.*;
@ -99,9 +101,12 @@ public class BaseAndroidTest extends TestCase
latch = new CountDownLatch(1); latch = new CountDownLatch(1);
Context context = targetContext.getApplicationContext();
File dbFile = DatabaseUtils.getDatabaseFile(context);
appComponent = DaggerHabitsApplicationTestComponent appComponent = DaggerHabitsApplicationTestComponent
.builder() .builder()
.appContextModule(new AppContextModule(targetContext.getApplicationContext())) .appContextModule(new AppContextModule(context))
.habitsModule(new HabitsModule(dbFile))
.build(); .build();
HabitsApplication.Companion.setComponent(appComponent); HabitsApplication.Companion.setComponent(appComponent);

@ -39,7 +39,7 @@ public class AndroidDatabaseTest extends BaseAndroidTest
public void setUp() public void setUp()
{ {
super.setUp(); super.setUp();
db = new AndroidDatabase(SQLiteDatabase.create(null)); db = new AndroidDatabase(SQLiteDatabase.create(null), null);
db.execute("create table test(color int, name string)"); db.execute("create table test(color int, name string)");
} }

@ -28,11 +28,12 @@ import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
import junit.framework.Assert.* import junit.framework.Assert.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.sync.* import org.isoron.uhabits.core.sync.*
import org.junit.* import org.junit.*
@MediumTest @MediumTest
class RemoteSyncServerTest { class RemoteSyncServerTest : BaseAndroidTest() {
private val mapper = ObjectMapper() private val mapper = ObjectMapper()
val data = SyncData(1, "Hello world") val data = SyncData(1, "Hello world")
@ -127,7 +128,7 @@ class RemoteSyncServerTest {
} }
} }
} }
}, baseURL = "") }, preferences = prefs)
} }
private fun MockRequestHandleScope.respondWithJson(content: Any) = private fun MockRequestHandleScope.respondWithJson(content: Any) =

@ -43,10 +43,6 @@ class HabitsApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
context = this context = this
HabitsApplication.component = DaggerHabitsApplicationComponent
.builder()
.appContextModule(AppContextModule(context))
.build()
if (isTestMode()) { if (isTestMode()) {
val db = DatabaseUtils.getDatabaseFile(context) val db = DatabaseUtils.getDatabaseFile(context)
@ -60,6 +56,14 @@ class HabitsApplication : Application() {
db.renameTo(File(db.absolutePath + ".invalid")) db.renameTo(File(db.absolutePath + ".invalid"))
DatabaseUtils.initializeDatabase(context) DatabaseUtils.initializeDatabase(context)
} }
val db = DatabaseUtils.getDatabaseFile(this)
HabitsApplication.component = DaggerHabitsApplicationComponent
.builder()
.appContextModule(AppContextModule(context))
.habitsModule(HabitsModule(db))
.build()
DateUtils.setStartDayOffset(3, 0) DateUtils.setStartDayOffset(3, 0)
widgetUpdater = component.widgetUpdater widgetUpdater = component.widgetUpdater

@ -28,13 +28,13 @@ import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.reminders.*; import org.isoron.uhabits.core.reminders.*;
import org.isoron.uhabits.core.sync.*;
import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.*; import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*; 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.*;

@ -26,10 +26,11 @@ import android.database.sqlite.*
import org.isoron.uhabits.core.database.* import org.isoron.uhabits.core.database.*
import org.isoron.uhabits.database.* import org.isoron.uhabits.database.*
import java.io.*
class HabitsDatabaseOpener( class HabitsDatabaseOpener(
context: Context, context: Context,
databaseFilename: String, private val databaseFilename: String,
private val version: Int private val version: Int
) : SQLiteOpenHelper(context, databaseFilename, null, version) { ) : SQLiteOpenHelper(context, databaseFilename, null, version) {
@ -49,7 +50,7 @@ class HabitsDatabaseOpener(
newVersion: Int) { newVersion: Int) {
db.disableWriteAheadLogging() db.disableWriteAheadLogging()
if (db.version < 8) throw UnsupportedDatabaseVersionException() if (db.version < 8) throw UnsupportedDatabaseVersionException()
val helper = MigrationHelper(AndroidDatabase(db)) val helper = MigrationHelper(AndroidDatabase(db, File(databaseFilename)))
helper.migrateTo(newVersion) helper.migrateTo(newVersion)
} }

@ -19,24 +19,34 @@
package org.isoron.uhabits package org.isoron.uhabits
import android.content.*
import dagger.* import dagger.*
import org.isoron.androidbase.*
import org.isoron.uhabits.core.* import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.commands.* import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.database.* import org.isoron.uhabits.core.database.*
import org.isoron.uhabits.core.io.*
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.models.sqlite.* import org.isoron.uhabits.core.models.sqlite.*
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.reminders.* import org.isoron.uhabits.core.reminders.*
import org.isoron.uhabits.core.sync.*
import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.database.* import org.isoron.uhabits.database.*
import org.isoron.uhabits.intents.* import org.isoron.uhabits.intents.*
import org.isoron.uhabits.io.*
import org.isoron.uhabits.notifications.* import org.isoron.uhabits.notifications.*
import org.isoron.uhabits.preferences.* import org.isoron.uhabits.preferences.*
import org.isoron.uhabits.sync.*
import org.isoron.uhabits.utils.* import org.isoron.uhabits.utils.*
import java.io.*
@Module @Module
class HabitsModule { class HabitsModule(dbFile: File) {
val db: Database = AndroidDatabase(DatabaseUtils.openDatabase(), dbFile)
@Provides @Provides
@AppScope @AppScope
fun getPreferences(storage: SharedPreferencesStorage): Preferences { fun getPreferences(storage: SharedPreferencesStorage): Preferences {
@ -76,7 +86,7 @@ class HabitsModule {
@Provides @Provides
@AppScope @AppScope
fun getModelFactory(): ModelFactory { fun getModelFactory(): ModelFactory {
return SQLModelFactory(AndroidDatabase(DatabaseUtils.openDatabase())) return SQLModelFactory(db)
} }
@Provides @Provides
@ -90,5 +100,29 @@ class HabitsModule {
fun getDatabaseOpener(opener: AndroidDatabaseOpener): DatabaseOpener { fun getDatabaseOpener(opener: AndroidDatabaseOpener): DatabaseOpener {
return opener return opener
} }
@Provides
@AppScope
fun getLogging(): Logging {
return AndroidLogging()
}
@Provides
@AppScope
fun getNetworkManager(@AppContext context: Context): NetworkManager {
return AndroidNetworkManager(context)
}
@Provides
@AppScope
fun getSyncServer(preferences: Preferences) : AbstractSyncServer {
return RemoteSyncServer(preferences)
}
@Provides
@AppScope
fun getDatabase(): Database {
return db
}
} }

@ -25,11 +25,11 @@ import org.isoron.uhabits.*
import org.isoron.uhabits.activities.* import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.sync.*
import org.isoron.uhabits.core.tasks.* 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() {

@ -52,7 +52,7 @@ class SyncActivity : BaseActivity(), SyncBehavior.Screen {
val component = (application as HabitsApplication).component val component = (application as HabitsApplication).component
val preferences = component.preferences val preferences = component.preferences
val server = RemoteSyncServer(baseURL = preferences.syncBaseURL) val server = RemoteSyncServer(preferences = preferences)
baseScreen = BaseScreen(this) baseScreen = BaseScreen(this)
behavior = SyncBehavior(this, preferences, server) behavior = SyncBehavior(this, preferences, server)

@ -22,8 +22,12 @@ package org.isoron.uhabits.database
import android.content.* import android.content.*
import android.database.sqlite.* import android.database.sqlite.*
import org.isoron.uhabits.core.database.* import org.isoron.uhabits.core.database.*
import java.io.*
class AndroidDatabase(private val db: SQLiteDatabase) : Database { class AndroidDatabase(
private val db: SQLiteDatabase,
private val file: File?,
) : Database {
override fun beginTransaction() = db.beginTransaction() override fun beginTransaction() = db.beginTransaction()
override fun setTransactionSuccessful() = db.setTransactionSuccessful() override fun setTransactionSuccessful() = db.setTransactionSuccessful()
@ -31,6 +35,10 @@ class AndroidDatabase(private val db: SQLiteDatabase) : Database {
override fun close() = db.close() override fun close() = db.close()
override fun getVersion() = db.version override fun getVersion() = db.version
override fun getFile(): File? {
return file
}
override fun query(query: String, vararg params: String) override fun query(query: String, vararg params: String)
= AndroidCursor(db.rawQuery(query, params)) = AndroidCursor(db.rawQuery(query, params))

@ -26,7 +26,12 @@ import javax.inject.*
class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener { class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener {
override fun open(file: File): AndroidDatabase { override fun open(file: File): AndroidDatabase {
return AndroidDatabase(SQLiteDatabase.openDatabase( return AndroidDatabase(
file.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)) db = SQLiteDatabase.openDatabase(
file.absolutePath,
null,
SQLiteDatabase.OPEN_READWRITE,
),
file = file)
} }
} }

@ -0,0 +1,48 @@
/*
* 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.io
import android.util.*
import org.isoron.uhabits.core.io.*
class AndroidLogging : Logging {
override fun getLogger(name: String): Logger {
return AndroidLogger(name)
}
}
class AndroidLogger(val name: String) : Logger {
override fun info(msg: String) {
Log.i(name, msg)
}
override fun debug(msg: String) {
Log.d(name, msg)
}
override fun error(msg: String) {
Log.e(name, msg)
}
override fun error(exception: Exception) {
Log.e(name, "Exception", exception)
}
}

@ -0,0 +1,57 @@
/*
* 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.net.*
import org.isoron.uhabits.core.sync.*
class AndroidNetworkManager(
val context: Context,
) : NetworkManager, ConnectivityManager.NetworkCallback() {
val listeners = mutableListOf<NetworkManager.Listener>()
var connected = false
init {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm.registerNetworkCallback(NetworkRequest.Builder().build(), this)
}
override fun addListener(listener: NetworkManager.Listener) {
if (connected) listener.onNetworkAvailable()
else listener.onNetworkLost()
listeners.add(listener)
}
override fun remoteListener(listener: NetworkManager.Listener) {
listeners.remove(listener)
}
override fun onAvailable(network: Network) {
connected = true
for (l in listeners) l.onNetworkAvailable()
}
override fun onLost(network: Network) {
connected = false
for (l in listeners) l.onNetworkLost()
}
}

@ -26,10 +26,11 @@ import io.ktor.client.features.*
import io.ktor.client.features.json.* import io.ktor.client.features.json.*
import io.ktor.client.request.* import io.ktor.client.request.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.sync.* import org.isoron.uhabits.core.sync.*
class RemoteSyncServer( class RemoteSyncServer(
private val baseURL: String, private val preferences: Preferences,
private val httpClient: HttpClient = HttpClient(Android) { private val httpClient: HttpClient = HttpClient(Android) {
install(JsonFeature) install(JsonFeature)
} }
@ -37,7 +38,7 @@ class RemoteSyncServer(
override suspend fun register(): String = Dispatchers.IO { override suspend fun register(): String = Dispatchers.IO {
try { try {
val url = "$baseURL/register" val url = "${preferences.syncBaseURL}/register"
Log.i("RemoteSyncServer", "POST $url") Log.i("RemoteSyncServer", "POST $url")
val response: RegisterReponse = httpClient.post(url) val response: RegisterReponse = httpClient.post(url)
return@IO response.key return@IO response.key
@ -48,7 +49,7 @@ class RemoteSyncServer(
override suspend fun put(key: String, newData: SyncData) = Dispatchers.IO { override suspend fun put(key: String, newData: SyncData) = Dispatchers.IO {
try { try {
val url = "$baseURL/db/$key" val url = "${preferences.syncBaseURL}/db/$key"
Log.i("RemoteSyncServer", "PUT $url") Log.i("RemoteSyncServer", "PUT $url")
val response: String = httpClient.put(url) { val response: String = httpClient.put(url) {
header("Content-Type", "application/json") header("Content-Type", "application/json")
@ -66,7 +67,7 @@ class RemoteSyncServer(
override suspend fun getData(key: String): SyncData = Dispatchers.IO { override suspend fun getData(key: String): SyncData = Dispatchers.IO {
try { try {
val url = "$baseURL/db/$key" val url = "${preferences.syncBaseURL}/db/$key"
Log.i("RemoteSyncServer", "GET $url") Log.i("RemoteSyncServer", "GET $url")
val data: SyncData = httpClient.get(url) val data: SyncData = httpClient.get(url)
return@IO data return@IO data
@ -80,7 +81,7 @@ class RemoteSyncServer(
override suspend fun getDataVersion(key: String): Long = Dispatchers.IO { override suspend fun getDataVersion(key: String): Long = Dispatchers.IO {
try { try {
val url = "$baseURL/db/$key/version" val url = "${preferences.syncBaseURL}/db/$key/version"
Log.i("RemoteSyncServer", "GET $url") Log.i("RemoteSyncServer", "GET $url")
val response: GetDataVersionResponse = httpClient.get(url) val response: GetDataVersionResponse = httpClient.get(url)
return@IO response.version return@IO response.version

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.database; package org.isoron.uhabits.core.database;
import java.io.*;
import java.util.*; import java.util.*;
public interface Database public interface Database
@ -54,6 +55,8 @@ public interface Database
int getVersion(); int getVersion();
File getFile();
interface ProcessCallback interface ProcessCallback
{ {
void process(Cursor cursor); void process(Cursor cursor);

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.database;
import org.apache.commons.lang3.*; import org.apache.commons.lang3.*;
import java.io.*;
import java.sql.*; import java.sql.*;
import java.util.*; import java.util.*;
@ -203,4 +204,10 @@ public class JdbcDatabase implements Database
return c.getInt(0); 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 androidx.annotation.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.database.*; import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
@ -47,10 +48,10 @@ public class LoopDBImporter extends AbstractImporter
private final CommandRunner runner; private final CommandRunner runner;
@Inject @Inject
public LoopDBImporter(@NonNull HabitList habitList, public LoopDBImporter(@AppScope @NonNull HabitList habitList,
@NonNull ModelFactory modelFactory, @AppScope @NonNull ModelFactory modelFactory,
@NonNull DatabaseOpener opener, @AppScope @NonNull DatabaseOpener opener,
@NonNull CommandRunner runner) @AppScope @NonNull CommandRunner runner)
{ {
super(habitList); super(habitList);
this.modelFactory = modelFactory; 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()
}
}

@ -17,74 +17,63 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.sync package org.isoron.uhabits.core.sync
import android.content.*
import android.net.*
import android.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.isoron.androidbase.*
import org.isoron.uhabits.core.* import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.commands.* 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 org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.sync.*
import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.tasks.*
import org.isoron.uhabits.utils.*
import java.io.* import java.io.*
import javax.inject.* import javax.inject.*
@AppScope @AppScope
class SyncManager @Inject constructor( class SyncManager @Inject constructor(
val preferences: Preferences, val preferences: Preferences,
private val importDataTaskFactory: ImportDataTaskFactory,
val commandRunner: CommandRunner, val commandRunner: CommandRunner,
@AppContext val context: Context val logging: Logging,
) : Preferences.Listener, CommandRunner.Listener, ConnectivityManager.NetworkCallback() { 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 var connected = false
private val tmpFile = File.createTempFile("import", "")
private val server = RemoteSyncServer(baseURL = preferences.syncBaseURL)
private val tmpFile = File.createTempFile("import", "", context.externalCacheDir)
private var currVersion = 1L private var currVersion = 1L
private var dirty = true private var dirty = true
private var taskRunner = SingleThreadTaskRunner()
private lateinit var encryptionKey: EncryptionKey private lateinit var encryptionKey: EncryptionKey
private lateinit var syncKey: String private lateinit var syncKey: String
init { init {
preferences.addListener(this) preferences.addListener(this)
commandRunner.addListener(this) commandRunner.addListener(this)
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager networkManager.addListener(this)
cm.registerNetworkCallback(NetworkRequest.Builder().build(), this)
} }
fun sync() = CoroutineScope(Dispatchers.Main).launch { fun sync() = CoroutineScope(Dispatchers.Main).launch {
if (!preferences.isSyncEnabled) { if (!preferences.isSyncEnabled) {
Log.i("SyncManager", "Device sync is disabled. Skipping sync.") logger.info("Device sync is disabled. Skipping sync.")
return@launch return@launch
} }
encryptionKey = EncryptionKey.fromBase64(preferences.encryptionKey) encryptionKey = EncryptionKey.fromBase64(preferences.encryptionKey)
syncKey = preferences.syncKey syncKey = preferences.syncKey
Log.i("SyncManager", "Starting sync (key: $syncKey)") logger.info("Starting sync (key: $syncKey)")
try { try {
pull() pull()
push() push()
Log.i("SyncManager", "Sync finished successfully.") logger.info("Sync finished successfully.")
} catch (e: ConnectionLostException) { } catch (e: ConnectionLostException) {
Log.i("SyncManager", "Network unavailable. Aborting sync.") logger.info("Network unavailable. Aborting sync.")
} catch (e: ServiceUnavailable) { } catch (e: ServiceUnavailable) {
Log.i("SyncManager", "Sync service unavailable. Aborting sync.") logger.info("Sync service unavailable. Aborting sync.")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("SyncManager", "Unexpected sync exception. Disabling sync.", e) logger.error("Unexpected sync exception. Disabling sync.")
logger.error(e)
preferences.disableSync() preferences.disableSync()
} }
} }
@ -95,45 +84,55 @@ class SyncManager @Inject constructor(
} }
if (!dirty) { if (!dirty) {
Log.i("SyncManager", "Local database not modified. Skipping push.") logger.info("Local database not modified. Skipping push.")
return return
} }
Log.i("SyncManager", "Encrypting local database...") logger.info("Encrypting local database...")
val db = DatabaseUtils.getDatabaseFile(context) val encryptedDB = db.file.encryptToString(encryptionKey)
val encryptedDB = db.encryptToString(encryptionKey)
val size = encryptedDB.length / 1024 val size = encryptedDB.length / 1024
try { try {
Log.i("SyncManager", "Pushing local database (version $currVersion, $size KB)") logger.info("Pushing local database (version $currVersion, $size KB)")
assertConnected() assertConnected()
server.put(preferences.syncKey, SyncData(currVersion, encryptedDB)) server.put(preferences.syncKey, SyncData(currVersion, encryptedDB))
dirty = false dirty = false
} catch (e: EditConflictException) { } catch (e: EditConflictException) {
Log.i("SyncManager", "Sync conflict detected while pushing.") logger.info("Sync conflict detected while pushing.")
setCurrentVersion(0) setCurrentVersion(0)
pull() pull()
push(depth = depth + 1) push(depth = depth + 1)
} }
} }
private suspend fun pull() { private suspend fun pull() = Dispatchers.IO {
Log.i("SyncManager", "Querying remote database version...") logger.info("Querying remote database version...")
assertConnected() assertConnected()
val remoteVersion = server.getDataVersion(syncKey) val remoteVersion = server.getDataVersion(syncKey)
Log.i("SyncManager", "Remote database version: $remoteVersion") logger.info("Remote database version: $remoteVersion")
if (remoteVersion <= currVersion) { if (remoteVersion <= currVersion) {
Log.i("SyncManager", "Local database is up-to-date. Skipping merge.") logger.info("Local database is up-to-date. Skipping merge.")
} else { } else {
Log.i("SyncManager", "Pulling remote database...") logger.info("Pulling remote database...")
assertConnected() assertConnected()
val data = server.getData(syncKey) val data = server.getData(syncKey)
val size = data.content.length / 1024 val size = data.content.length / 1024
Log.i("SyncManager", "Pulled remote database (version ${data.version}, $size KB)") logger.info("Pulled remote database (version ${data.version}, $size KB)")
Log.i("SyncManager", "Decrypting remote database and merging with local changes...") logger.info("Decrypting remote database and merging with local changes...")
data.content.decryptToFile(encryptionKey, tmpFile) data.content.decryptToFile(encryptionKey, tmpFile)
taskRunner.execute(importDataTaskFactory.create(tmpFile) { tmpFile.delete() })
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 dirty = true
setCurrentVersion(data.version + 1) setCurrentVersion(data.version + 1)
} }
@ -144,20 +143,20 @@ class SyncManager @Inject constructor(
fun onPause() = sync() fun onPause() = sync()
override fun onSyncEnabled() { override fun onSyncEnabled() {
Log.i("SyncManager", "Sync enabled.") logger.info("Sync enabled.")
setCurrentVersion(1) setCurrentVersion(1)
dirty = true dirty = true
sync() sync()
} }
override fun onAvailable(network: Network) { override fun onNetworkAvailable() {
Log.i("SyncManager", "Network available.") logger.info("Network available.")
connected = true connected = true
sync() sync()
} }
override fun onLost(network: Network) { override fun onNetworkLost() {
Log.i("SyncManager", "Network unavailable.") logger.info("Network unavailable.")
connected = false connected = false
} }
@ -172,7 +171,7 @@ class SyncManager @Inject constructor(
private fun setCurrentVersion(v: Long) { private fun setCurrentVersion(v: Long) {
currVersion = v currVersion = v
Log.i("SyncManager", "Setting local database version: $currVersion") logger.info("Setting local database version: $currVersion")
} }
} }
Loading…
Cancel
Save