mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Monitor network availability; other minor fixes
This commit is contained in:
@@ -24,6 +24,7 @@ import android.content.ClipboardManager
|
|||||||
import android.graphics.*
|
import android.graphics.*
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.text.*
|
import android.text.*
|
||||||
|
import android.util.*
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import com.google.zxing.*
|
import com.google.zxing.*
|
||||||
import com.google.zxing.qrcode.*
|
import com.google.zxing.qrcode.*
|
||||||
@@ -98,12 +99,13 @@ class SyncActivity : BaseActivity() {
|
|||||||
private var error = false
|
private var error = false
|
||||||
override fun doInBackground() {
|
override fun doInBackground() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val server = RemoteSyncServer(baseURL = preferences.syncBaseURL)
|
|
||||||
try {
|
try {
|
||||||
|
val server = RemoteSyncServer(baseURL = preferences.syncBaseURL)
|
||||||
syncKey = server.register()
|
syncKey = server.register()
|
||||||
encKey = EncryptionKey.generate()
|
encKey = EncryptionKey.generate()
|
||||||
preferences.enableSync(syncKey, encKey.base64)
|
preferences.enableSync(syncKey, encKey.base64)
|
||||||
} catch (e: ServiceUnavailable) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("SyncActivity", "Unexpected exception", e)
|
||||||
error = true
|
error = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import io.ktor.client.request.*
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
class RemoteSyncServer(
|
class RemoteSyncServer(
|
||||||
private val baseURL: String = "https://sync.loophabits.org",
|
private val baseURL: String,
|
||||||
private val httpClient: HttpClient = HttpClient(Android) {
|
private val httpClient: HttpClient = HttpClient(Android) {
|
||||||
install(JsonFeature)
|
install(JsonFeature)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package org.isoron.uhabits.sync
|
package org.isoron.uhabits.sync
|
||||||
|
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
import android.net.*
|
||||||
import android.util.*
|
import android.util.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.isoron.androidbase.*
|
import org.isoron.androidbase.*
|
||||||
@@ -30,60 +31,82 @@ import org.isoron.uhabits.core.tasks.*
|
|||||||
import org.isoron.uhabits.tasks.*
|
import org.isoron.uhabits.tasks.*
|
||||||
import org.isoron.uhabits.utils.*
|
import org.isoron.uhabits.utils.*
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.lang.RuntimeException
|
|
||||||
import javax.inject.*
|
import javax.inject.*
|
||||||
|
|
||||||
@AppScope
|
@AppScope
|
||||||
class SyncManager @Inject constructor(
|
class SyncManager @Inject constructor(
|
||||||
val preferences: Preferences,
|
val preferences: Preferences,
|
||||||
val importDataTaskFactory: ImportDataTaskFactory,
|
private val importDataTaskFactory: ImportDataTaskFactory,
|
||||||
val commandRunner: CommandRunner,
|
val commandRunner: CommandRunner,
|
||||||
@AppContext val context: Context
|
@AppContext val context: Context
|
||||||
) : Preferences.Listener, CommandRunner.Listener {
|
) : Preferences.Listener, CommandRunner.Listener, ConnectivityManager.NetworkCallback() {
|
||||||
|
|
||||||
|
private var connected = false
|
||||||
|
|
||||||
|
private val server = RemoteSyncServer(baseURL = preferences.syncBaseURL)
|
||||||
|
|
||||||
private val server = RemoteSyncServer()
|
|
||||||
private val tmpFile = File.createTempFile("import", "", context.externalCacheDir)
|
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 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
|
||||||
|
cm.registerNetworkCallback(NetworkRequest.Builder().build(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sync() {
|
fun sync() = CoroutineScope(Dispatchers.Main).launch {
|
||||||
if (!preferences.isSyncEnabled) {
|
if (!preferences.isSyncEnabled) {
|
||||||
Log.i("SyncManager", "Device sync is disabled. Skipping sync.")
|
Log.i("SyncManager", "Device sync is disabled. Skipping sync.")
|
||||||
return
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptionKey = EncryptionKey.fromBase64(preferences.encryptionKey)
|
encryptionKey = EncryptionKey.fromBase64(preferences.encryptionKey)
|
||||||
syncKey = preferences.syncKey
|
syncKey = preferences.syncKey
|
||||||
try {
|
|
||||||
Log.i("SyncManager", "Starting sync (key: $syncKey)")
|
Log.i("SyncManager", "Starting sync (key: $syncKey)")
|
||||||
|
|
||||||
|
try {
|
||||||
pull()
|
pull()
|
||||||
push()
|
push()
|
||||||
Log.i("SyncManager", "Sync finished")
|
} catch (e: ConnectionLostException) {
|
||||||
|
Log.i("SyncManager", "Network unavailable. Aborting sync.")
|
||||||
|
} catch (e: ServiceUnavailable) {
|
||||||
|
Log.i("SyncManager", "Sync service unavailable. Aborting sync.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("SyncManager", "Unexpected sync exception. Disabling sync", e)
|
Log.e("SyncManager", "Unexpected sync exception. Disabling sync.", e)
|
||||||
preferences.disableSync()
|
preferences.disableSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.i("SyncManager", "Sync finished successfully.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun push(depth: Int = 0) {
|
private suspend fun push(depth: Int = 0) {
|
||||||
if (depth >= 5) {
|
if (depth >= 5) {
|
||||||
throw RuntimeException()
|
throw RuntimeException()
|
||||||
}
|
}
|
||||||
if (dirty) {
|
|
||||||
|
if (!dirty) {
|
||||||
|
Log.i("SyncManager", "Local database not modified. Skipping push.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Log.i("SyncManager", "Encrypting local database...")
|
Log.i("SyncManager", "Encrypting local database...")
|
||||||
val db = DatabaseUtils.getDatabaseFile(context)
|
val db = DatabaseUtils.getDatabaseFile(context)
|
||||||
val encryptedDB = db.encryptToString(encryptionKey)
|
val encryptedDB = db.encryptToString(encryptionKey)
|
||||||
val size = encryptedDB.length / 1024
|
val size = encryptedDB.length / 1024
|
||||||
Log.i("SyncManager", "Pushing local database (version $currVersion, $size KB)")
|
|
||||||
try {
|
try {
|
||||||
|
Log.i("SyncManager", "Pushing local database (version $currVersion, $size KB)")
|
||||||
|
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) {
|
||||||
@@ -92,20 +115,19 @@ class SyncManager @Inject constructor(
|
|||||||
pull()
|
pull()
|
||||||
push(depth = depth + 1)
|
push(depth = depth + 1)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.i("SyncManager", "Local database not modified. Skipping push.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun pull() {
|
private suspend fun pull() {
|
||||||
Log.i("SyncManager", "Querying remote database version...")
|
Log.i("SyncManager", "Querying remote database version...")
|
||||||
|
assertConnected()
|
||||||
val remoteVersion = server.getDataVersion(syncKey)
|
val remoteVersion = server.getDataVersion(syncKey)
|
||||||
Log.i("SyncManager", "Remote database has version $remoteVersion")
|
Log.i("SyncManager", "Remote database version: $remoteVersion")
|
||||||
|
|
||||||
if (remoteVersion <= currVersion) {
|
if (remoteVersion <= currVersion) {
|
||||||
Log.i("SyncManager", "Local database is up-to-date. Skipping merge.")
|
Log.i("SyncManager", "Local database is up-to-date. Skipping merge.")
|
||||||
} else {
|
} else {
|
||||||
Log.i("SyncManager", "Pulling remote database...")
|
Log.i("SyncManager", "Pulling remote database...")
|
||||||
|
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)")
|
Log.i("SyncManager", "Pulled remote database (version ${data.version}, $size KB)")
|
||||||
@@ -117,29 +139,41 @@ class SyncManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCurrentVersion(v: Long) {
|
fun onResume() = sync()
|
||||||
currVersion = v
|
|
||||||
Log.i("SyncManager", "Setting local database version to $currVersion")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onResume() {
|
fun onPause() = sync()
|
||||||
sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onPause() {
|
|
||||||
sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSyncEnabled() {
|
override fun onSyncEnabled() {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
Log.i("SyncManager", "Sync enabled.")
|
||||||
|
setCurrentVersion(1)
|
||||||
|
dirty = true
|
||||||
sync()
|
sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
Log.i("SyncManager", "Network available.")
|
||||||
|
connected = true
|
||||||
|
sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
Log.i("SyncManager", "Network unavailable.")
|
||||||
|
connected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
|
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
|
||||||
if (!dirty) {
|
if (!dirty) setCurrentVersion(currVersion + 1)
|
||||||
setCurrentVersion(currVersion + 1)
|
|
||||||
}
|
|
||||||
dirty = true
|
dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun assertConnected() {
|
||||||
|
if (!connected) throw ConnectionLostException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setCurrentVersion(v: Long) {
|
||||||
|
currVersion = v
|
||||||
|
Log.i("SyncManager", "Setting local database version: $currVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionLostException : RuntimeException()
|
||||||
|
|||||||
Reference in New Issue
Block a user