ソースを参照

refactor: [1/n] Introduce the lpac project and lpac_jni

Peter Cai 2 年 前
コミット
85af3bcfc0

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "libs/lpac-jni/src/main/jni/lpac"]
+	path = libs/lpac-jni/src/main/jni/lpac
+	url = https://github.com/estkme/lpac

+ 4 - 3
.idea/compiler.xml

@@ -4,9 +4,10 @@
     <bytecodeTargetLevel target="1.7">
       <module name="OpenEUICC.app" target="17" />
       <module name="OpenEUICC.libs.hidden-apis-shim" target="17" />
-      <module name="OpenEUICC.libs.lpad-sm-dp-plus-connector" target="1.8" />
-      <module name="OpenEUICC.libs.lpad-sm-dp-plus-connector.main" target="1.8" />
-      <module name="OpenEUICC.libs.lpad-sm-dp-plus-connector.test" target="1.8" />
+      <module name="OpenEUICC.libs.lpac-jni" target="17" />
+      <module name="OpenEUICC.libs.lpad-sm-dp-plus-connector" target="17" />
+      <module name="OpenEUICC.libs.lpad-sm-dp-plus-connector.main" target="17" />
+      <module name="OpenEUICC.libs.lpad-sm-dp-plus-connector.test" target="17" />
     </bytecodeTargetLevel>
   </component>
 </project>

+ 1 - 0
.idea/gradle.xml

@@ -16,6 +16,7 @@
             <option value="$PROJECT_DIR$/libs" />
             <option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
             <option value="$PROJECT_DIR$/libs/hidden-apis-stub" />
+            <option value="$PROJECT_DIR$/libs/lpac-jni" />
             <option value="$PROJECT_DIR$/libs/lpad-sm-dp-plus-connector" />
           </set>
         </option>

+ 1 - 1
.idea/kotlinc.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="KotlinJpsPluginSettings">
-    <option name="version" value="1.6.21" />
+    <option name="version" value="1.9.20" />
   </component>
 </project>

+ 7 - 6
app/build.gradle

