commit d8f8327e2883c3a8c2212fb22ff0a7e5878825ca Author: JoeAllen <2494136784@qq.com> Date: Fri Jul 4 17:18:37 2025 +0800 feat: 添加蓝牙读卡功能- 新增蓝牙帮助类和读卡相关 Activity - 实现蓝牙设备扫描、连接和读卡功能 - 添加卡信息处理和展示逻辑 - 集成第三方库实现 EMV 进程和数据解析 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..d17e107 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +i9-bl-test \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..97f0a8e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8d66637 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d843f34 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a966338 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'com.serverapk.i9_bl_test' + compileSdk 36 + + defaultConfig { + applicationId "com.serverapk.i9_bl_test" + minSdk 24 + targetSdk 36 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation libs.appcompat + implementation libs.material + implementation libs.activity + implementation libs.constraintlayout + implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: []) + testImplementation libs.junit + androidTestImplementation libs.ext.junit + androidTestImplementation libs.espresso.core + + implementation 'com.google.code.gson:gson:2.8.9' + +} diff --git a/app/libs/j2xx.jar b/app/libs/j2xx.jar new file mode 100644 index 0000000..3207ef0 Binary files /dev/null and b/app/libs/j2xx.jar differ diff --git a/app/libs/pl2303g_driver.jar b/app/libs/pl2303g_driver.jar new file mode 100644 index 0000000..c1945b5 Binary files /dev/null and b/app/libs/pl2303g_driver.jar differ diff --git a/app/libs/pl2303lib_v2.0.4.10.jar b/app/libs/pl2303lib_v2.0.4.10.jar new file mode 100644 index 0000000..2fb3ce4 Binary files /dev/null and b/app/libs/pl2303lib_v2.0.4.10.jar differ diff --git a/app/libs/posDeviceSDKV2.2.4.jar b/app/libs/posDeviceSDKV2.2.4.jar new file mode 100644 index 0000000..56c2abe Binary files /dev/null and b/app/libs/posDeviceSDKV2.2.4.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/serverapk/i9_bl_test/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/serverapk/i9_bl_test/ExampleInstrumentedTest.java new file mode 100644 index 0000000..35e955c --- /dev/null +++ b/app/src/androidTest/java/com/serverapk/i9_bl_test/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.serverapk.i9_bl_test; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.serverapk.i9_bl_test", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..96f9e55 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/serverapk/i9_bl_test/BluetoothHelper.java b/app/src/main/java/com/serverapk/i9_bl_test/BluetoothHelper.java new file mode 100644 index 0000000..f2455ad --- /dev/null +++ b/app/src/main/java/com/serverapk/i9_bl_test/BluetoothHelper.java @@ -0,0 +1,54 @@ +package com.serverapk.i9_bl_test; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.util.Log; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@SuppressLint("MissingPermission") +public class BluetoothHelper { + + private static final String TAG = "BluetoothHelper"; + + public static Set getConnectedBluetoothDevices(Context context, ServiceListener listener) { + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { + Log.w(TAG, "蓝牙未启用"); + return new HashSet<>(); + } + + Set pairedDevices = bluetoothAdapter.getBondedDevices(); + for (BluetoothDevice device : pairedDevices) { + Log.d(TAG, "已配对设备: " + device.getName() + " - " + device.getAddress()); + } + + bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() { + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + List connectedDevices = proxy.getConnectedDevices(); + for (BluetoothDevice device : connectedDevices) { + Log.d(TAG, "已连接: " + device.getName() + " - " + device.getAddress()); + } + listener.onServiceConnected(connectedDevices); + } + + @Override + public void onServiceDisconnected(int profile) { + Log.d(TAG, "HEADSET profile 已断开"); + } + }, BluetoothProfile.HEADSET); + + return pairedDevices; + } + + public interface ServiceListener { + void onServiceConnected(List devices); + } +} diff --git a/app/src/main/java/com/serverapk/i9_bl_test/MainActivity.java b/app/src/main/java/com/serverapk/i9_bl_test/MainActivity.java new file mode 100644 index 0000000..0c322a9 --- /dev/null +++ b/app/src/main/java/com/serverapk/i9_bl_test/MainActivity.java @@ -0,0 +1,143 @@ +package com.serverapk.i9_bl_test; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.company.i9sslclient.bluetooth.DeviceInfo; + +import java.util.ArrayList; +import java.util.List; + +@SuppressLint("MissingPermission") +public class MainActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; + private static final long SCAN_PERIOD = 10000; + Handler handler = new Handler(Looper.getMainLooper()); + private BluetoothAdapter mBluetoothAdapter; + private boolean mScanning; + private List devices; + private List deviceInfoList; + private String mac; + BluetoothAdapter.LeScanCallback leScanCallback = (device, rssi, scanRecord) -> setMac(device); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + findViewById(R.id.test).setOnClickListener(view -> { + if (mac == null || mac.isEmpty() || mac.isBlank()) { + Log.e(TAG, "未找到读卡器"); + return; + } + Intent intent = new Intent(MainActivity.this, ReadCardActivity.class); + intent.putExtra("mac", mac); + startActivity(intent); + }); + init(); + } + + public void setMac(BluetoothDevice device) { + int size = devices.size(); + if (size > 0) { + boolean tmp = false; + + for (int i = 0; i < size; i++) { + if (device.getAddress().equalsIgnoreCase(devices.get(i))) { + tmp = true; + break; + } + } + + if (!tmp) { + if (device.getName() != null) { + Log.d(TAG, "device.getName()==" + device.getName()); + devices.add(device.getAddress()); + deviceInfoList.add(new DeviceInfo(device.getName(), device.getAddress())); + for (DeviceInfo info : deviceInfoList) { + if (info.getName().contains("I9")) { + mac = info.getIdentifier(); + + } + } + } + } + } else { + if (device.getName() != null) { + Log.d(TAG, "device.getName()==" + device.getName()); + devices.add(device.getAddress()); + deviceInfoList.add(new DeviceInfo(device.getName(), device.getAddress())); + + for (DeviceInfo info : deviceInfoList) { + if (info.getName().contains("I9")) { + mac = info.getIdentifier(); + } + } + } + } + } + + + public void scanLeDevice(final boolean enable) { + Log.d(TAG, "扫描 LE 设备 " + enable); + BluetoothHelper.getConnectedBluetoothDevices(this, devices -> { + devices.forEach(this::setMac); + }).forEach(this::setMac); + if (enable) { + handler.postDelayed(new Runnable() { + @Override + public void run() { + if ((mac == null || mac.isEmpty() || mac.isBlank()) && !mScanning) { + handler.post(this); + return; + } + Log.d(TAG, "停止扫描,已扫描到设备 " + mac); + mScanning = false; + mBluetoothAdapter.stopLeScan(leScanCallback); + } + }, SCAN_PERIOD); + + mScanning = true; + mBluetoothAdapter.startLeScan(leScanCallback); + } else { + mScanning = false; + mBluetoothAdapter.stopLeScan(leScanCallback); + } + } + + public boolean isScanning() { + return mScanning; + } + + void init() { + final BluetoothManager bluetoothManager = + (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); + mBluetoothAdapter = bluetoothManager.getAdapter(); + if (mBluetoothAdapter == null) { + Log.e(TAG, "Bluetooth not supported"); + return; + } + devices = new ArrayList<>(); + deviceInfoList = new ArrayList<>(); + scanLeDevice(true); + } +} diff --git a/app/src/main/java/com/serverapk/i9_bl_test/ReadCardActivity.java b/app/src/main/java/com/serverapk/i9_bl_test/ReadCardActivity.java new file mode 100644 index 0000000..fe2794b --- /dev/null +++ b/app/src/main/java/com/serverapk/i9_bl_test/ReadCardActivity.java @@ -0,0 +1,619 @@ +package com.serverapk.i9_bl_test; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; + +import androidx.activity.EdgeToEdge; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.company.i9sslclient.TLVprocess.Field55Test; +import com.company.i9sslclient.TLVprocess.TlvData; +import com.company.i9sslclient.bean.CardInfo; +import com.company.i9sslclient.bean.KernelInfo; +import com.company.i9sslclient.bean.TerminalInfo; +import com.company.i9sslclient.ble.BleController; +import com.company.i9sslclient.lib.TradeTag; +import com.company.i9sslclient.lib.Util; +import com.company.i9sslclient.listener.DeviceConnectionListener; +import com.company.i9sslclient.listener.TerminalInfoListstener; +import com.company.i9sslclient.service.BluetoothLeService; +import com.google.gson.Gson; +import com.serverapk.i9_bl_test.model.CardData; +import com.serverapk.i9_bl_test.model.CardQuery; + +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * + */ +public class ReadCardActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; + public String errMsg, readerErrMsg, mac, tusn, appVersion, kernelVersion, tradeData, money = "1", countryCode = "0702", currencyCode = "0702", cardNum; + Handler handler = new Handler(Looper.getMainLooper()); + private BluetoothAdapter mBluetoothAdapter; + private BluetoothLeService mBluetoothLeService; + private MyTerminalInfoListstener mMyTerminalInfoListstener; + private BleController mBluetoothController; + + private CardData cardData; + private int keyType = 0; + private final Runnable readRunnable = new Runnable() { + @Override + public void run() { + try { + StringBuilder tradeDataStr = new StringBuilder(); + tradeDataStr.append(TradeTag.TAG_1F01).append("01").append("71"); + + StringBuilder key = new StringBuilder(); + key.append(Util.toHexString(keyType + 1)); + String keyIndexStr = "0"; + key.append(Util.toHexString(Integer.parseInt(keyIndexStr))); + + tradeDataStr.append(TradeTag.TAG_1F04).append("06").append("020100000200"); + tradeDataStr.append(TradeTag.TAG_1F02).append("04").append("10000000"); + String date = Util.getCurrentDate() + Util.getCurrentTime(); + tradeDataStr.append(TradeTag.TAG_1F03).append("07").append(date); + tradeDataStr.append(TradeTag.TAG_1F07).append("03").append("313233"); + + int amount = Integer.parseInt(money) * 100; + String amountStr = String.valueOf(amount); + String emvinfo = getString(amountStr, tradeDataStr); + + tradeDataStr.append(TradeTag.TAG_1F09).append(Util.toHexString(emvinfo.length() / 2)).append(emvinfo); + + + Log.d(TAG, "tradeDataStr: " + tradeDataStr); + mBluetoothController.startEmvProcess(50, Util.HexToBin(tradeDataStr.toString())); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @NonNull + private String getString(String amountStr, StringBuilder tradeDataStr) { + String lenStr = Util.toHexString(amountStr.length()); + + tradeDataStr.append(TradeTag.TAG_1F08).append(lenStr).append(Util.BinToHex(amountStr.getBytes())); + String transationType = "00"; + return TradeTag.TAG_9C + "01" + transationType + TradeTag.TAG_9F1A + "02" + countryCode + + TradeTag.TAG_5F2A + "02" + currencyCode + "9a03" + Util.getCurrentDateYY() + "9f2103" + Util.getCurrentTime(); + } + }; + private ThreadPoolExecutor executorService; + private boolean isConnected = false; + private final ServiceConnection mServiceConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName componentName, IBinder service) { + mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService(); + if (!mBluetoothLeService.initialize()) { + Log.e(TAG, "无法初始化蓝牙"); + finish(); + return; + } + + mBluetoothController = BleController.getInstance(ReadCardActivity.this, null, + new DeviceConnectionListener() { + @Override + public void connected() { + isConnected = true; + Log.d(TAG, "connected1=========="); + beginReadCard(); + } + + @Override + public void connectFail() { + isConnected = false; + Log.e(TAG, "connectFail=========="); + } + + @Override + public void disConnected() { + isConnected = false; + Log.e(TAG, "disConnected=========="); + } + }, mMyTerminalInfoListstener, mBluetoothLeService); + + if (mBluetoothController != null) { + Log.d(TAG, "连接设备\n" + mac); + if (mBluetoothController.isConnected()) { + beginReadCard(); + return; + } + mBluetoothController.connectDevice(mac); + } else { + Log.e(TAG, "初始化 mBluetoothController 失败"); + finish(); + } + } + + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mBluetoothLeService = null; + Log.d(TAG, "onServiceDisconnected======="); + } + }; + + public static String complement0(String str, int len, boolean bool) { + if (str.length() >= len) { + return str; + } else { + StringBuilder complementstr = new StringBuilder(str); + while (complementstr.length() < len) { + if (bool) { + complementstr.insert(0, "0"); + } else { + complementstr.append("0"); + } + } + return complementstr.toString(); + } + } + + private void beginReadCard() { + Log.d(TAG, "开始读卡流程"); + currencyCode = complement0(currencyCode, 4, true); + countryCode = complement0(countryCode, 4, true); + if (executorService.getActiveCount() == 0 && executorService.getQueue().isEmpty() && !executorService.isShutdown() && !executorService.isTerminated()) { + executorService.execute(readRunnable); + + } + + + } + + private void handlerCard() { + CardQuery query = new CardQuery(); + query.setCardInfo(cardData); + String json = new Gson().toJson(query); + Log.d(TAG, "卡信息\t" + json); + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_read_card); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + findViewById(R.id.test).setOnClickListener(v -> finish()); + + final BluetoothManager bluetoothManager = + (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); + mBluetoothAdapter = bluetoothManager.getAdapter(); + + if (mBluetoothAdapter == null) { + + Log.e(TAG, "不支持蓝牙"); + return; + } + + executorService = new ThreadPoolExecutor( + 1, + 1, + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>() + ); + mMyTerminalInfoListstener = new MyTerminalInfoListstener(); + mac = getIntent().getStringExtra("mac"); + Log.d(TAG, "mac=" + mac); + Intent gattServiceIntent = new Intent(this, BluetoothLeService.class); + bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE); + + } + + private final class MyTerminalInfoListstener implements TerminalInfoListstener { + @Override + public void onlineResponse(int code, byte[] reScript, byte[] data) { + Log.d(TAG, "在线响应 code: %d" + code); + String sb = "发送在线流程结果 code: " + code + + "reScript: " + Util.BinToHex(reScript) + + "data: " + Util.BinToHex(data); + + Log.d(TAG, "在线响应\n" + sb); + } + + @Override + public void onSendApdu(List lists) { + Log.d(TAG, "发送APDU数据"); + StringBuilder sb = new StringBuilder(); + + if ((lists != null) && (!lists.isEmpty())) { + int size = lists.size(); + for (int i = 0; i < size; i++) { + sb.append("\n").append(i).append(" : ").append(Util.BinToHex(lists.get(i))); + } + } + Log.d(TAG, "在 APDU 响应数据上\n" + sb); + } + + @Override + public void onEncryptedData(byte[] data, String ksn) { + StringBuilder sb = new StringBuilder(); + sb.append("在加密数据上:").append(Util.BinToHex(data)); + if (ksn != null) { + sb.append("ksn:").append(ksn); + } + Log.d(TAG, sb.toString()); + } + + @Override + public void onPowerOnAPDU(int i, byte[] bytes) { + String sb = "开机 APDU" + + "\ncardType: " + i + + "\ndata: " + Util.BinToHex(bytes); + + Log.d(TAG, sb); + } + + @Override + public void onGetBatteryPower(int i) { + Log.d(TAG, "电池电量: " + i); + } + + @Override + public void onGetTerminalDateTime(String terminalTime) { + Log.d(TAG, "终止时间: " + terminalTime); + } + + @Override + public void onGetTerminalStatus(int status) { + Log.d(TAG, "获取终端状态: " + status); + } + + @Override + public void onUserImport(String s) { + Log.d(TAG, "用户导入: " + s); + + } + + @Override + public void onCardInfoImport(String s, String s1) { + String sb = "onCardInfoImport : " + s + + "onCardInfoImport : " + s1; + + Log.d(TAG, "卡信息导入: " + sb); + } + + @Override + public void onGetTerminalInfo(TerminalInfo devInfo) { + StringBuilder sb = new StringBuilder(); + sb.append("在“获取终端信息”上"); + if (18 == devInfo.getTerminalType()) { + sb.append("\nModel Name: " + "i9"); + } else { + sb.append("\nterminalType: ").append(devInfo.getTerminalType()); + } + sb.append("\nTSN: ").append(devInfo.getTerminalNo()); + + if (devInfo.getKernelVer() != null) { + sb.append("\nKernel Version: ").append(devInfo.getKernelVer()); + } + if (devInfo.getSoftVersion() != null) { + sb.append("\nApplicatin Version: ").append(devInfo.getSoftVersion()); + appVersion = devInfo.getSoftVersion(); + } + if (devInfo.getFirmwareVer() != null) { + sb.append("\nFirmware Version: ").append(devInfo.getFirmwareVer()); + } + if (devInfo.getHardwareVer() != null) { + sb.append("\nHardware Version: ").append(devInfo.getHardwareVer()); + } + if (devInfo.getSoftVersionDate() != null) { + sb.append("\nSoftware Date: ").append(devInfo.getSoftVersionDate()); + } + if (devInfo.getTsn() != null) { + sb.append("\nSN: ").append(devInfo.getTsn().toLowerCase()); + } + + if (devInfo.getKernelVersion() != null) { + sb.append("\nkernelVersion: ").append(devInfo.getKernelVersion()); + kernelVersion = devInfo.getKernelVersion(); + } + if (devInfo.getBluetoothName() != null) { + sb.append("\nbluetoothName: ").append(devInfo.getBluetoothName()); + } + if (devInfo.getBluetoothMAC() != null) { + sb.append("\nbluetoothMAC: ").append(devInfo.getBluetoothMAC()); + } + if (devInfo.getBluetoothVersion() != null) { + sb.append("\nbluetoothVersion: ").append(devInfo.getBluetoothVersion()); + } + if (devInfo.getProtocolType() != null) { + sb.append("\nprotocolType: ").append(devInfo.getProtocolType()); + } + if (devInfo.getMerchaantNo() != null) { + sb.append("\nmerchaantNo: ").append(devInfo.getMerchaantNo()); + } + if (devInfo.getTsn() != null) { + byte[] tsn = Util.hexStringToByteArray(devInfo.getTsn()); + boolean isBcd = false; + for (byte b : tsn) { + if ((b <= 32) || (b > 126)) { + isBcd = true; + break; + } + } + if (isBcd) { + sb.append("\nSN: ").append(devInfo.getTsn().toLowerCase()); + tusn = devInfo.getTsn().toLowerCase(); + } else { + sb.append("\nSN: ").append(new String(tsn)); + tusn = new String(tsn); + } + } + + Log.d(TAG, String.valueOf(sb)); + } + + @Override + public void onGetKernelInfo(KernelInfo kernelInfo) { + Log.d(TAG, "获取内核信息:" + kernelInfo); + + } + + @Override + public void onTimeOut() { + Log.e(TAG, "超时================"); + handler.removeCallbacks(readRunnable); + handler.post(readRunnable); + } + + @Override + public void onGetCardNo(String cardNo) { + Log.d(TAG, "获取卡号:" + cardNo); + } + + @Override + public void onCaculateMac(String mac, String ksn) { + Log.d(TAG, "mac = " + mac + ", ksn = " + ksn); + + } + + @Override + public void onGetMerchant(String merchant, String terminalNo) { + Log.d(TAG, "merchant = " + merchant + ", terminalNo = " + terminalNo); + } + + @Override + public void onPINEntryResult(byte[] pin) { + Log.e(TAG, "onPINEntryResult: " + Util.BinToHex(pin)); + } + + + @Override + public void onError(int code, String msg) { + String sb = new SimpleDateFormat("HH:mm:ss:SSS ").format(new java.util.Date()) + "onError" + + "\ncode: " + code + + "\nmsg: " + msg; + readerErrMsg = msg; + + Log.e(TAG, "onError============\n" + sb); + + } + + @Override + public void onWaitingcard() { + Log.d(TAG, "onWaitingcard"); + } + + @Override + public void onWaitingPin() { + Log.e(TAG, "onWaitingPin"); + } + + + @Override + public void onICCardInsertion() { + Log.e(TAG, "onICCardInsertion"); + } + + @Override + public void onNFCCardDetection() { + + Log.d(TAG, "onNFCCardDetection"); + } + + @Override + public void onDecryptData(byte[] bytes) { + Log.e(TAG, "bytes:" + Util.BinToHex(bytes)); + } + + @Override + public void onReadCard(CardInfo cardInfo) { + Log.d(TAG, "读取卡信息: %s" + cardInfo.getCardNo()); + tradeData = cardInfo.getEMVData(); + Log.d(TAG, "onReadCard:" + new Gson().toJson(cardInfo)); + cardNum = cardInfo.getCardNo(); + + cardData = new CardData(); + StringBuilder sb = new StringBuilder(); + sb.append("{"); + if (cardInfo.getControlModel() != null) { + cardData.setControlModel(cardInfo.getControlModel()); + sb.append("\ncontrolModel: ").append(cardInfo.getControlModel()); + } + cardData.setCardType(cardInfo.getCardType() + ""); + sb.append("\ncardType: ").append(cardInfo.getCardType()); + + if (cardInfo.getCardexpiryDate() != null) { + cardData.setCardexpiryDate(cardInfo.getCardexpiryDate()); + sb.append("\ncardexpiryDate: ").append(cardInfo.getCardexpiryDate()); + } + if (cardInfo.getTracks() != null) { + sb.append("\ntracks: ").append(Util.BinToHex(cardInfo.getTracks())); + } + if (cardInfo.getCardNo() != null) { + cardData.setCardNo(cardInfo.getCardNo()); + sb.append("\ncardNo: ").append(cardInfo.getCardNo()); + } + if (cardInfo.getCardName() != null) { + cardData.setCardName(cardInfo.getCardName()); + sb.append("\nCardName: ").append(cardInfo.getCardName()); + } + if (cardInfo.getPAN() != null) { + sb.append("\nPAN: ").append(cardInfo.getPAN()); + } + if (cardInfo.getCardSerial() != null) { + cardData.setCardSerial(cardInfo.getCardSerial()); + sb.append("\nCardSerial: ").append(cardInfo.getCardSerial()); + } + if (cardInfo.getCVM() != null) { + cardData.setCVM(cardInfo.getCVM()); + sb.append("\nCVM: ").append(cardInfo.getCVM()); + } + if (cardInfo.getIcTracks() != null) { + cardData.setIcTracks(Util.BinToHex(cardInfo.getIcTracks())); + sb.append("\nicTracks: ").append(Util.BinToHex(cardInfo.getIcTracks())); + } + if (cardInfo.getTerimalNo() != null) { + sb.append("\nterimalNo: ").append(cardInfo.getTerimalNo()); + } + cardData.setNfcTradeRespose(cardInfo.getNfcTradeRespose() + ""); + sb.append("\nnfcTradeRespose: ").append(cardInfo.getNfcTradeRespose()); + + if (cardInfo.getTracklength() != null) { + cardData.setTracklength(Util.BinToHex(cardInfo.getTracklength())); + sb.append("\ntracklength: ").append(Util.BinToHex(cardInfo.getTracklength())); + } + if (cardInfo.getOriginalTrack() != null) { + cardData.setOriginalTrack(Util.BinToHex(cardInfo.getOriginalTrack())); + sb.append("\noriginalTrack: ").append(Util.BinToHex(cardInfo.getOriginalTrack())); + } + if (cardInfo.getKSN() != null) { + sb.append("\nKSN: ").append(cardInfo.getKSN()); + } + if (cardInfo.getServiceCode() != null) { + cardData.setServiceCode(cardInfo.getServiceCode()); + sb.append("\nServiceCode: ").append(cardInfo.getServiceCode()); + } + + if ((cardInfo.getOriginalTrack() != null) && (cardInfo.getTracklength() != null)) { + int trck1len = (cardInfo.getTracklength()[0] & 0xFF); + int trck2len = ((cardInfo.getTracklength()[1] & 0xFF) + 1) / 2; + int trck3len = ((cardInfo.getTracklength()[2] & 0xFF) + 1) / 2; + if (trck1len > 0) { + + byte[] trck1 = new byte[trck1len]; + System.arraycopy(cardInfo.getOriginalTrack(), 0, trck1, 0, trck1len); + sb.append("\ntrck1:").append(Util.BinToHex(trck1)); + } + + if (trck2len > 0) { + byte[] trck2 = new byte[trck2len]; + System.arraycopy(cardInfo.getOriginalTrack(), trck1len, trck2, 0, trck2len); + sb.append("\ntrck2:").append(Util.BinToHex(trck2).substring(0, ((cardInfo.getTracklength()[1] & 0xFF)))); + } + + if (trck3len > 0) { + byte[] trck3 = new byte[trck3len]; + System.arraycopy(cardInfo.getOriginalTrack(), trck1len + trck2len, trck3, 0, trck3len); + sb.append("\ntrck3:").append(Util.BinToHex(trck3).substring(0, ((cardInfo.getTracklength()[2] & 0xFF)))); + } + } + + + if ("03".equals(cardInfo.getKernel_Type())) { + sb.append("\nKernel Type: ").append(cardInfo.getKernel_Type()); + sb.append("\nOutcome Parameter Set: ").append(cardInfo.getOutcome_Parameter_Set()); + sb.append("\nUser Interface Request_Data: ").append(cardInfo.getUser_Interface_Request_Data()); + sb.append("\nError Indication: ").append(cardInfo.getError_Indication()); + } + + if (cardInfo.getIcTracks() != null) { + String tags = Util.BinToHex(cardInfo.getIcTracks()); + + if (!TextUtils.isEmpty(tags)) { + + List tlvDataList = Field55Test.parseFiled55Data(tags); + for (TlvData tlvData : tlvDataList) { + if ("57".equals(tlvData.getTag())) { + Log.d(TAG, "tag_57 value: " + tlvData.getValue()); + } + } + } + } + Log.d(TAG, "读取卡信息=======" + new Gson().toJson(cardData)); + + int cardType = cardInfo.getCardType(); + if (cardType != -1) { + cardType = cardType & 0x0F; + switch (cardType) { + case 1: + case 13: + mBluetoothController.sendOnlineProcessResult(null); + break; + case 0: + case 2: + default: + break; + } + + } + + handlerCard(); + + } + + @Override + public void onShowLog(String l) { + Log.d(TAG, new SimpleDateFormat("HH:mm:ss:SSS ").format(new java.util.Date()) + "onShowLog log: " + l); + } + + @Override + public void onStopTradeSuccess() { + Log.e(TAG, "onStopTradeSuccess"); + } + + @Override + public void onExecuteSuccess(int code) { + Log.e(TAG, "onExecuteSuccess code: " + code); + if (code == 14) { + Log.e(TAG, "app download success"); + } else if (code == 15) { + Log.e(TAG, "kernel download success"); + } else if (code == 16) { + Log.e(TAG, "key download success"); + } + + } + + @Override + public void onData(int type, int code, byte[]... pin) { + StringBuilder sb = new StringBuilder(); + sb.append("type: ").append(type); + sb.append("\ncode: ").append(code); + if (pin != null) { + int len = pin.length; + for (int i = 0; i < len; i++) { + sb.append("\n").append(i).append(": ").append(Util.BinToHex(pin[i])); + } + } + + Log.d(TAG, sb.toString()); + } + } +} diff --git a/app/src/main/java/com/serverapk/i9_bl_test/model/CardData.java b/app/src/main/java/com/serverapk/i9_bl_test/model/CardData.java new file mode 100644 index 0000000..d6dd5f8 --- /dev/null +++ b/app/src/main/java/com/serverapk/i9_bl_test/model/CardData.java @@ -0,0 +1,149 @@ +package com.serverapk.i9_bl_test.model; + +import java.io.Serializable; + +/** + * 查询卡的使用扇区和密码保存扇区密码的请求实体 + */ + +public class CardData implements Serializable { + private static final long serialVersionUID = 1L; + + private String controlModel; + private String cardType; + private String cardexpiryDate; + private String cardNo; + private String sign; + private String cardName; + private String cardSerial; + private String CVM; + private String icTracks; + private String nfcTradeRespose; + private String tracklength; + private String originalTrack; + private String serviceCode; + private String trck2; + + private String cvv; + + public String getControlModel() { + return controlModel; + } + + public void setControlModel(String controlModel) { + this.controlModel = controlModel; + } + + + public String getCardexpiryDate() { + return cardexpiryDate; + } + + public void setCardexpiryDate(String cardexpiryDate) { + this.cardexpiryDate = cardexpiryDate; + } + + public String getCardNo() { + return cardNo; + } + + public void setCardNo(String cardNo) { + this.cardNo = cardNo; + } + + public String getSign() { + return sign; + } + + public void setSign(String sign) { + this.sign = sign; + } + + public String getCardName() { + return cardName; + } + + public void setCardName(String cardName) { + this.cardName = cardName; + } + + public String getCardSerial() { + return cardSerial; + } + + public void setCardSerial(String cardSerial) { + this.cardSerial = cardSerial; + } + + public String getCVM() { + return CVM; + } + + public void setCVM(String CVM) { + this.CVM = CVM; + } + + public String getIcTracks() { + return icTracks; + } + + public void setIcTracks(String icTracks) { + this.icTracks = icTracks; + } + + public String getCardType() { + return cardType; + } + + public void setCardType(String cardType) { + this.cardType = cardType; + } + + public String getNfcTradeRespose() { + return nfcTradeRespose; + } + + public void setNfcTradeRespose(String nfcTradeRespose) { + this.nfcTradeRespose = nfcTradeRespose; + } + + public String getTracklength() { + return tracklength; + } + + public void setTracklength(String tracklength) { + this.tracklength = tracklength; + } + + public String getOriginalTrack() { + return originalTrack; + } + + public void setOriginalTrack(String originalTrack) { + this.originalTrack = originalTrack; + } + + public String getServiceCode() { + return serviceCode; + } + + public void setServiceCode(String serviceCode) { + this.serviceCode = serviceCode; + } + + public String getTrck2() { + return trck2; + } + + public void setTrck2(String trck2) { + this.trck2 = trck2; + } + + public String getCvv() { + return cvv; + } + + public void setCvv(String cvv) { + this.cvv = cvv; + } +} diff --git a/app/src/main/java/com/serverapk/i9_bl_test/model/CardQuery.java b/app/src/main/java/com/serverapk/i9_bl_test/model/CardQuery.java new file mode 100644 index 0000000..d568238 --- /dev/null +++ b/app/src/main/java/com/serverapk/i9_bl_test/model/CardQuery.java @@ -0,0 +1,16 @@ +package com.serverapk.i9_bl_test.model; + +import java.io.Serializable; + +public class CardQuery implements Serializable { + private static final long serialVersionUID = 111L; + private CardData cardInfo; + + public CardData getCardInfo() { + return cardInfo; + } + + public void setCardInfo(CardData cardInfo) { + this.cardInfo = cardInfo; + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..7706ab9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..51fee18 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_read_card.xml b/app/src/main/res/layout/activity_read_card.xml new file mode 100644 index 0000000..10c35ec --- /dev/null +++ b/app/src/main/res/layout/activity_read_card.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..5323577 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..768b058 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e0b84b4 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + i9-bl-test + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..587b95c --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +