Monitor network availability; other minor fixes

pull/699/head
Alinson S. Xavier 5 years ago
parent 2b9fd74a1d
commit 6df5e9ebe9

@ -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() { override fun onSyncEnabled() {
Log.i("SyncManager", "Sync enabled.")
setCurrentVersion(1)
dirty = true
sync() sync()
} }
override fun onSyncEnabled() { override fun onAvailable(network: Network) {
CoroutineScope(Dispatchers.Main).launch { Log.i("SyncManager", "Network available.")
connected = true
sync() 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()

Loading…
Cancel
Save