mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Move SyncManager to uhabits-core
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user