From c7362bce12e5615d36eef04df93ed384491a339f Mon Sep 17 00:00:00 2001 From: kuba2k2 Date: Thu, 13 Oct 2022 21:27:55 +0200 Subject: [PATCH] [API/Usos] Add base rest API class. --- .../data/api/edziennik/usos/DataUsos.kt | 25 +++ .../data/api/edziennik/usos/data/UsosApi.kt | 170 ++++++++++++++++++ .../data/api/edziennik/usos/data/UsosData.kt | 49 +++++ .../edziennik/ext/TextExtensions.kt | 8 + .../utils/managers/UserActionManager.kt | 16 +- 5 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index b4a925bf..1dee14ba 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -27,6 +27,21 @@ class DataUsos( override fun generateUserCode() = "USOS:TEST" + var instanceUrl: String? + get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl } + set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value } + private var mInstanceUrl: String? = null + + var oauthConsumerKey: String? + get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey } + set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value } + private var mOauthConsumerKey: String? = null + + var oauthConsumerSecret: String? + get() { mOauthConsumerSecret = mOauthConsumerSecret ?: loginStore.getLoginData("oauthConsumerSecret", null); return mOauthConsumerSecret } + set(value) { loginStore.putLoginData("oauthConsumerSecret", value); mOauthConsumerSecret = value } + private var mOauthConsumerSecret: String? = null + var oauthTokenKey: String? get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey } set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value } @@ -36,4 +51,14 @@ class DataUsos( get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret } set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value } private var mOauthTokenSecret: String? = null + + var studentId: String? + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } + set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + private var mStudentId: String? = null + + var studentNumber: String? + get() { mStudentNumber = mStudentNumber ?: profile?.getStudentData("studentNumber", null); return mStudentNumber } + set(value) { profile?.putStudentData("studentNumber", value) ?: return; mStudentNumber = value } + private var mStudentNumber: String? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt new file mode 100644 index 00000000..6ffcd46a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) Kuba SzczodrzyƄski 2022-10-13. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import im.wangchao.mhttp.AbsCallbackHandler +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.body.MediaTypeUtils +import im.wangchao.mhttp.callback.JsonArrayCallbackHandler +import im.wangchao.mhttp.callback.JsonCallbackHandler +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE +import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.hmacSHA1 +import pl.szczodrzynski.edziennik.ext.toQueryString +import pl.szczodrzynski.edziennik.ext.urlEncode +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection.* +import java.util.UUID + +open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { + companion object { + private const val TAG = "UsosApi" + } + + enum class ResponseType { + OBJECT, + ARRAY, + PLAIN, + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + private fun valueToString(value: Any) = when (value) { + is String -> value + is Number -> value.toString() + is List<*> -> listToString(value) + else -> value.toString() + } + + private fun listToString(list: List<*>): String { + return list.map { + if (it is Pair<*, *> && it.first is String && it.second is List<*>) + return@map "${it.first}[${listToString(it.second as List<*>)}]" + return@map valueToString(it ?: "") + }.joinToString("|") + } + + private fun buildSignature(method: String, url: String, params: Map): String { + val query = params.toQueryString() + val signatureString = listOf( + method.uppercase(), + url.urlEncode(), + query.urlEncode(), + ).joinToString("&") + val signingKey = listOf( + data.oauthConsumerSecret ?: "", + data.oauthTokenSecret ?: "", + ).joinToString("&") { it.urlEncode() } + return signatureString.hmacSHA1(signingKey) + } + + fun apiRequest( + tag: String, + service: String, + params: Map, + responseType: ResponseType, + onSuccess: (data: T) -> Unit, + ) { + val url = "${data.instanceUrl}/services/$service" + d(tag, "Request: Usos/Api - $url") + val formData = params.mapValues { + valueToString(it.value) + } + val auth = mutableMapOf( + "realm" to url, + "oauth_consumer_key" to (data.oauthConsumerKey ?: ""), + "oauth_nonce" to UUID.randomUUID().toString(), + "oauth_signature_method" to "HMAC-SHA1", + "oauth_timestamp" to currentTimeUnix().toString(), + "oauth_token" to (data.oauthTokenKey ?: ""), + "oauth_version" to "1.0", + ) + val signature = buildSignature("POST", url, formData + auth) + auth["oauth_signature"] = signature + + val authString = auth.map { + """${it.key}="${it.value.urlEncode()}"""" + }.joinToString(", ") + + Request.builder() + .url(url) + .userAgent(SERVER_USER_AGENT) + .addHeader("Authorization", "OAuth $authString") + .post() + .setTextBody(formData.toQueryString(), MediaTypeUtils.APPLICATION_FORM) + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_UNAUTHORIZED) + .allowErrorCode(HTTP_FORBIDDEN) + .allowErrorCode(HTTP_NOT_FOUND) + .allowErrorCode(HTTP_UNAVAILABLE) + .callback(getCallback(tag, responseType, onSuccess)) + .build() + .enqueue() + } + + @Suppress("UNCHECKED_CAST") + private fun getCallback( + tag: String, + responseType: ResponseType, + onSuccess: (data: T) -> Unit, + ) = when (responseType) { + ResponseType.OBJECT -> object : JsonCallbackHandler() { + override fun onSuccess(data: JsonObject?, response: Response?) { + processResponse(response, data as T, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + ResponseType.ARRAY -> object : JsonArrayCallbackHandler() { + override fun onSuccess(data: JsonArray?, response: Response?) { + processResponse(response, data as T, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + ResponseType.PLAIN -> object : TextCallbackHandler() { + override fun onSuccess(data: String?, response: Response?) { + processResponse(response, data as T, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + } + + private fun processResponse( + response: Response?, + data: T?, + onSuccess: (data: T) -> Unit, + ) { + + } + + private fun processError( + tag: String, + response: Response?, + throwable: Throwable?, + ) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt new file mode 100644 index 00000000..bea52ea3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) Kuba SzczodrzyƄski 2022-10-13. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER +import pl.szczodrzynski.edziennik.utils.Utils.d + +class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + val id = data.targetEndpointIds.firstKey() + val lastSync = data.targetEndpointIds.remove(id) + useEndpoint(id, lastSync) { endpointId -> + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) { + d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync") + when (endpointId) { + ENDPOINT_USOS_API_USER -> { + data.startProgress(R.string.edziennik_progress_endpoint_student_info) + TemplateWebSample(data, lastSync, onSuccess) + } + else -> onSuccess(endpointId) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt index ebfb1410..84fe6631 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -18,6 +18,7 @@ import android.text.style.StyleSpan import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.mikepenz.materialdrawer.holder.StringHolder +import java.net.URLEncoder fun CharSequence?.isNotNullNorEmpty(): Boolean { return this != null && this.isNotEmpty() @@ -343,3 +344,10 @@ fun Int.toStringHolder() = StringHolder(this) fun CharSequence.toStringHolder() = StringHolder(this) fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this) + +fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20") + +fun Map.toQueryString() = this + .map { it.key.urlEncode() to it.value.urlEncode() } + .sortedBy { it.first } + .joinToString("&") { "${it.first}=${it.second}" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index 76d5733d..537915ac 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -99,8 +99,8 @@ class UserActionManager(val app: App) { private fun executeLibrus( activity: AppCompatActivity, profileId: Int?, - onSuccess: ((params: Bundle) -> Unit)? = null, - onFailure: (() -> Unit)? = null, + onSuccess: ((params: Bundle) -> Unit)?, + onFailure: (() -> Unit)?, ) { if (profileId == null) return @@ -125,4 +125,16 @@ class UserActionManager(val app: App) { onFailure = onFailure ).show() } + + private fun executeOauth( + activity: AppCompatActivity, + profileId: Int?, + params: Bundle?, + onSuccess: ((params: Bundle) -> Unit)?, + onFailure: (() -> Unit)?, + ) { + if (profileId == null || params == null) + return + + } }