From a2400172e2c57f6e61a1ae6e086daa244f8b3b81 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sun, 22 Nov 2020 13:00:59 -0600 Subject: [PATCH] Make registration functional --- android/gradle.properties | 1 + android/uhabits-android/build.gradle | 6 + .../uhabits/sync/RemoteSyncServerTest.kt | 133 ++++++++++++++++++ .../src/main/AndroidManifest.xml | 2 + .../uhabits/activities/sync/SyncActivity.kt | 54 ++++++- .../isoron/uhabits/sync/AbstractSyncServer.kt | 60 ++++++++ .../isoron/uhabits/sync/RemoteSyncServer.kt | 80 +++++++++++ .../java/org/isoron/uhabits/sync/SyncData.kt | 25 ++++ .../org/isoron/uhabits/sync/SyncException.kt | 28 ++++ .../src/main/res/layout/activity_sync.xml | 66 ++++++--- .../src/main/res/values/fontawesome.xml | 2 +- 11 files changed, 437 insertions(+), 20 deletions(-) create mode 100644 android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/sync/AbstractSyncServer.kt create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncData.kt create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncException.kt diff --git a/android/gradle.properties b/android/gradle.properties index 570285804..ca000507d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -10,6 +10,7 @@ KOTLIN_VERSION = 1.3.61 SUPPORT_LIBRARY_VERSION = 28.0.0 AUTO_FACTORY_VERSION = 1.0-beta6 BUILD_TOOLS_VERSION = 4.0.0 +KTOR_VERSION=1.4.2 org.gradle.parallel=false org.gradle.daemon=true diff --git a/android/uhabits-android/build.gradle b/android/uhabits-android/build.gradle index a4ce06526..c1f7b6659 100644 --- a/android/uhabits-android/build.gradle +++ b/android/uhabits-android/build.gradle @@ -94,6 +94,10 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KOTLIN_VERSION" implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4" implementation 'com.google.zxing:core:3.4.1' + implementation "io.ktor:ktor-client-core:$KTOR_VERSION" + implementation "io.ktor:ktor-client-android:$KTOR_VERSION" + implementation "io.ktor:ktor-client-json:$KTOR_VERSION" + implementation "io.ktor:ktor-client-jackson:$KTOR_VERSION" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' compileOnly "javax.annotation:jsr250-api:1.0" @@ -113,6 +117,8 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation "com.google.guava:guava:24.1-android" + androidTestImplementation "io.ktor:ktor-client-mock:$KTOR_VERSION" + androidTestImplementation "io.ktor:ktor-jackson:$KTOR_VERSION" androidTestImplementation project(":uhabits-core") kaptAndroidTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION" diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt new file mode 100644 index 000000000..e9ef2c3c5 --- /dev/null +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/sync/RemoteSyncServerTest.kt @@ -0,0 +1,133 @@ +/* + * 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.sync + +import com.fasterxml.jackson.databind.* +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.features.json.* +import io.ktor.client.request.* +import io.ktor.http.* +import junit.framework.Assert.* +import kotlinx.coroutines.* +import org.junit.* + +class RemoteSyncServerTest { + + private val mapper = ObjectMapper() + val data = SyncData(1, "Hello world") + + @Test + fun when_register_succeeds_should_return_key() = runBlocking { + val server = server("/register") { + respondWithJson(RegisterReponse("ABCDEF")) + } + assertEquals("ABCDEF", server.register()) + } + + @Test(expected = ServiceUnavailable::class) + fun when_register_fails_should_raise_correct_exception() = runBlocking { + val server = server("/register") { + respondError(HttpStatusCode.ServiceUnavailable) + } + server.register() + return@runBlocking + } + + @Test + fun when_get_data_version_succeeds_should_return_version() = runBlocking { + server("/ABC/version") { + respondWithJson(GetDataVersionResponse(5)) + }.apply { + assertEquals(5, getDataVersion("ABC")) + } + return@runBlocking + } + + @Test(expected = ServiceUnavailable::class) + fun when_get_data_version_with_server_error_should_raise_exception() = runBlocking { + server("/ABC/version") { + respondError(HttpStatusCode.InternalServerError) + }.apply { + getDataVersion("ABC") + } + return@runBlocking + } + + @Test(expected = KeyNotFoundException::class) + fun when_get_data_version_with_invalid_key_should_raise_exception() = runBlocking { + server("/ABC/version") { + respondError(HttpStatusCode.NotFound) + }.apply { + getDataVersion("ABC") + } + return@runBlocking + } + + @Test + fun when_get_data_succeeds_should_return_data() = runBlocking { + server("/ABC") { + respondWithJson(data) + }.apply { + assertEquals(data, getData("ABC")) + } + return@runBlocking + } + + @Test(expected = KeyNotFoundException::class) + fun when_get_data_with_invalid_key_should_raise_exception() = runBlocking { + server("/ABC") { + respondError(HttpStatusCode.NotFound) + }.apply { + getData("ABC") + } + return@runBlocking + } + + @Test + fun when_put_succeeds_should_not_raise_exceptions() = runBlocking { + server("/ABC") { + respondOk() + }.apply { + put("ABC", data) + } + return@runBlocking + } + + private fun server(expectedPath: String, + action: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData + ): AbstractSyncServer { + return RemoteSyncServer(httpClient = HttpClient(MockEngine) { + install(JsonFeature) + engine { + addHandler { request -> + when (request.url.fullPath) { + expectedPath -> action(request) + else -> error("unexpected call: ${request.url.fullPath}") + } + } + } + }) + } + + private fun MockRequestHandleScope.respondWithJson(content: Any) = + respond(mapper.writeValueAsBytes(content), + headers = headersOf("Content-Type" to listOf("application/json"))) +} \ No newline at end of file diff --git a/android/uhabits-android/src/main/AndroidManifest.xml b/android/uhabits-android/src/main/AndroidManifest.xml index 549e635e9..f043a92fb 100644 --- a/android/uhabits-android/src/main/AndroidManifest.xml +++ b/android/uhabits-android/src/main/AndroidManifest.xml @@ -23,6 +23,8 @@ + + + * + * 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.sync + +interface AbstractSyncServer { + /** + * Generates and returns a new sync key, which can be used to store and retrive + * data. + * + * @throws ServiceUnavailable If key cannot be generated at this time, for example, + * due to insufficient server resources, temporary server maintenance or network problems. + */ + suspend fun register(): String + + /** + * Replaces data for a given sync key. + * + * @throws KeyNotFoundException If key is not found + * @throws EditConflictException If the version of the data provided is not + * exactly the current data version plus one. + * @throws ServiceUnavailable If data cannot be put at this time, for example, due + * to insufficient server resources or network problems. + */ + suspend fun put(key: String, newData: SyncData) + + /** + * Returns data for a given sync key. + * + * @throws KeyNotFoundException If key is not found + * @throws ServiceUnavailable If data cannot be retrieved at this time, for example, due + * to insufficient server resources or network problems. + */ + suspend fun getData(key: String): SyncData + + /** + * Returns the current data version for the given key + * + * @throws KeyNotFoundException If key is not found + * @throws ServiceUnavailable If data cannot be retrieved at this time, for example, due + * to insufficient server resources or network problems. + */ + suspend fun getDataVersion(key: String): Long +} \ No newline at end of file diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt new file mode 100644 index 000000000..bfe1b5714 --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/RemoteSyncServer.kt @@ -0,0 +1,80 @@ +/* + * 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.sync + +import io.ktor.client.* +import io.ktor.client.engine.android.* +import io.ktor.client.features.* +import io.ktor.client.features.json.* +import io.ktor.client.request.* + +data class RegisterReponse(val key: String) +data class GetDataVersionResponse(val version: Long) + +class RemoteSyncServer( + private val baseURL: String = "https://sync.loophabits.org", + private val httpClient: HttpClient = HttpClient(Android) { + install(JsonFeature) + } +) : AbstractSyncServer { + + override suspend fun register(): String { + try { + val response: RegisterReponse = httpClient.post("$baseURL/register") + return response.key + } catch(e: ServerResponseException) { + throw ServiceUnavailable() + } + } + + override suspend fun put(key: String, newData: SyncData) { + try { + val response: String = httpClient.put("$baseURL/$key") { + header("Content-Type", "application/json") + body = newData + } + } catch (e: ServerResponseException) { + throw ServiceUnavailable() + } catch (e: ClientRequestException) { + throw KeyNotFoundException() + } + } + + override suspend fun getData(key: String): SyncData { + try { + return httpClient.get("$baseURL/$key") + } catch (e: ServerResponseException) { + throw ServiceUnavailable() + } catch (e: ClientRequestException) { + throw KeyNotFoundException() + } + } + + override suspend fun getDataVersion(key: String): Long { + try { + val response: GetDataVersionResponse = httpClient.get("$baseURL/$key/version") + return response.version + } catch(e: ServerResponseException) { + throw ServiceUnavailable() + } catch (e: ClientRequestException) { + throw KeyNotFoundException() + } + } +} diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncData.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncData.kt new file mode 100644 index 000000000..5f57d3c9c --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncData.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016-2020 Alinson 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.sync + +data class SyncData( + val version: Long, + val content: String +) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncException.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncException.kt new file mode 100644 index 000000000..5c5a81403 --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/sync/SyncException.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016-2020 Alinson 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.sync + +open class SyncException: RuntimeException() + +class KeyNotFoundException: SyncException() + +class ServiceUnavailable: SyncException() + +class EditConflictException: SyncException() \ No newline at end of file diff --git a/android/uhabits-android/src/main/res/layout/activity_sync.xml b/android/uhabits-android/src/main/res/layout/activity_sync.xml index b2f8dcb52..660d31bbd 100644 --- a/android/uhabits-android/src/main/res/layout/activity_sync.xml +++ b/android/uhabits-android/src/main/res/layout/activity_sync.xml @@ -52,6 +52,55 @@ android:id="@+id/instructions" /> + + + + + + + + + + + + + + + + + + + @@ -73,22 +122,7 @@ - - - - - - - - - + diff --git a/android/uhabits-android/src/main/res/values/fontawesome.xml b/android/uhabits-android/src/main/res/values/fontawesome.xml index 7c95f1b38..d4c3b4687 100644 --- a/android/uhabits-android/src/main/res/values/fontawesome.xml +++ b/android/uhabits-android/src/main/res/values/fontawesome.xml @@ -26,6 +26,7 @@ + @@ -123,7 +124,6 @@ -