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