mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 01:08:50 -06:00
Move sync classes to uhabits-core
This commit is contained in:
@@ -33,45 +33,37 @@ import org.isoron.androidbase.activities.*
|
||||
import org.isoron.androidbase.utils.*
|
||||
import org.isoron.androidbase.utils.InterfaceUtils.getFontAwesome
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.tasks.*
|
||||
import org.isoron.uhabits.core.ui.screens.sync.*
|
||||
import org.isoron.uhabits.databinding.*
|
||||
import org.isoron.uhabits.sync.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
|
||||
class SyncActivity : BaseActivity() {
|
||||
class SyncActivity : BaseActivity(), SyncBehavior.Screen {
|
||||
|
||||
private lateinit var syncManager: SyncManager
|
||||
private lateinit var preferences: Preferences
|
||||
private lateinit var taskRunner: TaskRunner
|
||||
private lateinit var baseScreen: BaseScreen
|
||||
private lateinit var binding: ActivitySyncBinding
|
||||
private lateinit var behavior: SyncBehavior
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
private var styledResources = StyledResources(this)
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
baseScreen = BaseScreen(this)
|
||||
|
||||
val component = (application as HabitsApplication).component
|
||||
taskRunner = component.taskRunner
|
||||
preferences = component.preferences
|
||||
syncManager = component.syncManager
|
||||
val preferences = component.preferences
|
||||
val server = RemoteSyncServer(baseURL = preferences.syncBaseURL)
|
||||
baseScreen = BaseScreen(this)
|
||||
behavior = SyncBehavior(this, preferences, server)
|
||||
|
||||
binding = ActivitySyncBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.errorIcon.typeface = getFontAwesome(this)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.elevation = 10.0f
|
||||
|
||||
binding.instructions.setText(Html.fromHtml(resources.getString(R.string.sync_instructions)))
|
||||
|
||||
binding.syncLink.setOnClickListener {
|
||||
copyToClipboard()
|
||||
}
|
||||
@@ -79,96 +71,62 @@ class SyncActivity : BaseActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if(preferences.syncKey.isBlank()) {
|
||||
register()
|
||||
} else {
|
||||
displayCurrentKey()
|
||||
scope.launch {
|
||||
behavior.onResume()
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayCurrentKey() {
|
||||
displayLink("https://loophabits.org/sync/${preferences.syncKey}#${preferences.encryptionKey}")
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
displayLoading()
|
||||
taskRunner.execute(object : Task {
|
||||
private lateinit var encKey: EncryptionKey
|
||||
private lateinit var syncKey: String
|
||||
private var error = false
|
||||
override fun doInBackground() {
|
||||
runBlocking {
|
||||
try {
|
||||
val server = RemoteSyncServer(baseURL = preferences.syncBaseURL)
|
||||
syncKey = server.register()
|
||||
encKey = EncryptionKey.generate()
|
||||
preferences.enableSync(syncKey, encKey.base64)
|
||||
} catch (e: Exception) {
|
||||
Log.e("SyncActivity", "Unexpected exception", e)
|
||||
error = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute() {
|
||||
if (error) {
|
||||
displayError()
|
||||
return;
|
||||
}
|
||||
displayCurrentKey()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun displayLoading() {
|
||||
binding.qrCode.visibility = View.GONE
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
binding.errorPanel.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun displayError() {
|
||||
binding.qrCode.visibility = View.GONE
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.errorPanel.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun copyToClipboard() {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Loop Sync Link", binding.syncLink.text))
|
||||
baseScreen.showMessage(R.string.copied_to_the_clipboard, binding.root)
|
||||
}
|
||||
|
||||
private fun displayLink(link: String) {
|
||||
suspend fun generateQR(msg: String): Bitmap = Dispatchers.IO {
|
||||
val writer = QRCodeWriter()
|
||||
val matrix = writer.encode(msg, BarcodeFormat.QR_CODE, 1024, 1024)
|
||||
val height = matrix.height
|
||||
val width = matrix.width
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
val bgColor = styledResources.getColor(R.attr.highContrastReverseTextColor)
|
||||
val fgColor = styledResources.getColor(R.attr.highContrastTextColor)
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
val color = if (matrix.get(x, y)) fgColor else bgColor
|
||||
bitmap.setPixel(x, y, color)
|
||||
}
|
||||
}
|
||||
return@IO bitmap
|
||||
}
|
||||
|
||||
suspend fun showQR(msg: String) {
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.qrCode.visibility = View.VISIBLE
|
||||
binding.qrCode.setImageBitmap(generateQR(msg))
|
||||
|
||||
}
|
||||
|
||||
override suspend fun showLoadingScreen() {
|
||||
binding.qrCode.visibility = View.GONE
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
binding.errorPanel.visibility = View.GONE
|
||||
}
|
||||
|
||||
override suspend fun showErrorScreen() {
|
||||
binding.qrCode.visibility = View.GONE
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.errorPanel.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override suspend fun showLink(link: String) {
|
||||
binding.qrCode.visibility = View.GONE
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
binding.errorPanel.visibility = View.GONE
|
||||
binding.syncLink.text = link
|
||||
displayQR(link)
|
||||
showQR(link)
|
||||
}
|
||||
|
||||
private fun displayQR(msg: String) {
|
||||
taskRunner.execute(object : Task {
|
||||
lateinit var bitmap: Bitmap
|
||||
override fun doInBackground() {
|
||||
val writer = QRCodeWriter()
|
||||
val matrix = writer.encode(msg, BarcodeFormat.QR_CODE, 1024, 1024)
|
||||
val height = matrix.height
|
||||
val width = matrix.width
|
||||
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
val bgColor = styledResources.getColor(R.attr.highContrastReverseTextColor)
|
||||
val fgColor = styledResources.getColor(R.attr.highContrastTextColor)
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
val color = if (matrix.get(x, y)) fgColor else bgColor
|
||||
bitmap.setPixel(x, y, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onPostExecute() {
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.qrCode.visibility = View.VISIBLE
|
||||
binding.qrCode.setImageBitmap(bitmap)
|
||||
}
|
||||
})
|
||||
override fun logError(msg: String) {
|
||||
Log.e("SyncActivity", msg)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,11 @@ dependencies {
|
||||
implementation 'androidx.annotation:annotation:1.0.0'
|
||||
implementation 'com.google.code.findbugs:jsr305:3.0.2'
|
||||
implementation 'org.apache.commons:commons-lang3:3.5'
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
implementation 'com.google.code.gson:gson:2.8.5'
|
||||
implementation "com.google.guava:guava:30.0-jre"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:$KOTLIN_VERSION"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:1.4-atlassian-1'
|
||||
@@ -23,11 +27,13 @@ dependencies {
|
||||
testImplementation 'org.json:json:20160810'
|
||||
testImplementation 'org.xerial:sqlite-jdbc:3.18.0'
|
||||
testImplementation 'nl.jqno.equalsverifier:equalsverifier:2.4.8'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:$KOTLIN_VERSION"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
||||
|
||||
implementation('com.opencsv:opencsv:3.10') {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION"
|
||||
|
||||
}
|
||||
|
||||
sourceCompatibility = "1.8"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.ui.screens.sync
|
||||
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.sync.*
|
||||
|
||||
class SyncBehavior(
|
||||
val screen: Screen,
|
||||
val preferences: Preferences,
|
||||
val server: AbstractSyncServer,
|
||||
) {
|
||||
suspend fun onResume() {
|
||||
if (preferences.syncKey.isBlank()) {
|
||||
register()
|
||||
} else {
|
||||
displayCurrentKey()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun displayCurrentKey() {
|
||||
screen.showLink("https://loophabits.org/sync/${preferences.syncKey}#${preferences.encryptionKey}")
|
||||
}
|
||||
|
||||
suspend fun register() {
|
||||
screen.showLoadingScreen()
|
||||
try {
|
||||
val syncKey = server.register()
|
||||
val encKey = EncryptionKey.generate()
|
||||
preferences.enableSync(syncKey, encKey.base64)
|
||||
displayCurrentKey()
|
||||
} catch (e: Exception) {
|
||||
screen.logError("Unexpected exception $e")
|
||||
screen.showErrorScreen()
|
||||
}
|
||||
}
|
||||
|
||||
interface Screen {
|
||||
suspend fun showLoadingScreen()
|
||||
suspend fun showErrorScreen()
|
||||
suspend fun showLink(link: String)
|
||||
fun logError(msg: String)
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,11 @@
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
package org.isoron.uhabits.utils
|
||||
package org.isoron.uhabits.sync
|
||||
|
||||
import android.util.*
|
||||
import com.google.common.io.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.apache.commons.codec.binary.*
|
||||
import java.io.*
|
||||
import java.nio.*
|
||||
import java.util.zip.*
|
||||
@@ -52,10 +53,10 @@ class EncryptionKey private constructor(
|
||||
return EncryptionKey(base64, spec)
|
||||
}
|
||||
|
||||
fun generate(): EncryptionKey {
|
||||
suspend fun generate(): EncryptionKey = Dispatchers.IO {
|
||||
try {
|
||||
val generator = KeyGenerator.getInstance("AES").apply { init(256) }
|
||||
return fromSecretKey(generator.generateKey())
|
||||
return@IO fromSecretKey(generator.generateKey())
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
@@ -128,6 +129,6 @@ fun File.encryptToString(key: EncryptionKey): String {
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.encodeBase64(): String = Base64.encodeToString(this, Base64.DEFAULT)
|
||||
fun String.decodeBase64(): ByteArray = Base64.decode(this, Base64.DEFAULT)
|
||||
fun ByteArray.encodeBase64(): String = Base64.encodeBase64(this).decodeToString()
|
||||
fun String.decodeBase64(): ByteArray = Base64.decodeBase64(this.toByteArray())
|
||||
|
||||
@@ -16,18 +16,17 @@
|
||||
* 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.utils
|
||||
package org.isoron.uhabits.core.sync
|
||||
|
||||
import androidx.test.filters.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.hamcrest.Matchers.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.sync.*
|
||||
import org.junit.*
|
||||
import org.junit.Assert.*
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
||||
@MediumTest
|
||||
class EncryptionExtTest : BaseAndroidTest() {
|
||||
class EncryptionExtTest {
|
||||
|
||||
@Test
|
||||
fun test_encode_decode() {
|
||||
@@ -39,7 +38,7 @@ class EncryptionExtTest : BaseAndroidTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_encrypt_decrypt_bytes() {
|
||||
fun test_encrypt_decrypt_bytes() = runBlocking {
|
||||
val original = ByteArray(5000)
|
||||
Random().nextBytes(original)
|
||||
val key = EncryptionKey.generate()
|
||||
@@ -49,7 +48,7 @@ class EncryptionExtTest : BaseAndroidTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_encrypt_decrypt_file() {
|
||||
fun test_encrypt_decrypt_file() = runBlocking {
|
||||
val original = File.createTempFile("file", ".txt")
|
||||
val writer = PrintWriter(original.outputStream())
|
||||
writer.println("hello world")
|
||||
Reference in New Issue
Block a user