From 124da43a1e1439f76f6caf70f90f3c91b7a918e4 Mon Sep 17 00:00:00 2001 From: riz081 Date: Tue, 17 Jun 2025 22:49:41 +0700 Subject: [PATCH] Safepoint Card Reading --- app/build.gradle | 7 +- app/src/main/AndroidManifest.xml | 4 +- .../java/com/example/bdkipoc/CacheHelper.java | 27 ++ .../java/com/example/bdkipoc/Constant.java | 17 + .../com/example/bdkipoc/MainActivity.java | 4 +- .../com/example/bdkipoc/MyApplication.java | 196 ++++++++++ .../java/com/example/bdkipoc/emv/EmvTTS.java | 145 ++++++++ .../bdkipoc/emv/ITTSProgressListener.java | 57 +++ .../bdkipoc/kredit/CreditCardActivity.java | 342 +++++++++++++++++- .../com/example/bdkipoc/utils/ByteUtil.java | 265 ++++++++++++++ .../com/example/bdkipoc/utils/LogUtil.java | 89 +++++ .../com/example/bdkipoc/utils/Utility.java | 107 ++++++ .../wrapper/CheckCardCallbackV2Wrapper.java | 44 +++ .../main/res/layout/activity_credit_card.xml | 124 +------ app/src/main/res/values/colors.xml | 22 +- app/src/main/res/values/dimens.xml | 8 + app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/styles.xml | 5 + 18 files changed, 1351 insertions(+), 121 deletions(-) create mode 100644 app/src/main/java/com/example/bdkipoc/CacheHelper.java create mode 100644 app/src/main/java/com/example/bdkipoc/Constant.java create mode 100644 app/src/main/java/com/example/bdkipoc/MyApplication.java create mode 100644 app/src/main/java/com/example/bdkipoc/emv/EmvTTS.java create mode 100644 app/src/main/java/com/example/bdkipoc/emv/ITTSProgressListener.java create mode 100644 app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java create mode 100644 app/src/main/java/com/example/bdkipoc/utils/LogUtil.java create mode 100644 app/src/main/java/com/example/bdkipoc/utils/Utility.java create mode 100644 app/src/main/java/com/example/bdkipoc/wrapper/CheckCardCallbackV2Wrapper.java create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/build.gradle b/app/build.gradle index 5f05a74..34c8584 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,19 +45,14 @@ android { } dependencies { + implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') implementation libs.appcompat implementation libs.material implementation libs.activity implementation libs.constraintlayout implementation libs.cardview implementation 'androidx.recyclerview:recyclerview:1.3.0' - - // Existing PayLib - // implementation(name: 'PayLib-release-2.0.17', ext: 'aar') - - // Tambahkan dependencies yang kompatibel dari referensi implementation 'com.sunmi:printerlibrary:1.0.15' - implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' // Test dependencies testImplementation libs.junit diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 45e3c91..e788356 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,7 @@ tools:ignore="QueryAllPackagesPermission" /> diff --git a/app/src/main/java/com/example/bdkipoc/CacheHelper.java b/app/src/main/java/com/example/bdkipoc/CacheHelper.java new file mode 100644 index 0000000..07073ce --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/CacheHelper.java @@ -0,0 +1,27 @@ +package com.example.bdkipoc; + +import android.content.Context; +import android.content.SharedPreferences; + +public class CacheHelper { + + private static final String PREFERENCE_FILE_NAME = "sm_pay_demo_obj"; + + private static final String KEY_LANGUAGE = "key_language"; + + public static void saveCurrentLanguage(int language) { + SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE); + int value = sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO); + if (value == language) return; + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(KEY_LANGUAGE, language); + editor.apply(); + } + + public static int getCurrentLanguage() { + SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE); + return sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO); + } + + +} diff --git a/app/src/main/java/com/example/bdkipoc/Constant.java b/app/src/main/java/com/example/bdkipoc/Constant.java new file mode 100644 index 0000000..9d11786 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/Constant.java @@ -0,0 +1,17 @@ +package com.example.bdkipoc; + +public class Constant { + + public static final String TAG = "SDKTestDemo"; + + public static final int LANGUAGE_AUTO = 0; + public static final int LANGUAGE_ZH_CN = 1; + public static final int LANGUAGE_EN_US = 2; + public static final int LANGUAGE_JA_JP = 3; + + public static final int SCAN_MODEL_NONE = 100; + public static final int SCAN_MODEL_P2Lite = 101; + + public static final String SCAN_MODEL_NONE_VALUE = "NONE"; + public static final String SCAN_MODEL_P2Lite_VALUE = "P2Lite"; +} diff --git a/app/src/main/java/com/example/bdkipoc/MainActivity.java b/app/src/main/java/com/example/bdkipoc/MainActivity.java index 1a3db64..b5f704e 100644 --- a/app/src/main/java/com/example/bdkipoc/MainActivity.java +++ b/app/src/main/java/com/example/bdkipoc/MainActivity.java @@ -19,6 +19,8 @@ import androidx.core.view.WindowInsetsCompat; import com.google.android.material.button.MaterialButton; +import com.example.bdkipoc.kredit.CreditCardActivity; + public class MainActivity extends AppCompatActivity { private boolean isExpanded = false; // False = showing only 9 main menus, True = showing all 15 menus @@ -158,7 +160,7 @@ public class MainActivity extends AppCompatActivity { if (cardView != null) { cardView.setOnClickListener(v -> { if (cardId == R.id.card_kartu_kredit) { - startActivity(new Intent(MainActivity.this, PaymentActivity.class)); + startActivity(new Intent(MainActivity.this, CreditCardActivity.class)); } else if (cardId == R.id.card_kartu_debit) { startActivity(new Intent(MainActivity.this, PaymentActivity.class)); } else if (cardId == R.id.card_qris) { diff --git a/app/src/main/java/com/example/bdkipoc/MyApplication.java b/app/src/main/java/com/example/bdkipoc/MyApplication.java new file mode 100644 index 0000000..9a0a48e --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/MyApplication.java @@ -0,0 +1,196 @@ +package com.example.bdkipoc; + +import android.app.Application; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.IBinder; +import android.util.DisplayMetrics; + +import com.example.bdkipoc.emv.EmvTTS; +import com.example.bdkipoc.utils.LogUtil; +import com.example.bdkipoc.utils.Utility; +import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2; +import com.sunmi.pay.hardware.aidlv2.etc.ETCOptV2; +import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2; +import com.sunmi.pay.hardware.aidlv2.print.PrinterOptV2; +import com.sunmi.pay.hardware.aidlv2.readcard.ReadCardOptV2; +import com.sunmi.pay.hardware.aidlv2.rfid.RFIDOptV2; +import com.sunmi.pay.hardware.aidlv2.security.BiometricManagerV2; +import com.sunmi.pay.hardware.aidlv2.security.DevCertManagerV2; +import com.sunmi.pay.hardware.aidlv2.security.NoLostKeyManagerV2; +import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2; +import com.sunmi.pay.hardware.aidlv2.system.BasicOptV2; +import com.sunmi.pay.hardware.aidlv2.tax.TaxOptV2; +import com.sunmi.pay.hardware.aidlv2.test.TestOptV2; +import com.sunmi.pay.hardware.wrapper.HCEManagerV2Wrapper; +import com.sunmi.peripheral.printer.InnerPrinterCallback; +import com.sunmi.peripheral.printer.InnerPrinterException; +import com.sunmi.peripheral.printer.InnerPrinterManager; +import com.sunmi.peripheral.printer.SunmiPrinterService; + +import java.util.Locale; + +import sunmi.paylib.SunmiPayKernel; + +public class MyApplication extends Application { + public static MyApplication app; + + public BasicOptV2 basicOptV2; // 获取基础操作模块 + public ReadCardOptV2 readCardOptV2; // 获取读卡模块 + public PinPadOptV2 pinPadOptV2; // 获取PinPad操作模块 + public SecurityOptV2 securityOptV2; // 获取安全操作模块 + public EMVOptV2 emvOptV2; // 获取EMV操作模块 + public TaxOptV2 taxOptV2; // 获取税控操作模块 + public ETCOptV2 etcOptV2; // 获取ETC操作模块 + public PrinterOptV2 printerOptV2; // 获取打印操作模块 + public TestOptV2 testOptV2; // 获取测试操作模块 + public DevCertManagerV2 devCertManagerV2; // 设备证书操作模块 + public NoLostKeyManagerV2 noLostKeyManagerV2; // NoLostKey操作模块 + public HCEManagerV2Wrapper hceV2Wrapper; // HCE操作模块 + public RFIDOptV2 rfidOptV2; // RFID操作模块 + public SunmiPrinterService sunmiPrinterService; // 打印模块 + //public IScanInterface scanInterface; // 扫码模块 (commented out) + public BiometricManagerV2 mBiometricManagerV2; // 生物特征模块 + + private boolean connectPaySDK;//是否已连接PaySDK + + @Override + public void onCreate() { + super.onCreate(); + app = this; + initLocaleLanguage(); + initEmvTTS(); + bindPrintService(); + bindPaySDKService(); + //bindScannerService(); // Commented out scanner service binding + } + + public static void initLocaleLanguage() { + Resources resources = app.getResources(); + DisplayMetrics dm = resources.getDisplayMetrics(); + Configuration config = resources.getConfiguration(); + int showLanguage = CacheHelper.getCurrentLanguage(); + if (showLanguage == Constant.LANGUAGE_AUTO) { + LogUtil.e(Constant.TAG, config.locale.getCountry() + "---这是系统语言"); + config.locale = Resources.getSystem().getConfiguration().locale; + } else if (showLanguage == Constant.LANGUAGE_ZH_CN) { + LogUtil.e(Constant.TAG, "这是中文"); + config.locale = Locale.SIMPLIFIED_CHINESE; + } else if (showLanguage == Constant.LANGUAGE_EN_US) { + LogUtil.e(Constant.TAG, "这是英文"); + config.locale = Locale.ENGLISH; + } else if (showLanguage == Constant.LANGUAGE_JA_JP) { + LogUtil.e(Constant.TAG, "这是日文"); + config.locale = Locale.JAPAN; + } + resources.updateConfiguration(config, dm); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + LogUtil.e(Constant.TAG, "onConfigurationChanged"); + } + + public boolean isConnectPaySDK() { + return connectPaySDK; + } + + /** + * bind PaySDK service + */ + public void bindPaySDKService() { + final SunmiPayKernel payKernel = SunmiPayKernel.getInstance(); + payKernel.initPaySDK(this, new SunmiPayKernel.ConnectCallback() { + @Override + public void onConnectPaySDK() { + LogUtil.e(Constant.TAG, "onConnectPaySDK..."); + emvOptV2 = payKernel.mEMVOptV2; + basicOptV2 = payKernel.mBasicOptV2; + pinPadOptV2 = payKernel.mPinPadOptV2; + readCardOptV2 = payKernel.mReadCardOptV2; + securityOptV2 = payKernel.mSecurityOptV2; + taxOptV2 = payKernel.mTaxOptV2; + etcOptV2 = payKernel.mETCOptV2; + printerOptV2 = payKernel.mPrinterOptV2; + testOptV2 = payKernel.mTestOptV2; + devCertManagerV2 = payKernel.mDevCertManagerV2; + noLostKeyManagerV2 = payKernel.mNoLostKeyManagerV2; + mBiometricManagerV2 = payKernel.mBiometricManagerV2; + hceV2Wrapper = payKernel.mHCEManagerV2Wrapper; + rfidOptV2 = payKernel.mRFIDOptV2; + connectPaySDK = true; + } + + @Override + public void onDisconnectPaySDK() { + LogUtil.e(Constant.TAG, "onDisconnectPaySDK..."); + connectPaySDK = false; + emvOptV2 = null; + basicOptV2 = null; + pinPadOptV2 = null; + readCardOptV2 = null; + securityOptV2 = null; + taxOptV2 = null; + etcOptV2 = null; + printerOptV2 = null; + devCertManagerV2 = null; + noLostKeyManagerV2 = null; + mBiometricManagerV2 = null; + rfidOptV2 = null; + Utility.showToast(R.string.connect_fail); + } + }); + } + + /** + * bind printer service + */ + private void bindPrintService() { + try { + InnerPrinterManager.getInstance().bindService(this, new InnerPrinterCallback() { + @Override + protected void onConnected(SunmiPrinterService service) { + sunmiPrinterService = service; + } + + @Override + protected void onDisconnected() { + sunmiPrinterService = null; + } + }); + } catch (InnerPrinterException e) { + e.printStackTrace(); + } + } + + /** + * bind scanner service (commented out) + */ + /* + public void bindScannerService() { + Intent intent = new Intent(); + intent.setPackage("com.sunmi.scanner"); + intent.setAction("com.sunmi.scanner.IScanInterface"); + bindService(intent, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + scanInterface = IScanInterface.Stub.asInterface(service); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + scanInterface = null; + } + }, Service.BIND_AUTO_CREATE); + } + */ + + private void initEmvTTS() { + EmvTTS.getInstance().init(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/emv/EmvTTS.java b/app/src/main/java/com/example/bdkipoc/emv/EmvTTS.java new file mode 100644 index 0000000..622490d --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/emv/EmvTTS.java @@ -0,0 +1,145 @@ +package com.example.bdkipoc.emv; + +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; +import android.util.Log; + +import com.example.bdkipoc.MyApplication; +import com.example.bdkipoc.utils.LogUtil; + +import java.util.Locale; + +public final class EmvTTS extends UtteranceProgressListener { + private static final String TAG = "EmvTTS"; + private TextToSpeech textToSpeech; + private boolean supportTTS; + private ITTSProgressListener listener; + + private EmvTTS() { + + } + + public static EmvTTS getInstance() { + return SingletonHolder.INSTANCE; + } + + public void setTTSListener(ITTSProgressListener l) { + listener = l; + } + + public void removeTTSListener() { + listener = null; + } + + private static final class SingletonHolder { + private static final EmvTTS INSTANCE = new EmvTTS(); + } + + public void init() { + //初始化TTS对象 + destroy(); + textToSpeech = new TextToSpeech(MyApplication.app, this::onTTSInit); + textToSpeech.setOnUtteranceProgressListener(this); + } + + public void play(String text) { + play(text, "0"); + } + + public void play(String text, String utteranceId) { + if (!supportTTS) { + Log.e(TAG, "PinPadTTS: play TTS failed, TTS not support..."); + return; + } + if (textToSpeech == null) { + Log.e(TAG, "PinPadTTS: play TTS slipped, textToSpeech not init.."); + return; + } + Log.e(TAG, "play() text: [" + text + "]"); + textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId); + } + + @Override + public void onStart(String utteranceId) { + Log.e(TAG, "播放开始,utteranceId:" + utteranceId); + if (listener != null) { + listener.onStart(utteranceId); + } + } + + @Override + public void onDone(String utteranceId) { + Log.e(TAG, "播放结束,utteranceId:" + utteranceId); + if (listener != null) { + listener.onDone(utteranceId); + } + } + + @Override + public void onError(String utteranceId) { + Log.e(TAG, "播放出错,utteranceId:" + utteranceId); + if (listener != null) { + listener.onError(utteranceId); + } + } + + @Override + public void onStop(String utteranceId, boolean interrupted) { + Log.e(TAG, "播放停止,utteranceId:" + utteranceId + ",interrupted:" + interrupted); + if (listener != null) { + listener.onStop(utteranceId, interrupted); + } + } + + void stop() { + if (textToSpeech != null) { + int code = textToSpeech.stop(); + Log.e(TAG, "tts stop() code:" + code); + } + } + + boolean isSpeaking() { + if (textToSpeech != null) { + return textToSpeech.isSpeaking(); + } + return false; + } + + void destroy() { + if (textToSpeech != null) { + textToSpeech.stop(); + textToSpeech.shutdown(); + textToSpeech = null; + } + } + + /** TTS初始化回调 */ + private void onTTSInit(int status) { + if (status != TextToSpeech.SUCCESS) { + LogUtil.e(TAG, "PinPadTTS: init TTS failed, status:" + status); + supportTTS = false; + return; + } + updateTtsLanguage(); + if (supportTTS) { + textToSpeech.setPitch(1.0f); + textToSpeech.setSpeechRate(1.0f); + LogUtil.e(TAG, "onTTSInit() success,locale:" + textToSpeech.getVoice().getLocale()); + } + } + + /** 更新TTS语言 */ + private void updateTtsLanguage() { + Locale locale = Locale.ENGLISH; + int result = textToSpeech.setLanguage(locale); + if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { + supportTTS = false; //系统不支持当前Locale对应的语音播报 + LogUtil.e(TAG, "updateTtsLanguage() failed, TTS not support in locale:" + locale); + } else { + supportTTS = true; + LogUtil.e(TAG, "updateTtsLanguage() success, TTS locale:" + locale); + } + } +} + + diff --git a/app/src/main/java/com/example/bdkipoc/emv/ITTSProgressListener.java b/app/src/main/java/com/example/bdkipoc/emv/ITTSProgressListener.java new file mode 100644 index 0000000..3085b8a --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/emv/ITTSProgressListener.java @@ -0,0 +1,57 @@ +package com.example.bdkipoc.emv; + +import android.speech.tts.TextToSpeech; + +public interface ITTSProgressListener { + + /** + * Called when an utterance "starts" as perceived by the caller. This will + * be soon before audio is played back in the case of a {@link TextToSpeech#speak} + * or before the first bytes of a file are written to the file system in the case + * of {@link TextToSpeech#synthesizeToFile}. + * + * @param utteranceId The utterance ID of the utterance. + */ + void onStart(String utteranceId); + + /** + * Called when an utterance has successfully completed processing. + * All audio will have been played back by this point for audible output, and all + * output will have been written to disk for file synthesis requests. + *

+ * This request is guaranteed to be called after {@link #onStart(String)}. + * + * @param utteranceId The utterance ID of the utterance. + */ + void onDone(String utteranceId); + + /** + * Called when an error has occurred during processing. This can be called + * at any point in the synthesis process. Note that there might be calls + * to {@link #onStart(String)} for specified utteranceId but there will never + * be a call to both {@link #onDone(String)} and {@link #onError(String)} for + * the same utterance. + * + * @param utteranceId The utterance ID of the utterance. + * @deprecated Use {@link #onError(String, int)} instead + */ + + /** + * @deprecated Use {@link #onError(String, int)} instead + */ + @Deprecated + void onError(String utteranceId); + + /** + * Called when an utterance has been stopped while in progress or flushed from the + * synthesis queue. This can happen if a client calls {@link TextToSpeech#stop()} + * or uses {@link TextToSpeech#QUEUE_FLUSH} as an argument with the + * {@link TextToSpeech#speak} or {@link TextToSpeech#synthesizeToFile} methods. + * + * @param utteranceId The utterance ID of the utterance. + * @param interrupted If true, then the utterance was interrupted while being synthesized + * and its output is incomplete. If false, then the utterance was flushed + * before the synthesis started. + */ + void onStop(String utteranceId, boolean interrupted); +} diff --git a/app/src/main/java/com/example/bdkipoc/kredit/CreditCardActivity.java b/app/src/main/java/com/example/bdkipoc/kredit/CreditCardActivity.java index 9a347a6..bf63b06 100644 --- a/app/src/main/java/com/example/bdkipoc/kredit/CreditCardActivity.java +++ b/app/src/main/java/com/example/bdkipoc/kredit/CreditCardActivity.java @@ -1,3 +1,341 @@ -public class CreditCardActivity { +package com.example.bdkipoc.kredit; + +import android.os.Bundle; +import android.os.RemoteException; +import android.widget.Button; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.bdkipoc.MyApplication; +import com.example.bdkipoc.R; +import com.example.bdkipoc.utils.ByteUtil; +import com.example.bdkipoc.utils.Utility; +import com.sunmi.pay.hardware.aidl.AidlConstants.CardType; +import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2; + +public class CreditCardActivity extends AppCompatActivity { + private TextView tvResult; + private Button btnCheckCard; + private boolean checkingCard; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + android.util.Log.d("CreditCard", "onCreate called"); + setContentView(R.layout.activity_credit_card); + initView(); + + // Check PaySDK status + if (MyApplication.app != null) { + android.util.Log.d("CreditCard", "MyApplication.app exists"); + android.util.Log.d("CreditCard", "PaySDK connected: " + MyApplication.app.isConnectPaySDK()); + android.util.Log.d("CreditCard", "readCardOptV2 null: " + (MyApplication.app.readCardOptV2 == null)); + } else { + android.util.Log.e("CreditCard", "MyApplication.app is null"); + } + } + + private void initView() { + android.util.Log.d("CreditCard", "initView called"); + + // Setup Toolbar as ActionBar + androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.card_test_credit_card); + } + } + + tvResult = findViewById(R.id.tv_result); + btnCheckCard = findViewById(R.id.btn_check_card); + + if (btnCheckCard != null) { + android.util.Log.d("CreditCard", "Button found, setting click listener"); + btnCheckCard.setOnClickListener(v -> { + android.util.Log.d("CreditCard", "Button clicked!"); + switchCheckCard(); + }); + } else { + android.util.Log.e("CreditCard", "Button not found!"); + } + + if (tvResult != null) { + tvResult.setText("Ready to scan card..."); + android.util.Log.d("CreditCard", "TextView initialized"); + } else { + android.util.Log.e("CreditCard", "TextView not found!"); + } + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + + private void switchCheckCard() { + android.util.Log.d("CreditCard", "switchCheckCard called, checkingCard: " + checkingCard); + try { + if (checkingCard) { + android.util.Log.d("CreditCard", "Stopping card check"); + MyApplication.app.readCardOptV2.cancelCheckCard(); + btnCheckCard.setText(R.string.card_start_check_card); + checkingCard = false; + } else { + android.util.Log.d("CreditCard", "Starting card check"); + checkCreditCard(); + checkingCard = true; + btnCheckCard.setText(R.string.card_stop_check_card); + } + } catch (Exception e) { + android.util.Log.e("CreditCard", "Error in switchCheckCard: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void checkCreditCard() { + try { + // Ensure PaySDK is bound first + if (MyApplication.app == null) { + tvResult.setText("Error: Application not initialized"); + android.util.Log.e("CreditCard", "MyApplication.app is null"); + return; + } + + // If not connected, try to bind first + if (!MyApplication.app.isConnectPaySDK()) { + tvResult.setText("Connecting to PaySDK..."); + android.util.Log.d("CreditCard", "PaySDK not connected, binding service..."); + MyApplication.app.bindPaySDKService(); + + // Wait a bit and retry + btnCheckCard.postDelayed(() -> { + if (MyApplication.app.isConnectPaySDK() && MyApplication.app.readCardOptV2 != null) { + startCardScan(); + } else { + tvResult.setText("Error: Failed to connect to PaySDK"); + checkingCard = false; + btnCheckCard.setText(R.string.card_start_check_card); + } + }, 2000); // Wait 2 seconds + return; + } + + if (MyApplication.app.readCardOptV2 == null) { + tvResult.setText("Error: Card reader not initialized"); + android.util.Log.e("CreditCard", "readCardOptV2 is null"); + return; + } + + startCardScan(); + + } catch (Exception e) { + e.printStackTrace(); + android.util.Log.e("CreditCard", "Error in checkCreditCard: " + e.getMessage()); + tvResult.setText("Error starting card scan: " + e.getMessage()); + } + } -} + private void startCardScan() { + try { + int cardType = CardType.MAGNETIC.getValue() | CardType.IC.getValue() | CardType.NFC.getValue(); + tvResult.setText("Starting card scan...\nPlease insert or swipe your card"); + + // Log for debugging + android.util.Log.d("CreditCard", "Starting checkCard with cardType: " + cardType); + + MyApplication.app.readCardOptV2.checkCard(cardType, mCheckCardCallback, 60); + + } catch (Exception e) { + e.printStackTrace(); + android.util.Log.e("CreditCard", "Error in startCardScan: " + e.getMessage()); + tvResult.setText("Error starting card scan: " + e.getMessage()); + } + } + + private final CheckCardCallbackV2 mCheckCardCallback = new CheckCardCallbackV2.Stub() { + @Override + public void findMagCard(Bundle info) throws RemoteException { + runOnUiThread(() -> handleMagCardResult(info)); + } + + @Override + public void findICCard(String atr) throws RemoteException { + Bundle info = new Bundle(); + info.putString("atr", atr); + runOnUiThread(() -> handleICCardResult(info)); + } + + @Override + public void findRFCard(String uuid) throws RemoteException { + // Handle RF card detection - changed to single parameter + Bundle info = new Bundle(); + info.putString("uuid", uuid); + runOnUiThread(() -> handleRFCardResult(info)); + } + + @Override + public void onError(int code, String message) throws RemoteException { + Bundle info = new Bundle(); + info.putInt("code", code); + info.putString("message", message); + runOnUiThread(() -> handleErrorResult(info)); + } + + @Override + public void findICCardEx(Bundle info) throws RemoteException { + runOnUiThread(() -> handleICCardResult(info)); + } + + @Override + public void findRFCardEx(Bundle info) throws RemoteException { + runOnUiThread(() -> handleRFCardResult(info)); + } + + @Override + public void onErrorEx(Bundle info) throws RemoteException { + runOnUiThread(() -> handleErrorResult(info)); + } + }; + + private void handleMagCardResult(Bundle info) { + android.util.Log.d("CreditCard", "=== MAGNETIC CARD DATA ==="); + + String track1 = Utility.null2String(info.getString("TRACK1")); + String track2 = Utility.null2String(info.getString("TRACK2")); + String track3 = Utility.null2String(info.getString("TRACK3")); + + // Log detailed track data + android.util.Log.d("CreditCard", "Track1: " + track1); + android.util.Log.d("CreditCard", "Track2: " + track2); + android.util.Log.d("CreditCard", "Track3: " + track3); + + // Log error codes if available + int track1ErrorCode = info.getInt("track1ErrorCode", 0); + int track2ErrorCode = info.getInt("track2ErrorCode", 0); + int track3ErrorCode = info.getInt("track3ErrorCode", 0); + + android.util.Log.d("CreditCard", "Track1 Error Code: " + track1ErrorCode); + android.util.Log.d("CreditCard", "Track2 Error Code: " + track2ErrorCode); + android.util.Log.d("CreditCard", "Track3 Error Code: " + track3ErrorCode); + + // Log additional info if available + String pan = info.getString("pan", ""); + String serviceCode = info.getString("servicecode", ""); + if (!pan.isEmpty()) android.util.Log.d("CreditCard", "PAN: " + pan); + if (!serviceCode.isEmpty()) android.util.Log.d("CreditCard", "Service Code: " + serviceCode); + + StringBuilder sb = new StringBuilder() + .append(getString(R.string.card_mag_card_detected)).append("\n") + .append("Track1:").append(track1).append("\n") + .append("Track2:").append(track2).append("\n") + .append("Track3:").append(track3); + tvResult.setText(sb); + switchCheckCard(); + } + + private void handleICCardResult(Bundle info) { + android.util.Log.d("CreditCard", "=== IC CARD DATA ==="); + + String atr = info.getString("atr", ""); + int cardType = info.getInt("cardType", -1); + + android.util.Log.d("CreditCard", "ATR: " + atr); + android.util.Log.d("CreditCard", "Card Type: " + cardType); + android.util.Log.d("CreditCard", "Full IC Card Data: " + bundleToString(info)); + + StringBuilder sb = new StringBuilder(); + sb.append(getString(R.string.card_ic_card_detected)).append("\n") + .append("ATR:").append(atr).append("\n"); + if (cardType != -1) { + sb.append("Card Type:").append(cardType).append("\n"); + } + tvResult.setText(sb); + switchCheckCard(); + } + + private void handleRFCardResult(Bundle info) { + android.util.Log.d("CreditCard", "=== RF/NFC CARD DATA ==="); + + String uuid = info.getString("uuid", ""); + String ats = info.getString("ats", ""); + int cardType = info.getInt("cardType", -1); + int sak = info.getInt("sak", -1); + int cardCategory = info.getInt("cardCategory", -1); + byte[] atqa = info.getByteArray("atqa"); + + android.util.Log.d("CreditCard", "UUID: " + uuid); + android.util.Log.d("CreditCard", "ATS: " + ats); + android.util.Log.d("CreditCard", "Card Type: " + cardType); + android.util.Log.d("CreditCard", "SAK: " + sak); + android.util.Log.d("CreditCard", "Card Category: " + cardCategory); + if (atqa != null) { + android.util.Log.d("CreditCard", "ATQA: " + ByteUtil.bytes2HexStr(atqa)); + } + android.util.Log.d("CreditCard", "Full RF Card Data: " + bundleToString(info)); + + StringBuilder sb = new StringBuilder(); + sb.append("RF Card Detected").append("\n") + .append("UUID: ").append(uuid).append("\n"); + if (!ats.isEmpty()) { + sb.append("ATS: ").append(ats).append("\n"); + } + if (sak != -1) { + sb.append("SAK: ").append(String.format("0x%02X", sak)).append("\n"); + } + tvResult.setText(sb); + switchCheckCard(); + } + + private void handleErrorResult(Bundle info) { + int code = info.getInt("code"); + String msg = info.getString("message"); + String error = "Error: " + msg + " (Code: " + code + ")"; + tvResult.setText(error); + switchCheckCard(); + } + + @Override + protected void onDestroy() { + cancelCheckCard(); + super.onDestroy(); + } + + private void cancelCheckCard() { + try { + MyApplication.app.readCardOptV2.cardOff(CardType.NFC.getValue()); + MyApplication.app.readCardOptV2.cardOff(CardType.IC.getValue()); + MyApplication.app.readCardOptV2.cancelCheckCard(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Helper method to convert Bundle to readable string for logging + */ + private String bundleToString(Bundle bundle) { + if (bundle == null) return "null"; + + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + sb.append(key).append("="); + if (value instanceof byte[]) { + sb.append(ByteUtil.bytes2HexStr((byte[]) value)); + } else { + sb.append(value); + } + sb.append(", "); + } + if (sb.length() > 1) { + sb.setLength(sb.length() - 2); // Remove last ", " + } + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java b/app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java new file mode 100644 index 0000000..51cb9e1 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java @@ -0,0 +1,265 @@ +package com.example.bdkipoc.utils; + +import android.text.TextUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public class ByteUtil { + + /** 打印内容 */ + public static String byte2PrintHex(byte[] raw, int offset, int count) { + if (raw == null) { + return null; + } + if (offset < 0 || offset > raw.length) { + offset = 0; + } + int end = offset + count; + if (end > raw.length) { + end = raw.length; + } + StringBuilder hex = new StringBuilder(); + for (int i = offset; i < end; i++) { + int v = raw[i] & 0xFF; + String hv = Integer.toHexString(v); + if (hv.length() < 2) { + hex.append(0); + } + hex.append(hv); + hex.append(" "); + } + if (hex.length() > 0) { + hex.deleteCharAt(hex.length() - 1); + } + return hex.toString().toUpperCase(); + } + + /** + * 将字节数组转换成16进制字符串 + * + * @param bytes 源字节数组 + * @return 转换后的16进制字符串 + */ + public static String bytes2HexStr(byte... bytes) { + if (bytes == null || bytes.length == 0) { + return ""; + } + return bytes2HexStr(bytes, 0, bytes.length); + } + + /** + * 将字节数组转换成16进制字符串 + * + * @param src 源字节数组 + * @param offset 偏移量 + * @param len 数据长度 + * @return 转换后的16进制字符串 + */ + public static String bytes2HexStr(byte[] src, int offset, int len) { + int end = offset + len; + if (src == null || src.length == 0 || offset < 0 || len < 0 || end > src.length) { + return ""; + } + byte[] buffer = new byte[len * 2]; + int h = 0, l = 0; + for (int i = offset, j = 0; i < end; i++) { + h = src[i] >> 4 & 0x0f; + l = src[i] & 0x0f; + buffer[j++] = (byte) (h > 9 ? h - 10 + 'A' : h + '0'); + buffer[j++] = (byte) (l > 9 ? l - 10 + 'A' : l + '0'); + } + return new String(buffer); + } + + public static byte[] hexStr2Bytes(String hexStr) { + if (TextUtils.isEmpty(hexStr)) { + return new byte[0]; + } + int length = hexStr.length() / 2; + char[] chars = hexStr.toCharArray(); + byte[] b = new byte[length]; + for (int i = 0; i < length; i++) { + b[i] = (byte) (char2Byte(chars[i * 2]) << 4 | char2Byte(chars[i * 2 + 1])); + } + return b; + } + + public static byte hexStr2Byte(String hexStr) { + return (byte) Integer.parseInt(hexStr, 16); + } + + public static String hexStr2Str(String hexStr) { + String vi = "0123456789ABC DEF".trim(); + char[] array = hexStr.toCharArray(); + byte[] bytes = new byte[hexStr.length() / 2]; + int temp; + for (int i = 0; i < bytes.length; i++) { + char c = array[2 * i]; + temp = vi.indexOf(c) * 16; + c = array[2 * i + 1]; + temp += vi.indexOf(c); + bytes[i] = (byte) (temp & 0xFF); + } + return new String(bytes); + } + + public static String hexStr2AsciiStr(String hexStr) { + String vi = "0123456789ABC DEF".trim(); + hexStr = hexStr.trim().replace(" ", "").toUpperCase(Locale.US); + char[] array = hexStr.toCharArray(); + byte[] bytes = new byte[hexStr.length() / 2]; + int temp = 0x00; + for (int i = 0; i < bytes.length; i++) { + char c = array[2 * i]; + temp = vi.indexOf(c) << 4; + c = array[2 * i + 1]; + temp |= vi.indexOf(c); + bytes[i] = (byte) (temp & 0xFF); + } + return new String(bytes); + } + + /** + * 将无符号short转换成int,大端模式(高位在前) + */ + public static int unsignedShort2IntBE(byte[] src, int offset) { + return (src[offset] & 0xff) << 8 | (src[offset + 1] & 0xff); + } + + /** + * 将无符号short转换成int,小端模式(低位在前) + */ + public static int unsignedShort2IntLE(byte[] src, int offset) { + return (src[offset] & 0xff) | (src[offset + 1] & 0xff) << 8; + } + + /** + * 将无符号byte转换成int + */ + public static int unsignedByte2Int(byte[] src, int offset) { + return src[offset] & 0xFF; + } + + /** + * 将字节数组转换成int,大端模式(高位在前) + */ + public static int unsignedInt2IntBE(byte[] src, int offset) { + int result = 0; + for (int i = offset; i < offset + 4; i++) { + result |= (src[i] & 0xff) << (offset + 3 - i) * 8; + } + return result; + } + + /** + * 将字节数组转换成int,小端模式(低位在前) + */ + public static int unsignedInt2IntLE(byte[] src, int offset) { + int value = 0; + for (int i = offset; i < offset + 4; i++) { + value |= (src[i] & 0xff) << (i - offset) * 8; + } + return value; + } + + /** + * 将int转换成byte数组,大端模式(高位在前) + */ + public static byte[] int2BytesBE(int src) { + byte[] result = new byte[4]; + for (int i = 0; i < 4; i++) { + result[i] = (byte) (src >> (3 - i) * 8); + } + return result; + } + + /** + * 将int转换成byte数组,小端模式(低位在前) + */ + public static byte[] int2BytesLE(int src) { + byte[] result = new byte[4]; + for (int i = 0; i < 4; i++) { + result[i] = (byte) (src >> i * 8); + } + return result; + } + + /** + * 将short转换成byte数组,大端模式(高位在前) + */ + public static byte[] short2BytesBE(short src) { + byte[] result = new byte[2]; + for (int i = 0; i < 2; i++) { + result[i] = (byte) (src >> (1 - i) * 8); + } + return result; + } + + /** + * 将short转换成byte数组,小端模式(低位在前) + */ + public static byte[] short2BytesLE(short src) { + byte[] result = new byte[2]; + for (int i = 0; i < 2; i++) { + result[i] = (byte) (src >> i * 8); + } + return result; + } + + /** + * 将字节数组列表合并成单个字节数组 + */ + public static byte[] concatByteArrays(byte[]... list) { + if (list == null || list.length == 0) { + return new byte[0]; + } + return concatByteArrays(Arrays.asList(list)); + } + + /** + * 将字节数组列表合并成单个字节数组 + */ + public static byte[] concatByteArrays(List list) { + if (list == null || list.isEmpty()) { + return new byte[0]; + } + int totalLen = 0; + for (byte[] b : list) { + if (b == null || b.length == 0) { + continue; + } + totalLen += b.length; + } + byte[] result = new byte[totalLen]; + int index = 0; + for (byte[] b : list) { + if (b == null || b.length == 0) { + continue; + } + System.arraycopy(b, 0, result, index, b.length); + index += b.length; + } + return result; + } + + + /** + * Convert char to byte + * + * @param c char + * @return byte + */ + private static int char2Byte(char c) { + if (c >= 'a') { + return (c - 'a' + 10) & 0x0f; + } + if (c >= 'A') { + return (c - 'A' + 10) & 0x0f; + } + return (c - '0') & 0x0f; + } + + +} diff --git a/app/src/main/java/com/example/bdkipoc/utils/LogUtil.java b/app/src/main/java/com/example/bdkipoc/utils/LogUtil.java new file mode 100644 index 0000000..f62df04 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/utils/LogUtil.java @@ -0,0 +1,89 @@ +package com.example.bdkipoc.utils; + +import android.text.TextUtils; +import android.util.Log; + +public class LogUtil { + + public static final int VERBOSE = 1; + public static final int DEBUG = 2; + public static final int INFO = 3; + public static final int WARN = 4; + public static final int ERROR = 5; + public static final int NOTHING = 6; + public static int LEVEL = VERBOSE; + + public static void setLevel(int Level) { + LEVEL = Level; + } + + public static void v(String TAG, String msg) { + if (LEVEL <= VERBOSE && !TextUtils.isEmpty(msg)) { + MyLog(VERBOSE, TAG, msg); + } + } + + public static void d(String TAG, String msg) { + if (LEVEL <= DEBUG && !TextUtils.isEmpty(msg)) { + MyLog(DEBUG, TAG, msg); + } + } + + public static void i(String TAG, String msg) { + if (LEVEL <= INFO && !TextUtils.isEmpty(msg)) { + MyLog(INFO, TAG, msg); + } + } + + public static void w(String TAG, String msg) { + if (LEVEL <= WARN && !TextUtils.isEmpty(msg)) { + MyLog(WARN, TAG, msg); + } + } + + public static void e(String TAG, String msg) { + if (LEVEL <= ERROR && !TextUtils.isEmpty(msg)) { + MyLog(ERROR, TAG, msg); + } + } + + private static void MyLog(int type, String TAG, String msg) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + int index = 4; + String className = stackTrace[index].getFileName(); + String methodName = stackTrace[index].getMethodName(); + int lineNumber = stackTrace[index].getLineNumber(); + methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("[ (") + .append(className) + .append(":") + .append(lineNumber) + .append(")#") + .append(methodName) + .append(" ] "); + stringBuilder.append(msg); + String logStr = stringBuilder.toString(); + switch (type) { + case VERBOSE: + Log.v(TAG, logStr); + break; + case DEBUG: + Log.d(TAG, logStr); + break; + case INFO: + Log.i(TAG, logStr); + break; + case WARN: + Log.w(TAG, logStr); + break; + case ERROR: + Log.e(TAG, logStr); + break; + default: + break; + } + } + + +} diff --git a/app/src/main/java/com/example/bdkipoc/utils/Utility.java b/app/src/main/java/com/example/bdkipoc/utils/Utility.java new file mode 100644 index 0000000..509274f --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/utils/Utility.java @@ -0,0 +1,107 @@ +package com.example.bdkipoc.utils; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import com.example.bdkipoc.MyApplication; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +public final class Utility { + private Utility() { + throw new AssertionError("Create instance of Utility is forbidden."); + } + + /** Bundle对象转换成字符串 */ + public static String bundle2String(Bundle bundle) { + return bundle2String(bundle, 1); + } + + /** + * 根据key排序后将Bundle内容拼接成字符串 + * + * @param bundle 要处理的bundle + * @param order 排序规则,0-不排序,1-升序,2-降序 + * @return 拼接后的字符串 + */ + public static String bundle2String(Bundle bundle, int order) { + if (bundle == null || bundle.keySet().isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + List list = new ArrayList<>(bundle.keySet()); + if (order == 1) { //升序 + Collections.sort(list, String::compareTo); + } else if (order == 2) {//降序 + Collections.sort(list, Collections.reverseOrder()); + } + for (String key : list) { + sb.append(key); + sb.append(":"); + Object value = bundle.get(key); + if (value instanceof byte[]) { + sb.append(ByteUtil.bytes2HexStr((byte[]) value)); + } else { + sb.append(value); + } + sb.append("\n"); + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** 将null转换成空串 */ + public static String null2String(String str) { + return str == null ? "" : str; + } + + public static String formatStr(String format, Object... params) { + return String.format(Locale.ENGLISH, format, params); + } + + /** check whether src is hex format */ + public static boolean checkHexValue(String src) { + return Pattern.matches("[0-9a-fA-F]+", src); + } + + /** 显示Toast */ + public static void showToast(final String msg) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> Toast.makeText(MyApplication.app, msg, Toast.LENGTH_SHORT).show()); + } + + /** 显示Toast */ + public static void showToast(int resId) { + showToast(MyApplication.app.getString(resId)); + } + + /** 根据结果码获取成功失败信息 */ + public static String getStateString(int code) { + return code == 0 ? "success" : "failed, code:" + code; + } + + /** 根据结果状态获取成功失败信息 */ + public static String getStateString(boolean state) { + return state ? "success" : "failed"; + } + + /** 将dp转成px */ + public static int dp2px(int dp) { + float density = MyApplication.app.getResources().getDisplayMetrics().density; + return Math.round(dp * density); + } + + /** 将px转成dp */ + public static int px2dp(int px) { + float density = MyApplication.app.getResources().getDisplayMetrics().density; + return Math.round(px / density); + } +} diff --git a/app/src/main/java/com/example/bdkipoc/wrapper/CheckCardCallbackV2Wrapper.java b/app/src/main/java/com/example/bdkipoc/wrapper/CheckCardCallbackV2Wrapper.java new file mode 100644 index 0000000..b26228a --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/wrapper/CheckCardCallbackV2Wrapper.java @@ -0,0 +1,44 @@ +package com.example.bdkipoc.wrapper; + +import android.os.Bundle; +import android.os.RemoteException; + +import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2; + + +public class CheckCardCallbackV2Wrapper extends CheckCardCallbackV2.Stub { + @Override + public void findMagCard(Bundle info) throws RemoteException { + + } + + @Override + public void findICCard(String atr) throws RemoteException { + + } + + @Override + public void findRFCard(String uuid) throws RemoteException { + + } + + @Override + public void onError(int code, String message) throws RemoteException { + + } + + @Override + public void findICCardEx(Bundle info) throws RemoteException { + + } + + @Override + public void findRFCardEx(Bundle info) throws RemoteException { + + } + + @Override + public void onErrorEx(Bundle info) throws RemoteException { + + } +} diff --git a/app/src/main/res/layout/activity_credit_card.xml b/app/src/main/res/layout/activity_credit_card.xml index bde7175..1b99d0e 100644 --- a/app/src/main/res/layout/activity_credit_card.xml +++ b/app/src/main/res/layout/activity_credit_card.xml @@ -1,120 +1,30 @@ + android:orientation="vertical"> + + - - - - - -