@@ -37,7 +37,7 @@ def keystoreProperties = new Properties()
 keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
 
 android {
-    compileSdk 31
+    compileSdk 34
 
     defaultConfig {
         applicationId "im.angry.openeuicc"
@@ -81,11 +81,12 @@ dependencies {
     compileOnly project(':libs:hidden-apis-stub')
     implementation project(':libs:hidden-apis-shim')
     implementation project(":libs:lpad-sm-dp-plus-connector")
-    implementation 'androidx.core:core-ktx:1.7.0'
-    implementation 'androidx.appcompat:appcompat:1.4.1'
-    implementation 'com.google.android.material:material:1.6.0'
-    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
-    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
+    implementation project(":libs:lpac-jni")
+    implementation 'androidx.core:core-ktx:1.12.0'
+    implementation 'androidx.appcompat:appcompat:1.6.1'
+    implementation 'com.google.android.material:material:1.10.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
     implementation "androidx.cardview:cardview:1.0.0"
     implementation 'com.journeyapps:zxing-android-embedded:4.3.0'

+ 2 - 2
build.gradle

@@ -2,8 +2,8 @@
 plugins {
     id 'com.android.application' version '8.1.2' apply false
     id 'com.android.library' version '8.1.2' apply false
-    id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
-    id 'org.jetbrains.kotlin.multiplatform' version '1.6.21' apply false
+    id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
+    id 'org.jetbrains.kotlin.multiplatform' version '1.9.20' apply false
 }
 
 task clean(type: Delete) {

+ 46 - 0
libs/lpac-jni/build.gradle

@@ -0,0 +1,46 @@
+plugins {
+    id 'com.android.library'
+    id 'org.jetbrains.kotlin.android'
+}
+
+android {
+    namespace 'net.typeblog.lpac_jni'
+    compileSdk 33
+
+    defaultConfig {
+        minSdk 27
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    externalNativeBuild {
+        ndkBuild {
+            path "src/main/jni/Android.mk"
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+}
+
+dependencies {
+
+    implementation 'androidx.core:core-ktx:1.12.0'
+    implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
+    implementation 'androidx.appcompat:appcompat:1.6.1'
+    implementation 'com.google.android.material:material:1.10.0'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+}

+ 0 - 0
libs/lpac-jni/consumer-rules.pro


+ 21 - 0
libs/lpac-jni/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 24 - 0
libs/lpac-jni/src/androidTest/java/net/typeblog/lpac_jni/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package net.typeblog.lpac_jni
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("net.typeblog.lpac_jni.test", appContext.packageName)
+    }
+}

+ 4 - 0
libs/lpac-jni/src/main/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+</manifest>

+ 12 - 0
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt

@@ -0,0 +1,12 @@
+package net.typeblog.lpac_jni
+
+/*
+ * Should reflect euicc_apdu_interface in lpac/euicc/interface.h
+ */
+sealed interface ApduInterface {
+    fun connect()
+    fun disconnect()
+    fun logicalChannelOpen(aid: ByteArray): Int
+    fun logicalChannelClose(handle: Int)
+    fun transmit(tx: ByteArray): ByteArray
+}

+ 28 - 0
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt

@@ -0,0 +1,28 @@
+package net.typeblog.lpac_jni
+
+/*
+ * Should reflect euicc_http_interface in lpac/euicc/interface.h
+ */
+sealed interface HttpInterface {
+    data class HttpResponse(val rcode: Int, val data: ByteArray) {
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (javaClass != other?.javaClass) return false
+
+            other as HttpResponse
+
+            if (rcode != other.rcode) return false
+            if (!data.contentEquals(other.data)) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = rcode
+            result = 31 * result + data.contentHashCode()
+            return result
+        }
+    }
+
+    fun transmit(url: String, tx: ByteArray): HttpResponse
+}

+ 11 - 0
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt

@@ -0,0 +1,11 @@
+package net.typeblog.lpac_jni
+
+private class LpacJni {
+    init {
+        System.loadLibrary("lpac-jni")
+    }
+
+    external fun createContext(apduInterface: ApduInterface, httpInterface: HttpInterface): Long
+    external fun destroyContext(handle: Long)
+    external fun setCurrentContext(handle: Long)
+}

+ 46 - 0
libs/lpac-jni/src/main/jni/Android.mk

@@ -0,0 +1,46 @@
+LOCAL_PATH := $(call my-dir)
+
+# function to find all *.c files under a directory
+define all-c-files-under
+$(patsubst ./%,%, \
+  $(shell cd $(LOCAL_PATH) ; \
+          find $(1) -name "*.c" -and -not -name ".*" -maxdepth 1) \
+ )
+endef
+
+include $(CLEAR_VARS)
+# libcjson
+LOCAL_MODULE := lpac-cjson
+LOCAL_SRC_FILES := \
+	$(call all-c-files-under, lpac/cjson)
+include $(BUILD_STATIC_LIBRARY)
+
+include $(CLEAR_VARS)
+# libasn1c, the ASN parser component from lpac
+LOCAL_MODULE := lpac-asn1c
+LOCAL_C_INCLUDES := \
+	$(LOCAL_PATH)/lpac/euicc/asn1c
+LOCAL_SRC_FILES := \
+	$(call all-c-files-under, lpac/euicc/asn1c/asn1)
+LOCAL_CFLAGS := -DHAVE_CONFIG_H
+include $(BUILD_STATIC_LIBRARY)
+
+include $(CLEAR_VARS)
+# libeuicc component from lpac, which contains the actual implementation
+LOCAL_MODULE := lpac-euicc
+LOCAL_STATIC_LIBRARIES := lpac-asn1c lpac-cjson
+LOCAL_C_INCLUDES := \
+	$(LOCAL_PATH)/lpac
+LOCAL_SRC_FILES := \
+	$(call all-c-files-under, lpac/euicc)
+include $(BUILD_STATIC_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := lpac-jni
+LOCAL_STATIC_LIBRARIES := lpac-euicc
+LOCAL_C_INCLUDES := \
+	$(LOCAL_PATH)/lpac
+LOCAL_SRC_FILES := \
+	lpac-jni/lpac-jni.c \
+	lpac-jni/interface-wrapper.c
+include $(BUILD_SHARED_LIBRARY)

+ 1 - 0
libs/lpac-jni/src/main/jni/Application.mk

@@ -0,0 +1 @@
+APP_ABI := all

+ 1 - 0
libs/lpac-jni/src/main/jni/lpac

@@ -0,0 +1 @@
+Subproject commit 2eaefa6f8d79f68eff6a8c03932b861425767330

+ 117 - 0
libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c

@@ -0,0 +1,117 @@
+#include <string.h>
+#include <malloc.h>
+#include "interface-wrapper.h"
+#include "lpac-jni.h"
+
+jmethodID method_apdu_connect;
+jmethodID method_apdu_disconnect;
+jmethodID method_apdu_logical_channel_open;
+jmethodID method_apdu_logical_channel_close;
+jmethodID method_apdu_transmit;
+
+jmethodID method_http_transmit;
+
+jfieldID field_resp_rcode;
+jfieldID field_resp_data;
+
+void interface_wrapper_init() {
+    LPAC_JNI_SETUP_ENV;
+    jclass apdu_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/ApduInterface");
+    method_apdu_connect = (*env)->GetMethodID(env, apdu_class, "connect", "()V");
+    method_apdu_disconnect = (*env)->GetMethodID(env, apdu_class, "disconnect", "()V");
+    method_apdu_logical_channel_open = (*env)->GetMethodID(env, apdu_class, "logicalChannelOpen", "([B)I");
+    method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose", "(I)V");
+    method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "([B)[B");
+
+    jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface");
+    method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit",
+                                               "(Ljava/lang/String;[B)Lnet/typeblog/lpac_jni/HttpInterface$HttpResponse;");
+
+    jclass resp_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface$HttpResponse");
+    field_resp_rcode = (*env)->GetFieldID(env, resp_class, "rcode", "I");
+    field_resp_data = (*env)->GetFieldID(env, resp_class, "data", "[B");
+}
+
+static int apdu_interface_connect(void) {
+    LPAC_JNI_BEGIN;
+    LPAC_JNI_ASSERT_CTX;
+    LPAC_JNI_SETUP_ENV;
+    (*env)->CallVoidMethod(env, jni_ctx->apdu_interface, method_apdu_connect);
+    LPAC_JNI_END(!((*env)->ExceptionCheck(env) == JNI_FALSE));
+}
+
+static void apdu_interface_disconnect(void) {
+    LPAC_JNI_BEGIN;
+    LPAC_JNI_ASSERT_CTX;
+    LPAC_JNI_SETUP_ENV;
+    (*env)->CallVoidMethod(env, jni_ctx->apdu_interface, method_apdu_disconnect);
+    LPAC_JNI_END0;
+}
+
+static int apdu_interface_logical_channel_open(const uint8_t *aid, uint8_t aid_len) {
+    LPAC_JNI_BEGIN;
+    LPAC_JNI_ASSERT_CTX;
+    LPAC_JNI_SETUP_ENV;
+    jbyteArray jbarr = (*env)->NewByteArray(env, aid_len);
+    (*env)->SetByteArrayRegion(env, jbarr, 0, aid_len, (const jbyte *) aid);
+    jint ret = (*env)->CallIntMethod(env, jni_ctx->apdu_interface, method_apdu_logical_channel_open, jbarr);
+    if ((*env)->ExceptionCheck(env) == JNI_TRUE) {
+        LPAC_JNI_END(-1);
+    } else {
+        LPAC_JNI_END(ret);
+    }
+}
+
+static void apdu_interface_logical_channel_close(uint8_t channel) {
+    LPAC_JNI_BEGIN;
+    LPAC_JNI_ASSERT_CTX;
+    LPAC_JNI_SETUP_ENV;
+    (*env)->CallVoidMethod(env, jni_ctx->apdu_interface, method_apdu_logical_channel_close, channel);
+    LPAC_JNI_END0;
+}
+
+static int apdu_interface_transmit(uint8_t **rx, uint32_t *rx_len, const uint8_t *tx, uint32_t tx_len) {
+    LPAC_JNI_BEGIN;
+    LPAC_JNI_ASSERT_CTX;
+    LPAC_JNI_SETUP_ENV;
+    jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
+    (*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
+    jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(env, jni_ctx->apdu_interface, method_apdu_transmit, txArr);
+    if ((*env)->ExceptionCheck(env) == JNI_TRUE) {
+        LPAC_JNI_END(-1);
+    }
+    *rx_len = (*env)->GetArrayLength(env, ret);
+    *rx = malloc(*rx_len * sizeof(uint8_t));
+    (*env)->GetByteArrayRegion(env, ret, 0, *rx_len, *rx);
+    LPAC_JNI_END(0);
+}
+
+static int http_interface_transmit(const char *url, uint32_t *rcode, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx, uint32_t tx_len) {
+    LPAC_JNI_BEGIN;
+    LPAC_JNI_ASSERT_CTX;
+    LPAC_JNI_SETUP_ENV;
+    jstring jurl = (*env)->NewString(env, url, strlen(url));
+    jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
+    (*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
+    jobject ret = (*env)->CallObjectMethod(env, jni_ctx->http_interface, method_http_transmit, jurl, txArr);
+    if ((*env)->ExceptionCheck(env) == JNI_TRUE) {
+        LPAC_JNI_END(-1);
+    }
+    *rcode = (*env)->GetIntField(env, ret, field_resp_rcode);
+    jbyteArray rxArr = (jbyteArray) (*env)->GetObjectField(env, ret, field_resp_data);
+    *rx_len = (*env)->GetArrayLength(env, rxArr);
+    *rx = malloc(*rx_len * sizeof(uint8_t));
+    (*env)->GetByteArrayRegion(env, rxArr, 0, *rx_len, *rx);
+    LPAC_JNI_END(0);
+}
+
+struct euicc_apdu_interface apdu_interface_wrapper = {
+        .connect = apdu_interface_connect,
+        .disconnect = apdu_interface_disconnect,
+        .logic_channel_open = apdu_interface_logical_channel_open,
+        .logic_channel_close = apdu_interface_logical_channel_close,
+        .transmit = apdu_interface_transmit
+};
+struct euicc_http_interface http_interface_wrapper = {
+        .transmit = http_interface_transmit
+};

+ 14 - 0
libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.h

@@ -0,0 +1,14 @@
+#pragma once
+#undef NDEBUG
+#include <assert.h>
+#include <euicc/interface.h>
+
+extern struct euicc_apdu_interface apdu_interface_wrapper;
+extern struct euicc_http_interface http_interface_wrapper;
+
+void interface_wrapper_init();
+
+#define LPAC_JNI_ASSERT_CTX assert(jni_ctx != NULL)
+#define LPAC_JNI_SETUP_ENV \
+    JNIEnv *env; \
+    (*jvm)->AttachCurrentThread(jvm, &env, NULL)

+ 49 - 0
libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c

@@ -0,0 +1,49 @@
+#include <euicc/interface.h>
+#include <malloc.h>
+#include <string.h>
+#include "lpac-jni.h"
+#include "interface-wrapper.h"
+
+pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER;
+struct lpac_jni_ctx *jni_ctx = NULL;
+JavaVM  *jvm = NULL;
+
+jint JNI_OnLoad(JavaVM *vm, void *reserved) {
+    jvm = vm;
+    interface_wrapper_init();
+    return 1;
+}
+
+JNIEXPORT jlong JNICALL
+Java_net_typeblog_lpac_1jni_LpacJni_createContext(JNIEnv *env, jobject thiz,
+                                                  jobject apdu_interface,
+                                                  jobject http_interface) {
+    LPAC_JNI_BEGIN;
+    struct lpac_jni_ctx *_ctx = malloc(sizeof(struct lpac_jni_ctx));
+    memset(_ctx, 0, sizeof(struct lpac_jni_ctx));
+    _ctx->ctx.interface.apdu = &apdu_interface_wrapper;
+    _ctx->ctx.interface.http = &http_interface_wrapper;
+    _ctx->apdu_interface = (*env)->NewGlobalRef(env, apdu_interface);
+    _ctx->http_interface = (*env)->NewGlobalRef(env, http_interface);
+    LPAC_JNI_END((jlong) _ctx);
+}
+
+JNIEXPORT void JNICALL
+Java_net_typeblog_lpac_1jni_LpacJni_destroyContext(JNIEnv *env, jobject thiz, jlong handle) {
+    LPAC_JNI_BEGIN;
+    struct lpac_jni_ctx *_ctx = (struct lpac_jni_ctx *) handle;
+    (*env)->DeleteGlobalRef(env, _ctx->apdu_interface);
+    (*env)->DeleteGlobalRef(env, _ctx->http_interface);
+    if (jni_ctx == _ctx) {
+        jni_ctx = NULL;
+    }
+    free(_ctx);
+    LPAC_JNI_END0;
+}
+
+JNIEXPORT void JNICALL
+Java_net_typeblog_lpac_1jni_LpacJni_setCurrentContext(JNIEnv *env, jobject thiz, jlong handle) {
+    LPAC_JNI_BEGIN;
+    jni_ctx = (struct lpac_jni_ctx *) handle;
+    LPAC_JNI_END0;
+}

+ 18 - 0
libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h

@@ -0,0 +1,18 @@
+#pragma once
+#include <euicc/euicc.h>
+#include <pthread.h>
+#include <jni.h>
+
+struct lpac_jni_ctx {
+    struct euicc_ctx ctx;
+    jobject apdu_interface;
+    jobject http_interface;
+};
+
+extern JavaVM *jvm;
+extern pthread_mutex_t global_lock;
+extern struct lpac_jni_ctx *jni_ctx;
+
+#define LPAC_JNI_BEGIN pthread_mutex_lock(&global_lock)
+#define LPAC_JNI_END0 pthread_mutex_unlock(&global_lock)
+#define LPAC_JNI_END(ret) LPAC_JNI_END0; return ret

+ 17 - 0
libs/lpac-jni/src/test/java/net/typeblog/lpac_jni/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package net.typeblog.lpac_jni
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}

+ 1 - 6
libs/lpad-sm-dp-plus-connector/build.gradle

@@ -26,9 +26,4 @@ task genAsn1(type: JavaExec) {
 compileJava.dependsOn genAsn1
 compileKotlin.dependsOn genAsn1
 
-description = 'LPAd SM-DP+ Connector'
-
-java {
-    sourceCompatibility = JavaVersion.VERSION_1_8
-    targetCompatibility = JavaVersion.VERSION_1_8
-}
+description = 'LPAd SM-DP+ Connector'

+ 1 - 0
settings.gradle

@@ -16,3 +16,4 @@ rootProject.name = "OpenEUICC"
 include ':app', ':libs:lpad-sm-dp-plus-connector'
 include ':libs:hidden-apis-stub'
 include ':libs:hidden-apis-shim'
+include ':libs:lpac-jni'