Safepoint Card Reading

This commit is contained in:
riz081 2025-06-17 22:49:41 +07:00
parent d7617186a6
commit 124da43a1e
18 changed files with 1351 additions and 121 deletions

View File

@ -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

View File

@ -24,6 +24,7 @@
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -71,7 +72,8 @@
android:name=".HistoryDetailActivity"
android:exported="false" />
<activity
android:name=".CreditCardActivity"
android:name=".kredit.CreditCardActivity"
style="@style/Theme.AppCompat"
android:exported="false" />
</application>

View File

@ -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);
}
}

View File

@ -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";
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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.
* <p>
* 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);
}

View File

@ -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();
}
}

View File

@ -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<byte[]> 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;
}
}

View File

@ -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;
}
}
}

View File

@ -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<String> 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);
}
}

View File

@ -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 {
}
}

View File

@ -1,120 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".CreditCardActivity">
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Credit Card Payment"
android:textSize="24sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/tvConnectionStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="PaySDK Status: Connecting..."
android:textSize="16sp"
android:padding="12dp"
android:background="@drawable/bg_status"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<Button
android:id="@+id/btnCheckStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Check Status"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btnReconnect"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Reconnect"
android:layout_marginStart="8dp" />
</LinearLayout>
<ScrollView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:layout_margin="16dp"
android:text="Ready to scan card..."
android:gravity="top" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvPaymentMethods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Payment Methods"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<Button
android:id="@+id/btnReadCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Read Card"
android:layout_marginBottom="8dp"
android:enabled="false" />
<Button
android:id="@+id/btnEMVPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="EMV Payment"
android:layout_marginBottom="8dp"
android:enabled="false" />
<Button
android:id="@+id/btnPinPad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="PIN Pad Test"
android:layout_marginBottom="8dp"
android:enabled="false" />
<Button
android:id="@+id/btnPrintTest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Print Test"
android:layout_marginBottom="16dp"
android:enabled="false" />
</LinearLayout>
</ScrollView>
<TextView
android:id="@+id/tvDebugInfo"
<Button
android:id="@+id/btn_check_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Debug info will appear here..."
android:textSize="12sp"
android:background="#f5f5f5"
android:padding="8dp"
android:maxLines="3"
android:ellipsize="end" />
android:layout_margin="16dp"
android:text="@string/card_start_check_card" />
</LinearLayout>

View File

@ -1,7 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="white">#FFFFFF</color>
<color name="colorOrange">#FF6600</color>
<color name="transparent">#00000000</color>
<color name="colorBackground">#F0F2F5</color>
<color name="colorTextTitle">#222222</color>
<color name="colorTextContent">#666666</color>
<color name="colorTextHelp">#999999</color>
<color name="colorLineColor">#d7d7d7</color>
<color name="FD5A52">#FD5A52</color>
<color name="CE6E6E6">#E6E6E6</color>
<color name="FF3C00">#FF3C00</color>
<color name="C999999">#999999</color>
<color name="black">#000000</color>
<!-- Banking App Theme Colors -->
<color name="primary_blue">#1976D2</color>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="smallSize">24dp</dimen>
<dimen name="itemSize">48dp</dimen>
<dimen name="titleSize">56dp</dimen>
</resources>

View File

@ -13,6 +13,11 @@
<string name="payment_status_success">Payment Successful!</string>
<string name="return_main">Return to Main Screen</string>
<string name="main_title">POC</string>
<!-- In res/values/strings.xml -->
<string name="connect_fail">Connection to payment service failed</string>
<!-- In res/values/strings.xml -->
<string name="card_test_credit_card">Credit Card Test</string>
<string name="card_mag_card_detected">Magnetic Card Detected</string>
<string name="card_ic_card_detected">IC Card Detected</string>
<string name="card_start_check_card">Start Check Card</string>
<string name="card_stop_check_card">Stop Check Card</string>
<string name="connect_fail">Connection failed</string>
</resources>

View File

@ -29,6 +29,11 @@
<item name="titleTextAppearance">@style/ToolbarTitleStyle</item>
</style>
<style name="Toolbar.TitleText" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">18sp</item>
<item name="android:textColor">@android:color/white</item>
</style>
<!-- Numpad Button Style -->
<style name="NumpadButton">
<item name="android:layout_width">0dp</item>