commit b6aaa50d419125384134d8a4435bac045480eb40
parent 74a63c75bda18044c747ebe3b9efa3f895cab9d3
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 12 Jun 2024 11:06:28 -0600
android: implement native networking API
Diffstat:
2 files changed, 225 insertions(+), 1 deletion(-)
diff --git a/QuickJS-android/qtart/src/main/java/net/taler/qtart/Networking.kt b/QuickJS-android/qtart/src/main/java/net/taler/qtart/Networking.kt
@@ -0,0 +1,116 @@
+package net.taler.qtart
+
+import android.util.Log
+import com.sun.jna.StringArray
+import java.util.concurrent.atomic.AtomicInteger
+import net.taler.qtart.TalerWalletCore.TalerNative.*
+
+object Networking {
+ private val lastRequestId = AtomicInteger(0)
+
+ interface RequestHandler {
+ fun handleRequest(req: RequestInfo, id: Int, sendResponse: (resp: ResponseInfo) -> Unit)
+ fun cancelRequest(id: Int): Boolean
+ }
+
+ enum class RedirectMode(val value: Int) {
+ Unknown(-1),
+
+ /**
+ * Handle redirects transparently.
+ */
+ Transparent(0),
+
+ /**
+ * Redirect status codes are returned to the client.
+ * The client can choose to follow them manually (or not).
+ */
+ Manual(1),
+
+ /**
+ * All redirect status codes result in an error.
+ */
+ Error(2);
+
+ companion object {
+ fun fromValue(value: Int): RedirectMode =
+ RedirectMode.values().find { it.value == value } ?: Unknown
+ }
+ }
+
+ class RequestInfo(
+ val url: String,
+ val method: String,
+ val headers: Array<String>,
+ val redirectMode: RedirectMode,
+ val timeoutMs: Long,
+ val debug: Boolean,
+ val body: ByteArray?,
+ )
+
+ class ResponseInfo(
+ val requestId: Int,
+ val status: Int,
+ val errorMsg: String?,
+ val headers: Array<String>,
+ val body: ByteArray?,
+ )
+
+ /**
+ * Function to create a new HTTP fetch request.
+ * The request can still be configured until it is started.
+ * An identifier for the request will be written to @a handle.
+ *
+ * @return negative number on error, positive request_id on success
+ */
+ internal fun httpCreateRequest(
+ reqInfo: JSHttpRequestInfo,
+ reqHandler: RequestHandler,
+ ) {
+ val requestId = lastRequestId.addAndGet(1)
+
+ reqHandler.handleRequest(
+ req = RequestInfo(
+ url = reqInfo.url!!,
+ method = reqInfo.method!!,
+ headers = reqInfo.request_headers!!.getStringArray(0),
+ redirectMode = RedirectMode.fromValue(reqInfo.redirect!!),
+ timeoutMs = reqInfo.timeout_ms!!.toLong(),
+ debug = reqInfo.debug != 0,
+ body = if (reqInfo.req_body_len!! > 0) {
+ reqInfo.req_body!!.getByteArray(0, reqInfo.req_body_len!!)
+ } else null,
+ ),
+ id = requestId,
+ ) { resp ->
+ Log.d("Networking", "Receiving response from Android side")
+ val rawResp = JSHttpResponseInfo()
+ rawResp.request_id = resp.requestId
+ rawResp.status = resp.status
+ rawResp.errmsg = resp.errorMsg
+ rawResp.response_headers = StringArray(resp.headers, false)
+ rawResp.num_response_headers = resp.headers.size
+ if (resp.body != null) {
+ rawResp.body = resp.body
+ rawResp.body_len = resp.body.size
+ } else {
+ rawResp.body_len = 0
+ }
+
+ Log.d("Networking", "Response ready to send to Qtart: $rawResp")
+ reqInfo.response_cb?.invoke(reqInfo.response_cb_cls!!, rawResp)
+ }
+ }
+
+ /**
+ * Cancel a request. The request_id will become invalid
+ * and the callback won't be called with request_id.
+ */
+ internal fun httpCancelRequest(
+ requestId: Int,
+ handler: RequestHandler,
+ ): Int {
+ val success = handler.cancelRequest(requestId)
+ return if (success) requestId else -requestId
+ }
+}
+\ No newline at end of file
diff --git a/QuickJS-android/qtart/src/main/java/net/taler/qtart/TalerWalletCore.kt b/QuickJS-android/qtart/src/main/java/net/taler/qtart/TalerWalletCore.kt
@@ -36,9 +36,10 @@ import com.sun.jna.Callback
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
+import com.sun.jna.Structure
class TalerWalletCore {
- private interface TalerNative : Library {
+ internal interface TalerNative : Library {
companion object {
val INSTANCE: TalerNative by lazy {
Native.load("talerwalletcore", TalerNative::class.java)
@@ -65,6 +66,80 @@ class TalerWalletCore {
fun TALER_WALLET_join(twi: Pointer)
fun TALER_start_redirect_std(logfn: TALER_LogFn, cls: Pointer)
fun TALER_set_curl_http_client(twi: Pointer)
+
+ /**
+ * Native networking
+ */
+
+ class JSHttpRequestInfo: Structure() {
+ @JvmField var response_cb: JSHttpResponseCb? = null
+ @JvmField var response_cb_cls: Pointer? = null
+ @JvmField var url: String? = null
+ @JvmField var method: String? = null
+ @JvmField var request_headers: Pointer? = null // Array<String>
+ @JvmField var redirect: Int? = null
+ @JvmField var timeout_ms: Int? = null
+ @JvmField var debug: Int? = null
+ @JvmField var req_body: Pointer? = null // ByteArray
+ @JvmField var req_body_len: Int? = null
+
+ override fun getFieldOrder() = mutableListOf(
+ "response_cb",
+ "response_cb_cls",
+ "url",
+ "method",
+ "request_headers",
+ "redirect",
+ "timeout_ms",
+ "debug",
+ "req_body",
+ "req_body_len",
+ )
+ }
+
+ class JSHttpResponseInfo: Structure() {
+ @JvmField var request_id: Int? = null
+ @JvmField var status: Int? = null
+ @JvmField var errmsg: String? = null
+ @JvmField var response_headers: Pointer? = null
+ @JvmField var num_response_headers: Int? = null
+ @JvmField var body: ByteArray? = null
+ @JvmField var body_len: Int? = null
+
+ override fun getFieldOrder() = mutableListOf(
+ "request_id",
+ "status",
+ "errmsg",
+ "response_headers",
+ "num_response_headers",
+ "body",
+ "body_len",
+ )
+ }
+
+ interface JSHttpResponseCb: Callback {
+ fun invoke(cls: Pointer, resp: JSHttpResponseInfo)
+ }
+
+ interface JSHttpRequestCb: Callback {
+ fun invoke(cls: Pointer, req_info: JSHttpRequestInfo)
+ }
+
+ interface JSHttpReqCreateFn: Callback {
+ fun invoke(cls: Pointer, req_info: JSHttpRequestInfo): Int
+ }
+
+ interface JSHttpReqCancelFn: Callback {
+ fun invoke(cls: Pointer, request_id: Int): Int
+ }
+
+ fun TALER_set_http_client_implementation(twi: Pointer, impl: Pointer)
+
+ fun TALER_pack_http_client_implementation(
+ req_create: JSHttpReqCreateFn,
+ req_cancel: JSHttpReqCancelFn,
+ handler_p: Pointer,
+ ): Pointer
}
private val walletInstance: Pointer = TalerNative.INSTANCE.TALER_WALLET_create()
@@ -73,6 +148,9 @@ class TalerWalletCore {
// Please don't refactor them, or else you'll suffer!
private var currentMessageHandler: TalerNative.TALER_WALLET_MessageHandlerFn? = null
private var currentLogHandler: TalerNative.TALER_LogFn? = null
+ private var currentReqCreateHandler: TalerNative.JSHttpReqCreateFn? = null
+ private var currentReqCancelHandler: TalerNative.JSHttpReqCancelFn? = null
+ private var currentResHandler: TalerNative.JSHttpResponseCb? = null
fun setMessageHandler(handler: (msg: String) -> Unit) {
this.currentMessageHandler = object : TalerNative.TALER_WALLET_MessageHandlerFn {
@@ -111,4 +189,33 @@ class TalerWalletCore {
fun join() {
TalerNative.INSTANCE.TALER_WALLET_join(walletInstance)
}
+
+ /**
+ * Native networking
+ */
+
+ fun setHttpClient(handler: Networking.RequestHandler) {
+ this.currentReqCreateHandler = object: TalerNative.JSHttpReqCreateFn {
+ override fun invoke(cls: Pointer, req_info: TalerNative.JSHttpRequestInfo): Int {
+ Networking.httpCreateRequest(req_info, handler)
+ // TODO: return positive number on success, negative on failure
+ // right now, this can't be done synchronously; doing it
+ // async might require a modification in the C API.
+ return 1
+ }
+ }
+ this.currentReqCancelHandler = object: TalerNative.JSHttpReqCancelFn {
+ override fun invoke(cls: Pointer, request_id: Int): Int {
+ return Networking.httpCancelRequest(request_id, handler)
+ }
+ }
+
+ val client = TalerNative.INSTANCE.TALER_pack_http_client_implementation(
+ this.currentReqCreateHandler!!,
+ this.currentReqCancelHandler!!,
+ this.walletInstance,
+ )
+
+ TalerNative.INSTANCE.TALER_set_http_client_implementation(walletInstance, client)
+ }
}