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" />
+ * 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