diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java index 81b49dea7..ce4664b2a 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java @@ -35,10 +35,12 @@ import junit.framework.*; import org.isoron.androidbase.*; import org.isoron.androidbase.activities.*; import org.isoron.androidbase.utils.*; +import org.isoron.uhabits.core.database.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.utils.*; +import org.isoron.uhabits.utils.*; import org.junit.*; import java.io.*; @@ -99,9 +101,12 @@ public class BaseAndroidTest extends TestCase latch = new CountDownLatch(1); + Context context = targetContext.getApplicationContext(); + File dbFile = DatabaseUtils.getDatabaseFile(context); appComponent = DaggerHabitsApplicationTestComponent .builder() - .appContextModule(new AppContextModule(targetContext.getApplicationContext())) + .appContextModule(new AppContextModule(context)) + .habitsModule(new HabitsModule(dbFile)) .build(); HabitsApplication.Companion.setComponent(appComponent); diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java index 24ca2e975..128994e20 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java @@ -39,7 +39,7 @@ public class AndroidDatabaseTest extends BaseAndroidTest public void 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)"); } diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt index 69088ec1a..896fa7d23 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt @@ -28,11 +28,12 @@ import io.ktor.client.request.* import io.ktor.http.* import junit.framework.Assert.* import kotlinx.coroutines.* +import org.isoron.uhabits.* import org.isoron.uhabits.core.sync.* import org.junit.* @MediumTest -class RemoteSyncServerTest { +class RemoteSyncServerTest : BaseAndroidTest() { private val mapper = ObjectMapper() val data = SyncData(1, "Hello world") @@ -127,7 +128,7 @@ class RemoteSyncServerTest { } } } - }, baseURL = "") + }, preferences = prefs) } private fun MockRequestHandleScope.respondWithJson(content: Any) = diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt index a33a22e3b..f2ca04790 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt @@ -43,10 +43,6 @@ class HabitsApplication : Application() { override fun onCreate() { super.onCreate() context = this - HabitsApplication.component = DaggerHabitsApplicationComponent - .builder() - .appContextModule(AppContextModule(context)) - .build() if (isTestMode()) { val db = DatabaseUtils.getDatabaseFile(context) @@ -60,6 +56,14 @@ class HabitsApplication : Application() { db.renameTo(File(db.absolutePath + ".invalid")) DatabaseUtils.initializeDatabase(context) } + + val db = DatabaseUtils.getDatabaseFile(this) + HabitsApplication.component = DaggerHabitsApplicationComponent + .builder() + .appContextModule(AppContextModule(context)) + .habitsModule(HabitsModule(db)) + .build() + DateUtils.setStartDayOffset(3, 0) widgetUpdater = component.widgetUpdater 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 f20c9254f..72776a23e 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 @@ -28,13 +28,13 @@ import org.isoron.uhabits.core.io.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.reminders.*; +import org.isoron.uhabits.core.sync.*; import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.ui.*; 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.*; diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsDatabaseOpener.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsDatabaseOpener.kt index 6bc86eaaa..639c55d63 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsDatabaseOpener.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsDatabaseOpener.kt @@ -26,10 +26,11 @@ import android.database.sqlite.* import org.isoron.uhabits.core.database.* import org.isoron.uhabits.database.* +import java.io.* class HabitsDatabaseOpener( context: Context, - databaseFilename: String, + private val databaseFilename: String, private val version: Int ) : SQLiteOpenHelper(context, databaseFilename, null, version) { @@ -49,7 +50,7 @@ class HabitsDatabaseOpener( newVersion: Int) { db.disableWriteAheadLogging() if (db.version < 8) throw UnsupportedDatabaseVersionException() - val helper = MigrationHelper(AndroidDatabase(db)) + val helper = MigrationHelper(AndroidDatabase(db, File(databaseFilename))) helper.migrateTo(newVersion) } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsModule.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsModule.kt index a1a813d51..9a274f17b 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsModule.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsModule.kt @@ -19,24 +19,34 @@ package org.isoron.uhabits +import android.content.* import dagger.* +import org.isoron.androidbase.* 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.models.* import org.isoron.uhabits.core.models.sqlite.* import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.reminders.* +import org.isoron.uhabits.core.sync.* import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.database.* import org.isoron.uhabits.intents.* +import org.isoron.uhabits.io.* import org.isoron.uhabits.notifications.* import org.isoron.uhabits.preferences.* +import org.isoron.uhabits.sync.* import org.isoron.uhabits.utils.* +import java.io.* @Module -class HabitsModule { +class HabitsModule(dbFile: File) { + + val db: Database = AndroidDatabase(DatabaseUtils.openDatabase(), dbFile) + @Provides @AppScope fun getPreferences(storage: SharedPreferencesStorage): Preferences { @@ -76,7 +86,7 @@ class HabitsModule { @Provides @AppScope fun getModelFactory(): ModelFactory { - return SQLModelFactory(AndroidDatabase(DatabaseUtils.openDatabase())) + return SQLModelFactory(db) } @Provides @@ -90,5 +100,29 @@ class HabitsModule { fun getDatabaseOpener(opener: AndroidDatabaseOpener): DatabaseOpener { 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 + } } 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 b40b71ef0..f25cda173 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 @@ -25,11 +25,11 @@ import org.isoron.uhabits.* import org.isoron.uhabits.activities.* import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.core.preferences.* +import org.isoron.uhabits.core.sync.* 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() { 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 b1272761f..2cdf20fe4 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 @@ -52,7 +52,7 @@ class SyncActivity : BaseActivity(), SyncBehavior.Screen { val component = (application as HabitsApplication).component val preferences = component.preferences - val server = RemoteSyncServer(baseURL = preferences.syncBaseURL) + val server = RemoteSyncServer(preferences = preferences) baseScreen = BaseScreen(this) behavior = SyncBehavior(this, preferences, server) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabase.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabase.kt index 684633098..5e167eee3 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabase.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabase.kt @@ -22,8 +22,12 @@ package org.isoron.uhabits.database import android.content.* import android.database.sqlite.* 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 setTransactionSuccessful() = db.setTransactionSuccessful() @@ -31,6 +35,10 @@ class AndroidDatabase(private val db: SQLiteDatabase) : Database { override fun close() = db.close() override fun getVersion() = db.version + override fun getFile(): File? { + return file + } + override fun query(query: String, vararg params: String) = AndroidCursor(db.rawQuery(query, params)) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabaseOpener.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabaseOpener.kt index 8aeb612e9..1da591e48 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabaseOpener.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/database/AndroidDatabaseOpener.kt @@ -26,7 +26,12 @@ import javax.inject.* class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener { override fun open(file: File): AndroidDatabase { - return AndroidDatabase(SQLiteDatabase.openDatabase( - file.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)) + return AndroidDatabase( + db = SQLiteDatabase.openDatabase( + file.absolutePath, + null, + SQLiteDatabase.OPEN_READWRITE, + ), + file = file) } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/io/AndroidLogging.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/io/AndroidLogging.kt new file mode 100644 index 000000000..be89666bd --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/io/AndroidLogging.kt @@ -0,0 +1,48 @@ +/* + * 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.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) + } + +} \ No newline at end of file diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/AndroidNetworkManager.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/AndroidNetworkManager.kt new file mode 100644 index 000000000..4b5a2262f --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/AndroidNetworkManager.kt @@ -0,0 +1,57 @@ +/* + * 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.net.* +import org.isoron.uhabits.core.sync.* + +class AndroidNetworkManager( + val context: Context, +) : NetworkManager, ConnectivityManager.NetworkCallback() { + + val listeners = mutableListOf() + 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() + } +} \ No newline at end of file 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 cb74a447b..987537217 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 @@ -26,10 +26,11 @@ import io.ktor.client.features.* import io.ktor.client.features.json.* import io.ktor.client.request.* import kotlinx.coroutines.* +import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.sync.* class RemoteSyncServer( - private val baseURL: String, + private val preferences: Preferences, private val httpClient: HttpClient = HttpClient(Android) { install(JsonFeature) } @@ -37,7 +38,7 @@ class RemoteSyncServer( override suspend fun register(): String = Dispatchers.IO { try { - val url = "$baseURL/register" + val url = "${preferences.syncBaseURL}/register" Log.i("RemoteSyncServer", "POST $url") val response: RegisterReponse = httpClient.post(url) return@IO response.key @@ -48,7 +49,7 @@ class RemoteSyncServer( override suspend fun put(key: String, newData: SyncData) = Dispatchers.IO { try { - val url = "$baseURL/db/$key" + val url = "${preferences.syncBaseURL}/db/$key" Log.i("RemoteSyncServer", "PUT $url") val response: String = httpClient.put(url) { header("Content-Type", "application/json") @@ -66,7 +67,7 @@ class RemoteSyncServer( override suspend fun getData(key: String): SyncData = Dispatchers.IO { try { - val url = "$baseURL/db/$key" + val url = "${preferences.syncBaseURL}/db/$key" Log.i("RemoteSyncServer", "GET $url") val data: SyncData = httpClient.get(url) return@IO data @@ -80,7 +81,7 @@ class RemoteSyncServer( override suspend fun getDataVersion(key: String): Long = Dispatchers.IO { try { - val url = "$baseURL/db/$key/version" + val url = "${preferences.syncBaseURL}/db/$key/version" Log.i("RemoteSyncServer", "GET $url") val response: GetDataVersionResponse = httpClient.get(url) return@IO response.version diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/Database.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/Database.java index 4c0c834d5..2b70ecc21 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/Database.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/Database.java @@ -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); diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/JdbcDatabase.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/JdbcDatabase.java index 6bbb6261b..93591cfd1 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/JdbcDatabase.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/JdbcDatabase.java @@ -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; + } } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/Logging.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/Logging.kt new file mode 100644 index 000000000..6197de3e3 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/Logging.kt @@ -0,0 +1,33 @@ +/* + * 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.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) +} \ No newline at end of file diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java index 9cbb81f0b..4e3f24b60 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java @@ -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; diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/sync/NetworkManager.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/sync/NetworkManager.kt new file mode 100644 index 000000000..2e181f5ad --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/sync/NetworkManager.kt @@ -0,0 +1,29 @@ +/* + * 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.core.sync + +interface NetworkManager { + fun addListener(listener: Listener) + fun remoteListener(listener: Listener) + interface Listener { + fun onNetworkAvailable() + fun onNetworkLost() + } +} \ No newline at end of file diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/sync/SyncManager.kt similarity index 56% rename from android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.kt rename to android/uhabits-core/src/main/java/org/isoron/uhabits/core/sync/SyncManager.kt index 9b3844c3f..0974090c6 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncManager.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/sync/SyncManager.kt @@ -17,74 +17,63 @@ * with this program. If not, see . */ -package org.isoron.uhabits.sync +package org.isoron.uhabits.core.sync -import android.content.* -import android.net.* -import android.util.* import kotlinx.coroutines.* -import org.isoron.androidbase.* 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 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 javax.inject.* @AppScope class SyncManager @Inject constructor( val preferences: Preferences, - private val importDataTaskFactory: ImportDataTaskFactory, val commandRunner: CommandRunner, - @AppContext val context: Context -) : Preferences.Listener, CommandRunner.Listener, ConnectivityManager.NetworkCallback() { - + 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 server = RemoteSyncServer(baseURL = preferences.syncBaseURL) - - private val tmpFile = File.createTempFile("import", "", context.externalCacheDir) - + private val tmpFile = File.createTempFile("import", "") private var currVersion = 1L - private var dirty = true - - private var taskRunner = SingleThreadTaskRunner() - private lateinit var encryptionKey: EncryptionKey - private lateinit var syncKey: String init { preferences.addListener(this) commandRunner.addListener(this) - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - cm.registerNetworkCallback(NetworkRequest.Builder().build(), this) + networkManager.addListener(this) } fun sync() = CoroutineScope(Dispatchers.Main).launch { if (!preferences.isSyncEnabled) { - Log.i("SyncManager", "Device sync is disabled. Skipping sync.") + logger.info("Device sync is disabled. Skipping sync.") return@launch } encryptionKey = EncryptionKey.fromBase64(preferences.encryptionKey) syncKey = preferences.syncKey - Log.i("SyncManager", "Starting sync (key: $syncKey)") + logger.info("Starting sync (key: $syncKey)") try { pull() push() - Log.i("SyncManager", "Sync finished successfully.") + logger.info("Sync finished successfully.") } catch (e: ConnectionLostException) { - Log.i("SyncManager", "Network unavailable. Aborting sync.") + logger.info("Network unavailable. Aborting sync.") } catch (e: ServiceUnavailable) { - Log.i("SyncManager", "Sync service unavailable. Aborting sync.") + logger.info("Sync service unavailable. Aborting sync.") } catch (e: Exception) { - Log.e("SyncManager", "Unexpected sync exception. Disabling sync.", e) + logger.error("Unexpected sync exception. Disabling sync.") + logger.error(e) preferences.disableSync() } } @@ -95,45 +84,55 @@ class SyncManager @Inject constructor( } if (!dirty) { - Log.i("SyncManager", "Local database not modified. Skipping push.") + logger.info("Local database not modified. Skipping push.") return } - Log.i("SyncManager", "Encrypting local database...") - val db = DatabaseUtils.getDatabaseFile(context) - val encryptedDB = db.encryptToString(encryptionKey) + logger.info("Encrypting local database...") + val encryptedDB = db.file.encryptToString(encryptionKey) val size = encryptedDB.length / 1024 try { - Log.i("SyncManager", "Pushing local database (version $currVersion, $size KB)") + logger.info("Pushing local database (version $currVersion, $size KB)") assertConnected() server.put(preferences.syncKey, SyncData(currVersion, encryptedDB)) dirty = false } catch (e: EditConflictException) { - Log.i("SyncManager", "Sync conflict detected while pushing.") + logger.info("Sync conflict detected while pushing.") setCurrentVersion(0) pull() push(depth = depth + 1) } } - private suspend fun pull() { - Log.i("SyncManager", "Querying remote database version...") + private suspend fun pull() = Dispatchers.IO { + logger.info("Querying remote database version...") assertConnected() val remoteVersion = server.getDataVersion(syncKey) - Log.i("SyncManager", "Remote database version: $remoteVersion") + logger.info("Remote database version: $remoteVersion") 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 { - Log.i("SyncManager", "Pulling remote database...") + logger.info("Pulling remote database...") assertConnected() val data = server.getData(syncKey) val size = data.content.length / 1024 - Log.i("SyncManager", "Pulled remote database (version ${data.version}, $size KB)") - Log.i("SyncManager", "Decrypting remote database and merging with local changes...") + 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) - 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 setCurrentVersion(data.version + 1) } @@ -144,20 +143,20 @@ class SyncManager @Inject constructor( fun onPause() = sync() override fun onSyncEnabled() { - Log.i("SyncManager", "Sync enabled.") + logger.info("Sync enabled.") setCurrentVersion(1) dirty = true sync() } - override fun onAvailable(network: Network) { - Log.i("SyncManager", "Network available.") + override fun onNetworkAvailable() { + logger.info("Network available.") connected = true sync() } - override fun onLost(network: Network) { - Log.i("SyncManager", "Network unavailable.") + override fun onNetworkLost() { + logger.info("Network unavailable.") connected = false } @@ -172,7 +171,7 @@ class SyncManager @Inject constructor( private fun setCurrentVersion(v: Long) { currVersion = v - Log.i("SyncManager", "Setting local database version: $currVersion") + logger.info("Setting local database version: $currVersion") } }