From 017bc50698ded62e57d4855f995c854a6e18d31a Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 21 Dec 2020 17:02:49 -0600 Subject: [PATCH] Move sync classes to uhabits-core --- .../uhabits/activities/sync/SyncActivity.kt | 122 ++++++------------ android/uhabits-core/build.gradle | 8 +- .../core/ui/screens/sync/SyncBehavior.kt | 61 +++++++++ .../isoron/uhabits/sync/AbstractSyncServer.kt | 0 .../org/isoron/uhabits/sync}/EncryptionExt.kt | 13 +- .../java/org/isoron/uhabits/sync/SyncData.kt | 0 .../org/isoron/uhabits/sync/SyncException.kt | 0 .../uhabits/core/sync}/EncryptionExtTest.kt | 13 +- 8 files changed, 121 insertions(+), 96 deletions(-) create mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/sync/SyncBehavior.kt rename android/{uhabits-android => uhabits-core}/src/main/java/org/isoron/uhabits/sync/AbstractSyncServer.kt (100%) rename android/{uhabits-android/src/main/java/org/isoron/uhabits/utils => uhabits-core/src/main/java/org/isoron/uhabits/sync}/EncryptionExt.kt (91%) rename android/{uhabits-android => uhabits-core}/src/main/java/org/isoron/uhabits/sync/SyncData.kt (100%) rename android/{uhabits-android => uhabits-core}/src/main/java/org/isoron/uhabits/sync/SyncException.kt (100%) rename android/{uhabits-android/src/androidTest/java/org/isoron/uhabits/utils => uhabits-core/src/test/java/org/isoron/uhabits/core/sync}/EncryptionExtTest.kt (90%) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt index d05c5c70a..b1272761f 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/sync/SyncActivity.kt @@ -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 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 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 - } - } + 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 fun onPostExecute() { - if (error) { - displayError() - return; - } - displayCurrentKey() - } - }) } - private fun displayLoading() { + override suspend fun showLoadingScreen() { binding.qrCode.visibility = View.GONE binding.progress.visibility = View.VISIBLE binding.errorPanel.visibility = View.GONE } - private fun displayError() { + override suspend fun showErrorScreen() { 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) { + 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) } } \ No newline at end of file diff --git a/android/uhabits-core/build.gradle b/android/uhabits-core/build.gradle index 0e89a2eb7..cf004ef34 100644 --- a/android/uhabits-core/build.gradle +++ b/android/uhabits-core/build.gradle @@ -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" diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/sync/SyncBehavior.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/sync/SyncBehavior.kt new file mode 100644 index 000000000..bb84b3bac --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/sync/SyncBehavior.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016-2020 Álinson Santos Xavier + * + * 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 . + */ + +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) + } +} \ No newline at end of file diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/AbstractSyncServer.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/sync/AbstractSyncServer.kt similarity index 100% rename from android/uhabits-android/src/main/java/org/isoron/uhabits/sync/AbstractSyncServer.kt rename to android/uhabits-core/src/main/java/org/isoron/uhabits/sync/AbstractSyncServer.kt diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/utils/EncryptionExt.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/sync/EncryptionExt.kt similarity index 91% rename from android/uhabits-android/src/main/java/org/isoron/uhabits/utils/EncryptionExt.kt rename to android/uhabits-core/src/main/java/org/isoron/uhabits/sync/EncryptionExt.kt index 86f0cce44..fea844d3c 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/utils/EncryptionExt.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/sync/EncryptionExt.kt @@ -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()) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncData.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/sync/SyncData.kt similarity index 100% rename from android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncData.kt rename to android/uhabits-core/src/main/java/org/isoron/uhabits/sync/SyncData.kt diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncException.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/sync/SyncException.kt similarity index 100% rename from android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncException.kt rename to android/uhabits-core/src/main/java/org/isoron/uhabits/sync/SyncException.kt diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/utils/EncryptionExtTest.kt b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/sync/EncryptionExtTest.kt similarity index 90% rename from android/uhabits-android/src/androidTest/java/org/isoron/uhabits/utils/EncryptionExtTest.kt rename to android/uhabits-core/src/test/java/org/isoron/uhabits/core/sync/EncryptionExtTest.kt index 5569ef1f7..0525f4363 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/utils/EncryptionExtTest.kt +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/sync/EncryptionExtTest.kt @@ -16,18 +16,17 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ -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")