commit 7ad09b2b5f562a6d24d01c5ce6279f69a89d83b3 Author: ahmeddatexpay Date: Wed Sep 24 18:33:01 2025 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..09de6c5 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..976cf21 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..da323f3 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,76 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'com.test.cardreadtest' + compileSdk 35 + + signingConfigs { + release { + storeFile file('new-keystore.jks') + storePassword 'MPOS73356xDp' + keyPassword 'MPOS73356xDp' + keyAlias 'mulberrypos' + } + debug { + storeFile file('new-keystore.jks') + storePassword 'MPOS73356xDp' + keyPassword 'MPOS73356xDp' + keyAlias 'mulberrypos' + } + } + + + defaultConfig { + applicationId "com.test.cardreadtest" + minSdk 26 + targetSdk 34 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + viewBinding { + enabled = true + } + dataBinding { + enabled = true + } + packagingOptions { + doNotStrip '**/lib/*.so' // 保留所有SO文件 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: []) + + + implementation 'androidx.recyclerview:recyclerview:1.3.1' // Use the latest version + implementation 'androidx.viewpager2:viewpager2:1.0.0' // Use the latest version + implementation 'androidx.databinding:databinding-runtime:8.0.0' + + implementation 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter:4.0.0' + implementation 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter-recyclerview:4.0.0' + implementation 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter-viewpager2:4.0.0' + + implementation libs.appcompat + implementation libs.material + implementation libs.activity + implementation libs.constraintlayout + testImplementation libs.junit + androidTestImplementation libs.ext.junit + androidTestImplementation libs.espresso.core +} \ No newline at end of file diff --git a/app/libs/mulberry_pos_sdk_7.4.5.aar b/app/libs/mulberry_pos_sdk_7.4.5.aar new file mode 100644 index 0000000..4182e7f Binary files /dev/null and b/app/libs/mulberry_pos_sdk_7.4.5.aar differ diff --git a/app/libs/mulberry_print_sdk_1.5.5.aar b/app/libs/mulberry_print_sdk_1.5.5.aar new file mode 100644 index 0000000..08c4e3d Binary files /dev/null and b/app/libs/mulberry_print_sdk_1.5.5.aar differ diff --git a/app/libs/mvvmhabit-release.aar b/app/libs/mvvmhabit-release.aar new file mode 100644 index 0000000..f0c5bf5 Binary files /dev/null and b/app/libs/mvvmhabit-release.aar differ diff --git a/app/new-keystore.jks b/app/new-keystore.jks new file mode 100644 index 0000000..3c104b3 Binary files /dev/null and b/app/new-keystore.jks differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/test/cardreadtest/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/test/cardreadtest/ExampleInstrumentedTest.java new file mode 100644 index 0000000..d2335fe --- /dev/null +++ b/app/src/androidTest/java/com/test/cardreadtest/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.test.cardreadtest; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.test.cardreadtest", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ba6c814 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/test/cardreadtest/MainActivity.java b/app/src/main/java/com/test/cardreadtest/MainActivity.java new file mode 100644 index 0000000..d519ee4 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/MainActivity.java @@ -0,0 +1,38 @@ +package com.test.cardreadtest; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.test.cardreadtest.payment.PaymentActivity; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + Button btnReadCard = findViewById(R.id.btnReadCard); + btnReadCard.setOnClickListener(v -> { + // For testing, you can pass dummy values + Intent intent = new Intent(MainActivity.this, PaymentActivity.class); + intent.putExtra("amount", "5.00"); + intent.putExtra("deviceAddress", "UART"); + startActivity(intent); + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/test/cardreadtest/common/enums/POS_TYPE.java b/app/src/main/java/com/test/cardreadtest/common/enums/POS_TYPE.java new file mode 100644 index 0000000..15ff572 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/common/enums/POS_TYPE.java @@ -0,0 +1,5 @@ +package com.test.cardreadtest.common.enums; + +public enum POS_TYPE { + BLUETOOTH, UART, USB, BLUETOOTH_BLE +} diff --git a/app/src/main/java/com/test/cardreadtest/common/enums/PaymentType.java b/app/src/main/java/com/test/cardreadtest/common/enums/PaymentType.java new file mode 100644 index 0000000..bc7b2db --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/common/enums/PaymentType.java @@ -0,0 +1,37 @@ +package com.test.cardreadtest.common.enums; + +public enum PaymentType { + GOODS("GOODS"), + SERVICES("SERVICES"), + CASH("CASH"), + CASHBACK("CASHBACK"), + PURCHASE_REFUND("PURCHASE_REFUND"), + INQUIRY("INQUIRY"), + TRANSFER("TRANSFER"), + ADMIN("ADMIN"), + PAYMENT("PAYMENT"), + SALE("SALE"), + CHANGE_PIN("CHANGE_PIN"), + BALANCE("BALANCE"), + BALANCE_UPDATE("BALANCE_UPDATE"), + REFOUND("REFUND"); + + private final String value; + + PaymentType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static String[] getValues() { + PaymentType[] types = PaymentType.values(); + String[] values = new String[types.length]; + for (int i = 0; i < types.length; i++) { + values[i] = types[i].getValue(); + } + return values; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/test/cardreadtest/common/enums/TransCardMode.java b/app/src/main/java/com/test/cardreadtest/common/enums/TransCardMode.java new file mode 100644 index 0000000..1198b96 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/common/enums/TransCardMode.java @@ -0,0 +1,32 @@ +package com.test.cardreadtest.common.enums; + +import com.mulberry.xpos.QPOSService; + +public enum TransCardMode { + SWIPE_TAP_INSERT_CARD_NOTUP(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_NOTUP),SWIPE_TAP_INSERT_CARD(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD), ONLY_INSERT_CARD(QPOSService.CardTradeMode.ONLY_INSERT_CARD), ONLY_SWIPE_CARD(QPOSService.CardTradeMode.ONLY_SWIPE_CARD), TAP_INSERT_CARD(QPOSService.CardTradeMode.TAP_INSERT_CARD), + TAP_INSERT_CARD_NOTUP(QPOSService.CardTradeMode.TAP_INSERT_CARD_NOTUP), UNALLOWED_LOW_TRADE(QPOSService.CardTradeMode.UNALLOWED_LOW_TRADE), SWIPE_INSERT_CARD(QPOSService.CardTradeMode.SWIPE_INSERT_CARD), SWIPE_TAP_INSERT_CARD_UNALLOWED_LOW_TRADE(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_UNALLOWED_LOW_TRADE), + SWIPE_TAP_INSERT_CARD_NOTUP_UNALLOWED_LOW_TRADE(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_NOTUP_UNALLOWED_LOW_TRADE), ONLY_TAP_CARD(QPOSService.CardTradeMode.ONLY_TAP_CARD), + ONLY_TAP_CARD_QF(QPOSService.CardTradeMode.ONLY_TAP_CARD_QF), + SWIPE_TAP_INSERT_CARD_DOWN(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_DOWN), SWIPE_INSERT_CARD_UNALLOWED_LOW_TRADE(QPOSService.CardTradeMode.SWIPE_INSERT_CARD_UNALLOWED_LOW_TRADE), + SWIPE_TAP_INSERT_CARD_UNALLOWED_LOW_TRADE_NEW(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_UNALLOWED_LOW_TRADE_NEW), ONLY_INSERT_CARD_NOPIN(QPOSService.CardTradeMode.ONLY_INSERT_CARD_NOPIN), + SWIPE_TAP_INSERT_CARD_NOTUP_DELAY(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_NOTUP_DELAY); + + protected QPOSService.CardTradeMode cardTradeModeValue; + + public QPOSService.CardTradeMode getCardTradeModeValue() { + return cardTradeModeValue; + } + + TransCardMode(QPOSService.CardTradeMode i) { + this.cardTradeModeValue = i; + } + + public static String[] getCardTradeModes() { + TransCardMode[] types = TransCardMode.values(); + String[] values = new String[types.length]; + for (int i = 0; i < types.length; i++) { + values[i] = types[i].getCardTradeModeValue().name(); + } + return values; + } +} diff --git a/app/src/main/java/com/test/cardreadtest/payment/PaymentActivity.java b/app/src/main/java/com/test/cardreadtest/payment/PaymentActivity.java new file mode 100644 index 0000000..08d1858 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/payment/PaymentActivity.java @@ -0,0 +1,203 @@ +package com.test.cardreadtest.payment; + +import static android.content.Intent.getIntent; + +import android.app.Application; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; +import androidx.lifecycle.ViewModelProvider; + +import com.test.cardreadtest.R; +import com.test.cardreadtest.databinding.ActivityPaymentBinding; +import com.test.cardreadtest.posAPI.ConnectionServiceCallback; +import com.test.cardreadtest.posAPI.POSManager; +import com.test.cardreadtest.posAPI.PaymentServiceCallback; +import com.test.cardreadtest.utils.TRACE; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + + +//import me.goldze.mvvmhabit.utils.ToastUtils; + +public class PaymentActivity extends AppCompatActivity { + private static final String TAG = "PaymentActivity"; + private String amount; + private String deviceAddress; + private PaymentViewModel viewModel; + private ActivityPaymentBinding binding; + private PaymentServiceCallback paymentServiceCallback; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Simple DataBinding inflation - NO third-party dependencies + binding = ActivityPaymentBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + // Use standard ViewModelProvider + viewModel = new ViewModelProvider(this).get(PaymentViewModel.class); + binding.setViewModel(viewModel); + + initData(); + } + + /** + * Initialize payment activity data + * Sets up initial UI state and starts transaction + */ + public void initData() { + paymentServiceCallback = new PaymentCallback(); + + // Get intent data + Intent intent = getIntent(); + amount = intent.getStringExtra("amount"); + + // For testing, use default values if not provided + if (amount == null) amount = "1.00"; + + viewModel.displayAmount(amount);//display to UI + startTransaction(); + } + + /** + * Start payment transaction in background thread + * Handles device connection and transaction initialization + */ + private void startTransaction() { + new Thread(() -> { + // Initialize POSManager if not already done + POSManager.init(getApplicationContext()); + + if(!POSManager.getInstance().isDeviceReady()){ + POSManager.getInstance().connect(deviceAddress, new ConnectionServiceCallback() { + @Override + public void onRequestNoQposDetected() { + runOnUiThread(() -> Log.d(TAG, "No device detected")); + } + + @Override + public void onRequestQposConnected() { + runOnUiThread(() -> Log.d(TAG, "Device connected")); + } + + @Override + public void onRequestQposDisconnected() { + runOnUiThread(() -> { + Log.d(TAG, "Device disconnected"); + finish(); + }); + } + }); + } + + // Start transaction with callback + POSManager.getInstance().startTransaction(amount, paymentServiceCallback); + }).start(); + } + + /** + * Inner class to handle payment callbacks + * Implements all payment related events and UI updates + */ + private class PaymentCallback implements PaymentServiceCallback { + @Override + public void onRequestWaitingUser() { + runOnUiThread(() -> { + viewModel.setWaitingStatus(true); + Log.d(TAG, "Please insert/swipe/tap card"); + }); + } + + @Override + public void onRequestTime() { + String terminalTime = new SimpleDateFormat("yyyyMMddHHmmss").format(Calendar.getInstance().getTime()); + TRACE.d("onRequestTime: " + terminalTime); + POSManager.getInstance().sendTime(terminalTime); + } + + @Override + public void onRequestSelectEmvApp(ArrayList appList) { + TRACE.d("onRequestSelectEmvApp():" + appList.toString()); + // Auto-select first app for testing + if (!appList.isEmpty()) { + POSManager.getInstance().selectEmvApp(0); + } + } + + @Override + public void onQposRequestPinResult(List dataList, int offlineTime) { + TRACE.d("onQposRequestPinResult = " + dataList + "\nofflineTime: " + offlineTime); + } + + @Override + public void onRequestSetPin(boolean isOfflinePin, int tryNum) { + TRACE.d("onRequestSetPin = " + isOfflinePin + "\ntryNum: " + tryNum); + } + + @Override + public void onRequestSetPin() { + TRACE.i("onRequestSetPin()"); + } + + @Override + public void onRequestDisplay(com.mulberry.xpos.QPOSService.Display displayMsg) { + TRACE.d("onRequestDisplay(Display displayMsg):" + displayMsg.toString()); + } + + @Override + public void onTransactionCompleted(com.test.cardreadtest.posAPI.PaymentResult result) { + runOnUiThread(() -> { + Log.d(TAG, "Transaction completed: " + result.getTransactionType()); + // Display basic card info for testing + if (result.getMaskedPAN() != null) { + Log.d(TAG, "Card: " + result.getMaskedPAN()); + } +// finish(); + }); + } + + @Override + public void onTransactionFailed(String errorMessage, String data) { + runOnUiThread(() -> { + Log.d(TAG, "Transaction failed: " + errorMessage); + finish(); + }); + } + + @Override + public void onRequestOnlineProcess(final String tlv) { + TRACE.d("onRequestOnlineProcess" + tlv); + // For testing, just send success response + POSManager.getInstance().sendOnlineProcessResult("8A023030"); + } + + @Override + public void onReturnGetPinInputResult(int num) { + TRACE.i("onReturnGetPinInputResult ===" + num); + } + } + + @Override + public void onBackPressed() { + // Cancel transaction and go back + POSManager.getInstance().cancelTransaction(); + super.onBackPressed(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (POSManager.getInstance() != null) { + POSManager.getInstance().unregisterCallbacks(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/test/cardreadtest/payment/PaymentViewModel.java b/app/src/main/java/com/test/cardreadtest/payment/PaymentViewModel.java new file mode 100644 index 0000000..0793973 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/payment/PaymentViewModel.java @@ -0,0 +1,19 @@ +package com.test.cardreadtest.payment; + +import androidx.lifecycle.ViewModel; + +public class PaymentViewModel extends ViewModel { + + public PaymentViewModel() { + super(); + } + + public void displayAmount(String amount) { + // For testing + System.out.println("Amount to display: " + amount); + } + + public void setWaitingStatus(boolean waiting) { + System.out.println("Waiting status: " + waiting); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/test/cardreadtest/posAPI/ConnectionServiceCallback.java b/app/src/main/java/com/test/cardreadtest/posAPI/ConnectionServiceCallback.java new file mode 100644 index 0000000..8741c64 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/posAPI/ConnectionServiceCallback.java @@ -0,0 +1,26 @@ +package com.test.cardreadtest.posAPI; + +/** + * Connection Service Callback Interface + * Handle all connection-related callback methods + */ +public interface ConnectionServiceCallback { + + // ==================== Device Connection Status Callbacks ==================== + + /** + * Request QPOS Connection + */ + default void onRequestQposConnected() {} + + /** + * Request QPOS Disconnection + */ + default void onRequestQposDisconnected() {} + + /** + * Request No QPOS Detected + */ + default void onRequestNoQposDetected() {} + +} \ No newline at end of file diff --git a/app/src/main/java/com/test/cardreadtest/posAPI/POSManager.java b/app/src/main/java/com/test/cardreadtest/posAPI/POSManager.java new file mode 100644 index 0000000..f1be0d0 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/posAPI/POSManager.java @@ -0,0 +1,626 @@ +package com.test.cardreadtest.posAPI; + + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.usb.UsbDevice; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.Toast; + +import androidx.core.app.ActivityCompat; + +import com.test.cardreadtest.common.enums.POS_TYPE; +import com.test.cardreadtest.common.enums.TransCardMode; +import com.test.cardreadtest.utils.DeviceUtils; +import com.test.cardreadtest.utils.TRACE; +import com.mulberry.xpos.CQPOSService; +import com.mulberry.xpos.QPOSService; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class POSManager { + private static volatile POSManager instance; + private QPOSService pos; + private Context context; + private QPOSServiceListener listener; + // Callback management + private final List connectionCallbacks = new CopyOnWriteArrayList<>(); + private final List transactionCallbacks = new CopyOnWriteArrayList<>(); + private Handler mainHandler; + private CountDownLatch connectLatch; + private PaymentResult paymentResult; + private POS_TYPE posType; + private boolean isICC; + private SharedPreferences sharedPreferences; + private static final String PREFS_NAME = "POSManagerPrefs"; + private static final String TAG = "POSManager"; + + private POSManager(Context context) { + this.context = context.getApplicationContext(); + this.listener = new QPOSServiceListener(); + this.sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + mainHandler = new Handler(Looper.getMainLooper()); + paymentResult = new PaymentResult(); + Log.d(TAG, "POSManager initialized"); + } + + /** + * Initialize POSManager with application context + * + * @param context Application context + */ + public static void init(Context context) { + getInstance(context); + } + + public static POSManager getInstance(Context context) { + if (instance == null) { + synchronized (POSManager.class) { + if (instance == null) { + instance = new POSManager(context); + Log.d("POSManager", "New POSManager instance created"); + } + } + } + return instance; + } + + /** + * Get singleton instance of POSManager + * + * @return POSManager instance + */ + public static POSManager getInstance() { + if (instance == null) { + throw new IllegalStateException("POS must be initialized with context first"); + } + return instance; + } + + /** + * Connect to POS device + * + * @param deviceAddress Device address (Bluetooth address or USB port) + * @param callback Callback to handle connection events + */ + public void connect(String deviceAddress, ConnectionServiceCallback callback) { + Log.d(TAG, "Connecting to device: " + deviceAddress); + connectLatch = new CountDownLatch(1); + registerConnectionCallback(callback); + + // start connect + connect("UART"); + try { + boolean waitSuccess = connectLatch.await(5, TimeUnit.SECONDS); + if (!waitSuccess) { + Log.w(TAG, "Connection timeout"); + TRACE.i("Connection timeout"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e(TAG, "Connection interrupted", e); + throw new IllegalStateException("Connection interrupted", e); + } + } + + public void connect(String deviceAddress) { + Log.d(TAG, "Starting connection process for: " + deviceAddress); +// if (!deviceAddress.isEmpty()) { +// if (deviceAddress.contains(":")) { +// posType = POS_TYPE.BLUETOOTH; +// Log.d(TAG, "Bluetooth connection detected"); +// initMode(QPOSService.CommunicationMode.BLUETOOTH); +// if (pos != null) { +// pos.setDeviceAddress(deviceAddress); +// pos.connectBluetoothDevice(true, 25, deviceAddress); +// } +// } else { +// posType = POS_TYPE.USB; +// Log.d(TAG, "USB connection detected"); +// initMode(QPOSService.CommunicationMode.USB); +// } +// } else { + posType = POS_TYPE.UART; + Log.d(TAG, "UART connection detected"); + initMode(QPOSService.CommunicationMode.UART); + pos.openUart(); +// +// } + } + + public void initMode(QPOSService.CommunicationMode mode) { + Log.d(TAG, "Initializing POS mode: " + mode); + pos = QPOSService.getInstance(context, mode); + if (pos == null) { + Log.e(TAG, "Failed to get QPOSService instance for mode: " + mode); + return; + } + if (mode == QPOSService.CommunicationMode.USB_OTG_CDC_ACM) { + pos.setUsbSerialDriver(QPOSService.UsbOTGDriver.CDCACM); + } + pos.setContext(context); + pos.initListener(listener); + Log.d(TAG, "POS mode initialized successfully"); + } + + public QPOSService getQPOSService() { + return pos; + } + + public void clearPosService() { + Log.d(TAG, "Clearing POS service"); + pos = null; + } + + public void setICC(boolean ICC) { + isICC = ICC; + Log.d(TAG, "ICC set to: " + ICC); + } + + /** + * Check if device is ready for transaction + * + * @return true if device is connected and ready + */ + public boolean isDeviceReady() { + boolean ready = pos != null; + Log.d(TAG, "isDeviceReady: " + ready); + return ready; + } + + public void setDeviceAddress(String address) { + if (pos != null) { + pos.setDeviceAddress(address); + Log.d(TAG, "Device address set to: " + address); + } + } + + public QPOSService.TransactionType getTransType() { + String transactionTypeString = sharedPreferences.getString("transactionType", ""); + if (transactionTypeString.isEmpty()) { + transactionTypeString = "GOODS"; + Log.d(TAG, "Using default transaction type: GOODS"); + } else { + Log.d(TAG, "Retrieved transaction type: " + transactionTypeString); + } + return QPOSService.TransactionType.GOODS; + } + + public QPOSService.CardTradeMode getCardTradeMode() { + String modeName = sharedPreferences.getString("cardMode", ""); + Log.d(TAG, "Retrieved card mode: " + modeName); + + QPOSService.CardTradeMode cardTradeMode = QPOSService.CardTradeMode.TAP_INSERT_CARD_NOTUP; + if (modeName.isEmpty()) { + if (DeviceUtils.isSmartDevices()) { + cardTradeMode = QPOSService.CardTradeMode.TAP_INSERT_CARD_NOTUP; + Log.d(TAG, "Using SWIPE_TAP_INSERT_CARD_NOTUP for smart device"); + } else { + cardTradeMode = QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD; + Log.d(TAG, "Using SWIPE_TAP_INSERT_CARD for non-smart device"); + } + } else { + Log.d(TAG, "Using configured card mode"); + } + return QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_NOTUP ; + } + + /** + * Start a payment transaction + * + * @param amount Transaction amount + */ + public void startTransaction(String amount, PaymentServiceCallback callback) { + Log.d(TAG, "Starting transaction with amount: " + amount); + if (!isDeviceReady()) { + Log.e(TAG, "Cannot start transaction - device not ready"); + return; + } + getDeviceId(); + if (callback != null) { + registerPaymentCallback(callback); + Log.d(TAG, "Payment callback registered"); + } + + int currencyCode = sharedPreferences.getInt("currencyCode", 156); + Log.d(TAG, "Using currency code: " + currencyCode); + + pos.setCardTradeMode(QPOSService.CardTradeMode.SWIPE_TAP_INSERT_CARD_NOTUP); + pos.setAmount("20", "10", "643", QPOSService.TransactionType.GOODS); + + pos.doTrade(20); // 20-second timeout + +// if (pos != null) { +// pos.setCardTradeMode(getCardTradeMode()); +// pos.setAmount(amount, "", String.valueOf(currencyCode), getTransType()); +// pos.doTrade(60); +// Log.d(TAG, "Transaction started successfully"); +// } else { +// Log.e(TAG, "POS service is null, cannot start transaction"); +// } + } + + public void getDeviceId() { + Log.d(TAG, "Getting device ID"); + if (pos == null) { + Log.e(TAG, "POS service is null, cannot get device ID"); + return; + } + + Hashtable posIdTable = pos.syncGetQposId(5); + String posId = posIdTable.get("posId") == null ? "" : (String) posIdTable.get("posId"); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("posID", posId); + editor.apply(); + + Log.d(TAG, "Device ID retrieved and saved: " + posId); + TRACE.i("posid :" + posId); + } + + /** + * Cancel ongoing transaction + */ + public void cancelTransaction() { + Log.d(TAG, "Cancelling transaction"); + if (pos != null) { + pos.cancelTrade(); + Log.d(TAG, "Transaction cancelled"); + } else { + Log.w(TAG, "Cannot cancel transaction - POS service is null"); + } + } + + public void sendTime(String terminalTime) { + Log.d(TAG, "Sending terminal time: " + terminalTime); + if (pos != null) { + pos.sendTime(terminalTime); + } + } + + public void selectEmvApp(int position) { + Log.d(TAG, "Selecting EMV app at position: " + position); + if (pos != null) { + pos.selectEmvApp(position); + } + } + + public void cancelSelectEmvApp() { + Log.d(TAG, "Cancelling EMV app selection"); + if (pos != null) { + pos.cancelSelectEmvApp(); + } + } + + public void pinMapSync(String value, int timeout) { + Log.d(TAG, "PIN map sync with timeout: " + timeout); + if (pos != null) { + pos.pinMapSync(value, timeout); + } + } + + public void cancelPin() { + Log.d(TAG, "Cancelling PIN input"); + if (pos != null) { + pos.cancelPin(); + } + } + + public boolean isOnlinePin() { + boolean onlinePin = pos != null && pos.isOnlinePin(); + Log.d(TAG, "isOnlinePin: " + onlinePin); + return onlinePin; + } + + public int getCvmPinTryLimit() { + int limit = pos != null ? pos.getCvmPinTryLimit() : 0; + Log.d(TAG, "CVM PIN try limit: " + limit); + return limit; + } + + public void bypassPin() { + Log.d(TAG, "Bypassing PIN"); + if (pos != null) { + pos.sendPin("".getBytes()); + } + } + + public void sendCvmPin(String pinBlock, boolean isEncrypted) { + Log.d(TAG, "Sending CVM PIN, encrypted: " + isEncrypted); + if (pos != null) { + pos.sendCvmPin(pinBlock, isEncrypted); + } + } + + public Hashtable getEncryptData() { + Log.d(TAG, "Getting encrypted data"); + return pos != null ? pos.getEncryptData() : new Hashtable<>(); + } + + public Hashtable getNFCBatchData() { + Log.d(TAG, "Getting NFC batch data"); + return pos != null ? pos.getNFCBatchData() : new Hashtable<>(); + } + + public void sendOnlineProcessResult(String tlv) { + Log.d(TAG, "Sending online process result, TLV length: " + (tlv != null ? tlv.length() : 0)); + if (pos != null) { + pos.sendOnlineProcessResult(tlv); + } + } + + public Hashtable anlysEmvIccData(String tlv) { + Log.d(TAG, "Analyzing EMV ICC data, TLV length: " + (tlv != null ? tlv.length() : 0)); + return pos != null ? pos.anlysEmvIccData(tlv) : new Hashtable<>(); + } + + public void updateDeviceFirmware(Activity activity, String blueTootchAddress) { + Log.d(TAG, "Updating device firmware"); + if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Requesting storage permission"); + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1001); + } else { + Log.d(TAG, "Storage permission already granted"); + } + } + + public void close() { + Log.d(TAG, "Closing POS manager"); + TRACE.d("start close"); + if (pos == null || posType == null) { + Log.d(TAG, "POS service already closed"); + TRACE.d("return close"); + } else if (posType == POS_TYPE.BLUETOOTH) { + pos.disconnectBT(); + Log.d(TAG, "Bluetooth disconnected"); + } else if (posType == POS_TYPE.BLUETOOTH_BLE) { + pos.disconnectBLE(); + Log.d(TAG, "BLE disconnected"); + } else if (posType == POS_TYPE.UART) { + pos.closeUart(); + Log.d(TAG, "UART closed"); + } else if (posType == POS_TYPE.USB) { + pos.closeUsb(); + Log.d(TAG, "USB closed"); + } else { + pos.disconnectBT(); + Log.d(TAG, "Default Bluetooth disconnect"); + } + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("deviceAddress", ""); + editor.apply(); + + clearPosService(); + Log.d(TAG, "POS manager closed successfully"); + } + + /** + * register payment callback + */ + public void registerPaymentCallback(PaymentServiceCallback callback) { + if (callback != null && !transactionCallbacks.contains(callback)) { + transactionCallbacks.add(callback); + Log.d(TAG, "Payment callback registered, total callbacks: " + transactionCallbacks.size()); + } + } + + /** + * register connection service callback + */ + public void registerConnectionCallback(ConnectionServiceCallback callback) { + if (callback != null && !connectionCallbacks.contains(callback)) { + connectionCallbacks.add(callback); + Log.d(TAG, "Connection callback registered, total callbacks: " + connectionCallbacks.size()); + } + } + + public void unregisterCallbacks() { + int connectionCount = connectionCallbacks.size(); + int transactionCount = transactionCallbacks.size(); + connectionCallbacks.clear(); + transactionCallbacks.clear(); + Log.d(TAG, "Callbacks unregistered. Connection: " + connectionCount + ", Transaction: " + transactionCount); + } + + private void notifyConnectionCallbacks(CallbackAction action) { + mainHandler.post(() -> { + Log.d(TAG, "Notifying " + connectionCallbacks.size() + " connection callbacks"); + for (ConnectionServiceCallback callback : connectionCallbacks) { + try { + action.execute(callback); + } catch (Exception e) { + Log.e(TAG, "Error in connection callback: " + e.getMessage()); + TRACE.e("Error in connection callback: " + e.getMessage()); + } + } + }); + } + + private void notifyTransactionCallbacks(CallbackAction action) { + mainHandler.post(() -> { + Log.d(TAG, "Notifying " + transactionCallbacks.size() + " transaction callbacks"); + for (PaymentServiceCallback callback : transactionCallbacks) { + try { + action.execute(callback); + } catch (Exception e) { + Log.e(TAG, "Error in transaction callback: " + e.getMessage()); + TRACE.e("Error in transaction callback: " + e.getMessage()); + } + } + }); + } + + @FunctionalInterface + private interface CallbackAction { + void execute(T callback) throws Exception; + } + + private class QPOSServiceListener extends CQPOSService { + + @Override + public void onRequestQposConnected() { + Log.d(TAG, "onRequestQposConnected"); + connectLatch.countDown(); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean("isConnected", true); + editor.apply(); + + notifyConnectionCallbacks(cb -> cb.onRequestQposConnected()); + } + + @Override + public void onRequestQposDisconnected() { + Log.d(TAG, "onRequestQposDisconnected"); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean("isConnected", false); + editor.apply(); + + clearPosService(); + connectLatch.countDown(); + notifyConnectionCallbacks(cb -> cb.onRequestQposDisconnected()); + } + + @Override + public void onRequestNoQposDetected() { + Log.d(TAG, "onRequestNoQposDetected"); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean("isConnected", false); + editor.apply(); + + clearPosService(); + connectLatch.countDown(); + notifyConnectionCallbacks(cb -> cb.onRequestNoQposDetected()); + } + + @Override + public void onDoTradeResult(QPOSService.DoTradeResult result, Hashtable decodeData) { + + // Handle ICC card for EMV processing + Log.d(TAG, "onDoTradeResult result:" + result); + Log.d(TAG, "onDoTradeResult decodeData:" + decodeData); + setICC(false); + if (result == QPOSService.DoTradeResult.ICC) { + setICC(true); + paymentResult.setTransactionType(result.name()); + if (pos != null) { + pos.doEmvApp(QPOSService.EmvOption.START); + } + } else if (result == QPOSService.DoTradeResult.NFC_OFFLINE || result == QPOSService.DoTradeResult.NFC_ONLINE || result == QPOSService.DoTradeResult.MCR) { + paymentResult.setTransactionType(result.name()); + notifyTransactionCallbacks(cb -> cb.onTransactionCompleted(paymentResult)); + } + } + + @Override + public void onRequestTransactionResult(QPOSService.TransactionResult transactionResult) { + Log.d(TAG, "onRequestTransactionResult: " + transactionResult); + } + + @Override + public void onRequestWaitingUser() { + Log.d(TAG, "onRequestWaitingUser"); + notifyTransactionCallbacks(cb -> cb.onRequestWaitingUser()); + } + + @Override + public void onRequestTime() { + Log.d(TAG, "onRequestTime"); + notifyTransactionCallbacks(cb -> cb.onRequestTime()); + } + + @Override + public void onRequestSelectEmvApp(ArrayList appList) { + Log.d(TAG, "onRequestSelectEmvApp, app count: " + (appList != null ? appList.size() : 0)); + notifyTransactionCallbacks(cb -> cb.onRequestSelectEmvApp(appList)); + } + + @Override + public void onRequestOnlineProcess(String tlv) { + Log.d(TAG, "onRequestOnlineProcess, TLV length: " + (tlv != null ? tlv.length() : 0)); + notifyTransactionCallbacks(cb -> cb.onRequestOnlineProcess(tlv)); + } + + @Override + public void onRequestBatchData(String tlv) { + Log.d(TAG, "onRequestBatchData, TLV length: " + (tlv != null ? tlv.length() : 0)); + paymentResult.setTlv(tlv); + notifyTransactionCallbacks(cb -> cb.onTransactionCompleted(paymentResult)); + } + + @Override + public void onRequestSetPin(boolean isOfflinePin, int tryNum) { + Log.d(TAG, "onRequestSetPin, offline: " + isOfflinePin + ", tryNum: " + tryNum); + notifyTransactionCallbacks(cb -> cb.onRequestSetPin(isOfflinePin, tryNum)); + } + + @Override + public void onRequestDisplay(QPOSService.Display displayMsg) { + Log.d(TAG, "onRequestDisplay: " + displayMsg); + TRACE.i("parent onRequestDisplay"); + } + + @Override + public void onError(QPOSService.Error errorState) { + Log.e(TAG, "onError: " + errorState); + notifyTransactionCallbacks(cb -> cb.onTransactionFailed(errorState.name(), null)); + } + + @Override + public void onReturnReversalData(String tlv) { + Log.d(TAG, "onReturnReversalData, TLV length: " + (tlv != null ? tlv.length() : 0)); + paymentResult.setTlv(tlv); + notifyTransactionCallbacks(cb -> cb.onTransactionCompleted(paymentResult)); + } + + @Override + public void onEmvICCExceptionData(String tlv) { + Log.e(TAG, "onEmvICCExceptionData, TLV length: " + (tlv != null ? tlv.length() : 0)); + notifyTransactionCallbacks(cb -> cb.onTransactionFailed("Decline", tlv)); + } + + @Override + public void onGetCardInfoResult(Hashtable cardInfo) { + Log.d(TAG, "onGetCardInfoResult, card info keys: " + (cardInfo != null ? cardInfo.keySet().size() : 0)); + notifyTransactionCallbacks(cb -> cb.onGetCardInfoResult(cardInfo)); + } + + @Override + public void onRequestSetPin() { + Log.d(TAG, "onRequestSetPin"); + notifyTransactionCallbacks(cb -> cb.onRequestSetPin()); + } + + @Override + public void onReturnGetPinInputResult(int num) { + Log.d(TAG, "onReturnGetPinInputResult: " + num); + notifyTransactionCallbacks(cb -> cb.onReturnGetPinInputResult(num)); + } + + @Override + public void onQposRequestPinResult(List dataList, int offlineTime) { + Log.d(TAG, "onQposRequestPinResult, data count: " + (dataList != null ? dataList.size() : 0) + ", offlineTime: " + offlineTime); + notifyTransactionCallbacks(cb -> cb.onQposRequestPinResult(dataList, offlineTime)); + } + + @Override + public void onTradeCancelled() { + Log.d(TAG, "onTradeCancelled"); + notifyTransactionCallbacks(cb -> cb.onTransactionFailed("Cancel", null)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/test/cardreadtest/posAPI/PaymentResult.java b/app/src/main/java/com/test/cardreadtest/posAPI/PaymentResult.java new file mode 100644 index 0000000..561795a --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/posAPI/PaymentResult.java @@ -0,0 +1,222 @@ +package com.test.cardreadtest.posAPI; + +import java.io.Serializable; + +public class PaymentResult implements Serializable { + private boolean isConnected; + private String status; + private String amount; + private String formatID; + private String maskedPAN; + private String expiryDate; + private String cardHolderName; + private String serviceCode; + private String track1Length; + private String track2Length; + private String track3Length; + private String encTracks; + private String encTrack1; + private String encTrack2; + private String encTrack3; + private String partialTrack; + private String pinKsn; + private String trackksn; + private String pinBlock; + private String encPAN; + private String trackRandomNumber; + private String pinRandomNumber; + private String tlv; + private String transactionType; + + public String getTransactionType() { + return transactionType; + } + + public void setTransactionType(String transactionType) { + this.transactionType = transactionType; + } + + public boolean isConnected() { + return isConnected; + } + + public void setConnected(boolean connected) { + isConnected = connected; + } + + public String getTlv() { + return tlv; + } + + public void setTlv(String tlv) { + this.tlv = tlv; + } + + public String getAmount() { + return amount; + } + + public void setAmount(String amount) { + this.amount = amount; + } + + public String getCardHolderName() { + return cardHolderName; + } + + public void setCardHolderName(String cardHolderName) { + this.cardHolderName = cardHolderName; + } + + public String getEncPAN() { + return encPAN; + } + + public void setEncPAN(String encPAN) { + this.encPAN = encPAN; + } + + public String getEncTrack1() { + return encTrack1; + } + + public void setEncTrack1(String encTrack1) { + this.encTrack1 = encTrack1; + } + + public String getEncTrack2() { + return encTrack2; + } + + public void setEncTrack2(String encTrack2) { + this.encTrack2 = encTrack2; + } + + public String getEncTrack3() { + return encTrack3; + } + + public void setEncTrack3(String encTrack3) { + this.encTrack3 = encTrack3; + } + + public String getEncTracks() { + return encTracks; + } + + public void setEncTracks(String encTracks) { + this.encTracks = encTracks; + } + + public String getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(String expiryDate) { + this.expiryDate = expiryDate; + } + + public String getFormatID() { + return formatID; + } + + public void setFormatID(String formatID) { + this.formatID = formatID; + } + + public String getMaskedPAN() { + return maskedPAN; + } + + public void setMaskedPAN(String maskedPAN) { + this.maskedPAN = maskedPAN; + } + + public String getPartialTrack() { + return partialTrack; + } + + public void setPartialTrack(String partialTrack) { + this.partialTrack = partialTrack; + } + + public String getPinBlock() { + return pinBlock; + } + + public void setPinBlock(String pinBlock) { + this.pinBlock = pinBlock; + } + + public String getPinKsn() { + return pinKsn; + } + + public void setPinKsn(String pinKsn) { + this.pinKsn = pinKsn; + } + + public String getPinRandomNumber() { + return pinRandomNumber; + } + + public void setPinRandomNumber(String pinRandomNumber) { + this.pinRandomNumber = pinRandomNumber; + } + + public String getServiceCode() { + return serviceCode; + } + + public void setServiceCode(String serviceCode) { + this.serviceCode = serviceCode; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getTrack1Length() { + return track1Length; + } + + public void setTrack1Length(String track1Length) { + this.track1Length = track1Length; + } + + public String getTrack2Length() { + return track2Length; + } + + public void setTrack2Length(String track2Length) { + this.track2Length = track2Length; + } + + public String getTrack3Length() { + return track3Length; + } + + public void setTrack3Length(String track3Length) { + this.track3Length = track3Length; + } + + public String getTrackksn() { + return trackksn; + } + + public void setTrackksn(String trackksn) { + this.trackksn = trackksn; + } + + public String getTrackRandomNumber() { + return trackRandomNumber; + } + + public void setTrackRandomNumber(String trackRandomNumber) { + this.trackRandomNumber = trackRandomNumber; + } +} diff --git a/app/src/main/java/com/test/cardreadtest/posAPI/PaymentServiceCallback.java b/app/src/main/java/com/test/cardreadtest/posAPI/PaymentServiceCallback.java new file mode 100644 index 0000000..870cdc8 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/posAPI/PaymentServiceCallback.java @@ -0,0 +1,72 @@ +package com.test.cardreadtest.posAPI; + +import com.mulberry.xpos.QPOSService; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +/** + * Payment Service Callback Interface + * Handle all transaction-related callback methods + */ +public interface PaymentServiceCallback { + + // ==================== Core Transaction Callbacks ==================== + + /** + * Waiting for user operation (insert/swipe/tap card) + */ + default void onRequestWaitingUser() {} + + /** + * Request time + */ + default void onRequestTime() {} + + /** + * Request to select EMV application + */ + default void onRequestSelectEmvApp(ArrayList appList) {} + + /** + * Request online processing + */ + default void onRequestOnlineProcess(String tlv) {} + + /** + * Request to display message + */ + default void onRequestDisplay(QPOSService.Display displayMsg) {} + + // ==================== PIN Related Callbacks ==================== + + /** + * PIN request result + */ + default void onQposRequestPinResult(List dataList, int offlineTime) {} + + /** + * Request to set PIN + */ + default void onRequestSetPin(boolean isOfflinePin, int tryNum) {} + + /** + * Request to set PIN (no parameters) + */ + default void onRequestSetPin() {} + + /** + * Return PIN input result + */ + default void onReturnGetPinInputResult(int num) {} + + /** + * Get card information result + */ + default void onGetCardInfoResult(Hashtable cardInfo) {} + + default void onTransactionCompleted(PaymentResult result) {} + default void onTransactionFailed(String errorMessage,String data) {} + +} diff --git a/app/src/main/java/com/test/cardreadtest/utils/DevUtils.java b/app/src/main/java/com/test/cardreadtest/utils/DevUtils.java new file mode 100644 index 0000000..3aecb55 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/utils/DevUtils.java @@ -0,0 +1,120 @@ +package com.test.cardreadtest.utils; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.provider.Settings; +import android.text.TextUtils; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.UUID; + +public class DevUtils { + /** + * Gets the version number + * + * @return The version number of the current app + */ + public static String getPackageVersionName(Context context, String pkgName) { + try { + PackageManager manager = context.getPackageManager(); + PackageInfo info = manager.getPackageInfo(pkgName, 0); + //PackageManager.GET_CONFIGURATIONS + return info.versionName; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Get process name + */ + public static String getProcessName(int pid) { + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader("/proc/" + pid + "/cmdline")); + String processName = reader.readLine(); + if (!TextUtils.isEmpty(processName)) { + processName = processName.trim(); + } + return processName; + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; + } + + /** + * Obtain the unique identifier of the device + */ + public static String getDeviceId(Context context) { + String deviceId = ""; + try { + // Obtain the device serial number + String serial = Build.SERIAL; + // Obtain ANDROID_ID + String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + // Obtain device hardware information + String hardware = Build.HARDWARE; + String model = Build.MODEL; + // Obtain device fingerprint + String fingerprint = Build.FINGERPRINT; + String country = DeviceUtils.getDevieCountry(context); + String time = new SimpleDateFormat("yyMMddHHmmss").format(Calendar.getInstance().getTime()); + + // Generate a unique ID by combining device information + deviceId = model + "-"+hardware + "-"+ country + "-"+ time; + } catch (Exception e) { + e.printStackTrace(); + // If the acquisition fails, use UUID as an alternative solution + deviceId = UUID.randomUUID().toString(); + } + return deviceId; + } + + /** + * SHA256 encryption + */ + private static String SHA256(String str) { + MessageDigest messageDigest; + String encodeStr = ""; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(str.getBytes("UTF-8")); + encodeStr = byte2Hex(messageDigest.digest()); + } catch (Exception e) { + e.printStackTrace(); + } + return encodeStr; + } + + /** + * Convert bytes to hex + */ + private static String byte2Hex(byte[] bytes) { + StringBuilder stringBuilder = new StringBuilder(); + for (byte b : bytes) { + String temp = Integer.toHexString(b & 0xFF); + if (temp.length() == 1) { + stringBuilder.append("0"); + } + stringBuilder.append(temp); + } + return stringBuilder.toString(); + } +} diff --git a/app/src/main/java/com/test/cardreadtest/utils/DeviceUtils.java b/app/src/main/java/com/test/cardreadtest/utils/DeviceUtils.java new file mode 100644 index 0000000..5afaf07 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/utils/DeviceUtils.java @@ -0,0 +1,200 @@ +package com.test.cardreadtest.utils; + +import android.app.Application; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.provider.Settings; +import android.telephony.TelephonyManager; + +import androidx.annotation.RequiresApi; + +import com.test.cardreadtest.common.enums.POS_TYPE; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * [Describe the functionality of this class in one sentence] + * + * @author : [DH] + * @createTime : [2024/9/3 10:43] + * @updateRemark : [Explain the content of this modification] + */ +public class DeviceUtils { + + /** + * Get the current mobile system language。 + * + * @return Return the current system language. For example, if the current setting is "Chinese-China", return "zh-CN" + */ + public static String getSystemLanguage() { + return Locale.getDefault().getLanguage(); + } + + /** + * Retrieve the list of languages (Locale list) on the current system + * + * @return Lists of languages + */ + public static Locale[] getSystemLanguageList() { + return Locale.getAvailableLocales(); + } + + /** + * obtain androidId + * + * @return + */ + public static String getAndroidId(Context context) { + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + /** + * Is the camera available + * + * @return + */ + public static boolean isSupportCamera(Context context) { + PackageManager packageManager = context.getPackageManager(); + return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } + + /** + * Obtain mobile phone manufacturers + * HuaWei + * + * @return Mobile phone manufacturers + */ + public static String getPhoneBrand() { + return Build.BRAND; + } + + /** + * Get phone model + * + * @return Mobile phone model + */ + public static String getPhoneModel() { + return Build.MODEL; + } + + /** + * Get the current mobile system version number + * Android 10 + * + * @return System Version Number + */ + public static String getVersionRelease() { + return Build.VERSION.RELEASE; + } + + /** + * Get the current mobile device name + * Unified device model, not the device name in 'About Mobile' + * + * @return device name + */ + public static String getDeviceName() { + return Build.DEVICE; + } + + /** + * HUAWEI HWELE ELE-AL00 10 + * + * @return + */ + public static String getPhoneDetail() { + return "Brand:" + DeviceUtils.getPhoneBrand() + " || Name:" + DeviceUtils.getDeviceName() + " || Model:" + DeviceUtils.getPhoneModel() + " || Version:" + DeviceUtils.getVersionRelease(); + } + + /** + * Get the name of the phone motherboard + * + * @return Motherboard name + */ + public static String getDeviceBoard() { + return Build.BOARD; + } + + + public static boolean isSmartDevices() { + + if ("D20".equals(Build.MODEL) || "D30".equals(Build.MODEL) || "D50".equals(Build.MODEL) || "D60".equals(Build.MODEL) + || "D70".equals(Build.MODEL) || "D30M".equals(Build.MODEL) || "S10".equals(Build.MODEL) + || "D80".equals(Build.MODEL) || "D80K".equals(Build.MODEL) || "M60".equals(Build.MODEL) || "M20".equals(Build.MODEL) || "M70".equals(Build.MODEL)) { + return true; + } + return false; + } + + public static boolean isPrinterDevices() { + if ("D30".equals(Build.MODEL) || "D60".equals(Build.MODEL) + || "D70".equals(Build.MODEL) || "D30M".equals(Build.MODEL) || "D80".equals(Build.MODEL) || "D80K".equals(Build.MODEL) || "M60".equals(Build.MODEL) || "M20".equals(Build.MODEL) || "M70".equals(Build.MODEL)) { + return true; + } + return false; + } + + /** + * Obtain the name of the mobile phone manufacturer + * HuaWei + * + * @return Mobile phone manufacturer name + */ + public static String getDeviceManufacturer() { + return Build.MANUFACTURER; + } + + public static String getDevieCountry(Context context) { + TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + String code = telephonyManager.getNetworkCountryIso(); + return code; + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public static Context getGlobalApplicationContext() { + try { + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); + currentActivityThreadMethod.setAccessible(true); + Object activityThread = currentActivityThreadMethod.invoke(null); + Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); + mInitialApplicationField.setAccessible(true); + Application application = (Application) mInitialApplicationField.get(activityThread); + return application.getApplicationContext(); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException | NoSuchFieldException e) { + e.printStackTrace(); + } + return null; + } + + public static boolean isAppInstalled(Context context, String packageName) { + PackageManager packageManager = context.getPackageManager(); + try { + packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); + TRACE.d("[PrinterManager] isAppInstalled "); + return true; + } catch (PackageManager.NameNotFoundException e) { + TRACE.d("not found pacakge == " + e.toString()); + return false; + } + } + + public static POS_TYPE getDevicePosType(String deviceTypeName) { + if (deviceTypeName.equals(POS_TYPE.UART.name())) { + return POS_TYPE.UART; + } else if (deviceTypeName.equals(POS_TYPE.USB.name())) { + return POS_TYPE.USB; + } else if (deviceTypeName.equals(POS_TYPE.BLUETOOTH.name())) { + return POS_TYPE.BLUETOOTH; + } + return POS_TYPE.BLUETOOTH; + } + + public static final String UART_AIDL_SERVICE_APP_PACKAGE_NAME = "com.dspread.sdkservice";//新架构的service包名 + +} diff --git a/app/src/main/java/com/test/cardreadtest/utils/QPOSUtil.java b/app/src/main/java/com/test/cardreadtest/utils/QPOSUtil.java new file mode 100644 index 0000000..007cdb4 --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/utils/QPOSUtil.java @@ -0,0 +1,413 @@ +package com.test.cardreadtest.utils; + +import android.content.Context; +import android.media.AudioManager; + +import com.mulberry.xpos.Util; +import com.mulberry.xpos.utils.AESUtil; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.Hashtable; + + +public class QPOSUtil { + static final String HEXES = "0123456789ABCDEF"; + + public static String byteArray2Hex(byte[] raw) { + if (raw == null) { + return null; + } + final StringBuilder hex = new StringBuilder(2 * raw.length); + for (final byte b : raw) { + hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))); + } + return hex.toString(); + } + + //根据n和e获取公钥 + public static RSAPublicKey getPublicKey(String modulus, String publicExponent) { + BigInteger m = new BigInteger(modulus, 16); + BigInteger e = new BigInteger(publicExponent, 16); + + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(m, e); + + KeyFactory keyFactory; + RSAPublicKey publicKey = null; + try { + keyFactory = KeyFactory.getInstance("RSA"); + publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec); + } catch (Exception e1) { + e1.printStackTrace(); + } + return publicKey; + } + + /* + * Convert hex value to ascii code + **/ + public static String convertHexToString(String hex) { + + StringBuilder sb = new StringBuilder(); + StringBuilder temp = new StringBuilder(); + + //49204c6f7665204a617661 split into two characters 49, 20, 4c... + for (int i = 0; i < hex.length() - 1; i += 2) { + + //grab the hex in pairs + String output = hex.substring(i, (i + 2)); + //convert hex to decimal + int decimal = Integer.parseInt(output, 16); + //convert the decimal to character + sb.append((char) decimal); + + temp.append(decimal); + } + + return sb.toString(); + } + + /** + * convert int to byte[] + * @param i need to be converted to byte array + * @return byte array + */ + public static byte[] intToByteArray(int i) { + byte[] result = new byte[2]; +// result[0] = (byte)((i >> 24) & 0xFF); +// result[1] = (byte)((i >> 16) & 0xFF); + result[0] = (byte)((i >> 8) & 0xFF); + result[1] = (byte)(i & 0xFF); + return result; + } + + //16 byte xor + public static String xor16(byte[] src1, byte[] src2){ + byte[] results = new byte[16]; + for (int i = 0; i < results.length; i++){ + results[i] = (byte)(src1[i] ^ src2[i]); + } + return QPOSUtil.byteArray2Hex(results); + } + + /** + * convert a string in hexadecimal format to byte in hexadecimal format 44 --> byte 0x44 + * + * @param hexString + * @return + */ + public static byte[] HexStringToByteArray(String hexString) {// + if (hexString == null || hexString.equals("")) { + return new byte[]{}; + } + if (hexString.length() == 1 || hexString.length() % 2 != 0) { + hexString = "0" + hexString; + } + hexString = hexString.toUpperCase(); + int length = hexString.length() / 2; + char[] hexChars = hexString.toCharArray(); + byte[] d = new byte[length]; + for (int i = 0; i < length; i++) { + int pos = i * 2; + d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); + } + return d; + } + + private static byte charToByte(char c) { + return (byte) "0123456789ABCDEF".indexOf(c); + } + + /** + * convert Chinese string into hexadecimal array + * + * @param str + * @return + */ + public static byte[] CNToHex(String str) { + // String string = ""; + // for (int i = 0; i < str.length(); i++) { + // String s = String.valueOf(str.charAt(i)); + // byte[] bytes = null; + // try { + // bytes = s.getBytes("gbk"); + // } catch (UnsupportedEncodingException e) { + // e.printStackTrace(); + // } + // for (int j = 0; j < bytes.length; j++) { + // string += Integer.toHexString(bytes[j] & 0xff); + // } + // } + byte[] b = null; + try { + b = str.getBytes("GBK"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return b; + } + + //Low position ahead + public static byte[] intToBytes( int value ) + { + byte[] src = new byte[2]; + src[1] = (byte) ((value>>8) & 0xFF); + src[0] = (byte) (value & 0xFF); + return src; + } + + public static String intToHex2(int i) { + String string = null; + if (i >= 0 && i < 10) { + string = "0" + i; + } else { + string = Integer.toHexString(i); + } + if(string.length() == 2){ + string = "00" + string; + }else if (string.length() == 1) { + string = "000" + string; + }else if(string.length() == 3){ + string = "0" + string; + } + return string; + } + + /** + * convert byte to hexadecimal string + * + * @param b + * @return + */ + public static String getHexString(byte[] b) { + StringBuffer result = new StringBuffer(""); + for (int i = 0; i < b.length; i++) { + result.append("0x" + Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1) + ","); + } + return result.substring(0, result.length() - 1); + } + + /** + * convert int to hexadecimal byte + * + * @param i + * @return + */ + public static byte[] IntToHex(int i) { + String string = null; + if (i >= 0 && i < 10) { + string = "0" + i; + } else { + string = Integer.toHexString(i); + } + return HexStringToByteArray(string); + } + + /** + * convert the specified byte array to hexadecimal and print + * + * @param b + */ + public static void printHexString(byte[] b) { + for (int i = 0; i < b.length; i++) { + String hex = Integer.toHexString(b[i] & 0xFF); + if (hex.length() == 1) { + hex = '0' + hex; + } + System.out.print(hex.toUpperCase()); + } + + } + + /** + * convert hexadecimal byte to int + * + * @param b + * @return + */ + public static int byteArrayToInt(byte[] b) { + int result = 0; + for (int i = 0; i < b.length; i++) { + result <<= 8; + result |= (b[i] & 0xff); // + } + return result; + } + + /** + * XOR input byte stream + * + * @param b + * @param startPos + * @param Len + * @return + */ + public static byte XorByteStream(byte[] b, int startPos, int Len) { + byte bRet = 0x00; + for (int i = 0; i < Len; i++) { + bRet ^= b[startPos + i]; + } + return bRet; + } + + /** + * Gets the subarray from array that starts at offset. + */ + public static byte[] get(byte[] array, int offset) { + return get(array, offset, array.length - offset); + } + + /** + * Gets the subarray of length length from array that + * starts at offset. + */ + public static byte[] get(byte[] array, int offset, int length) { + byte[] result = new byte[length]; + System.arraycopy(array, offset, result, 0, length); + return result; + } + + public static void turnUpVolume(Context context, int factor) { + int sv; + + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + sv = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, sv * factor / 10, AudioManager.FLAG_PLAY_SOUND); + } + + + public static byte[] bcd2asc(byte[] src) { + byte[] results = new byte[src.length * 2]; + for (int i = 0; i < src.length; i++) { + // high Nibble conversion + if (((src[i] & 0xF0) >> 4) <= 9) { + results[2 * i] = (byte) (((src[i] & 0xF0) >> 4) + 0x30); + } else { + results[2 * i] = (byte) (((src[i] & 0xF0) >> 4) + 0x37); // 大写A~F + } + // low Nibble conversion + if ((src[i] & 0x0F) <= 9) { + results[2 * i + 1] = (byte) ((src[i] & 0x0F) + 0x30); + } else { + results[2 * i + 1] = (byte) ((src[i] & 0x0F) + 0x37); // 大写A~F + } + } + return results; + } + + public static byte[] ecb(byte[] in) { + + byte[] a1 = new byte[8]; + + for (int i = 0; i < (in.length / 8); i++) { + byte[] temp = new byte[8]; + System.arraycopy(in, i * 8, temp, 0, temp.length); + a1 = xor8(a1, temp); + } + if ((in.length % 8) != 0) { + byte[] temp = new byte[8]; + System.arraycopy(in, (in.length / 8) * 8, temp, 0, in.length - (in.length / 8) * 8); + a1 = xor8(a1, temp); + } + return bcd2asc(a1); + } + + public static byte[] xor8(byte[] src1, byte[] src2) { + byte[] results = new byte[8]; + for (int i = 0; i < results.length; i++) { + results[i] = (byte) (src1[i] ^ src2[i]); + } + return results; + } + + + public static boolean checkStringAllZero(String str) { + if (str.startsWith("0")) + return true; + boolean result = true; +// Integer.MAX_VALUE 4 bytes +// long MAX_VALUE = 0x7fffffffffffffffL; + int byteCou = str.length() / 2; + int count; + if (byteCou % 4 == 0) { + count = byteCou / 4; + } else { + count = byteCou / 4 + 1; + } + String sub = null; + for (int i = 0; i < count; i++) { + if (i == count - 1) { + sub = str.substring(i * 8, sub.length()); + } else { + sub = str.substring(i * 8, (i + 1) * 8); + } + long l = Long.parseLong(sub, 16); + if (l > 0) { + result = false; + break; + } + } + return result; + } + + public static String readRSANStream(InputStream in) throws Exception { + try { + BufferedReader br = new BufferedReader(new InputStreamReader(in,"UTF-8")); + String line = null; + StringBuilder sb = new StringBuilder(); + + while ((line = br.readLine()) != null) { + sb.append(line); + sb.append('\r'); + } + return sb.toString(); + } catch (IOException var5) { + throw new Exception("鍏\ue104挜鏁版嵁娴佽\ue1f0鍙栭敊锟�?"); + } catch (NullPointerException var6) { + throw new Exception("鍏\ue104挜杈撳叆娴佷负锟�?"); + } + } + + public static String buildCvmPinBlock(Hashtable value, String pin) { + String randomData = value.get("RandomData") == null ? "" : value.get("RandomData"); + String pan = value.get("PAN") == null ? "" : value.get("PAN"); + String AESKey = value.get("AESKey") == null ? "" : value.get("AESKey"); + String isOnline = value.get("isOnlinePin") == null ? "" : value.get("isOnlinePin"); + String pinTryLimit = value.get("pinTryLimit") == null ? "" : value.get("pinTryLimit"); + //iso-format4 pinblock + int pinLen = pin.length(); + pin = "4" + Integer.toHexString(pinLen) + pin; + for (int i = 0; i < 14 - pinLen; i++) { + pin = pin + "A"; + } + pin += randomData.substring(0, 16); + String panBlock = ""; + int panLen = pan.length(); + int m = 0; + if (panLen < 12) { + panBlock = "0"; + for (int i = 0; i < 12 - panLen; i++) { + panBlock += "0"; + } + panBlock = panBlock + pan + "0000000000000000000"; + } else { + m = pan.length() - 12; + panBlock = m + pan; + for (int i = 0; i < 31 - panLen; i++) { + panBlock += "0"; + } + } + String pinBlock1 = AESUtil.encrypt(AESKey, pin); + pin = Util.xor16(HexStringToByteArray(pinBlock1), HexStringToByteArray(panBlock)); + String pinBlock2 = AESUtil.encrypt(AESKey, pin); + return pinBlock2; + } + +} diff --git a/app/src/main/java/com/test/cardreadtest/utils/TLV.java b/app/src/main/java/com/test/cardreadtest/utils/TLV.java new file mode 100644 index 0000000..9512f8d --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/utils/TLV.java @@ -0,0 +1,23 @@ +package com.test.cardreadtest.utils; + +import java.util.List; + +public class TLV { + + public String tag; + public String length; + public String value; + + public boolean isNested; + public List tlvList; + + @Override + public String toString() { + return "TLV{" + + "tag='" + tag + '\'' + + ", length='" + length + '\'' + + ", value='" + value + '\'' + + ", isNested=" + isNested + + '}'; + } +} diff --git a/app/src/main/java/com/test/cardreadtest/utils/TLVParser.java b/app/src/main/java/com/test/cardreadtest/utils/TLVParser.java new file mode 100644 index 0000000..82b913b --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/utils/TLVParser.java @@ -0,0 +1,284 @@ +package com.test.cardreadtest.utils; + +import java.util.ArrayList; +import java.util.List; + + + + + +/* +* +* tlv:5F200D202020202020202020202020204F08A0000003330101015F24032412319F160F4243544553543132333435363738009F21031142259A031808039F02060000000011119F03060000000000009F34034203009F120A50424F432044454249549F0607A00000033301015F300202209F4E0F616263640000000000000000000000C40A621067FFFFFFFFF0474FC10A00000332100300E0008BC708B04EFFA147D84FB4C00A00000332100300E00074C28201983ABB68B0A87865BFCAC1FCD6D2794C9C293A667EA2E0DF8FE08658105DF18EE870CDE7714573245EF4F1509F4F7DD2D8AA3A0700570556BB30C5BB3AA0D95C26B9A7A1A0FE45CCCF939D7587D3DBDF3D1D96722F7F9F8C1E0077C89BA4D4D267F74A60CF65E1D66F62685B6E41C25BDAEA4F353EBF9021195824842693CB76733CDEBFC61C6E75F9A87DBB33181C301074FDD300028A1037B8372CE0943EFBA84C82D2448DD142941895136A46CF65F84DFC6A792F502D556DA84106584AFDE8A0838B45E8E1BDAE9747FDF91C10E9D7BC9C5EE15CF0A1746ADDB8F7CB96EA672B127B19FF06A733509B5A04F5BF31D1678C2E5951CABE67E34E97AD946B4DACF3CA500188625890BCA60D7D29A63ED9F6CAEE3369C4E5DC9C2F890200FF24986DD6931BB13FC145D46B1961888B9317263C22351F98796A4FF75CF2262797535D54FD7B58F24535286C3A0EFA9524EE642EB6818EED427F8A447244A883E73FB36AAFB72B2C8EF0829E086CC87E6005E3CBE4C7E3A79CBF339320342B547C4E6D256BB98F78FE9E9A5434EF4CAB734093CD0329667FF2FA + +* +* */ +public class TLVParser { + + + private static ArrayList tlvList = new ArrayList(); + + public static List parse(String tlv) { + try { + tlvList.clear(); + return getTLVList(hexToByteArray(tlv)); + } catch (Exception e) { + if (tlvList.size() > 0) + return tlvList; + return null; + } + } + + private static List getTLVList(byte[] data) { + int index = 0; + + byte[] tag; + byte[] length; + byte[] value; + boolean isNested; + TLV tlv = null; + while (index < data.length) { + + isNested = false; + + if ((data[index] & (byte) 0x20) == (byte) (0x20)) { + isNested = true; + //Composite structure + } else { + isNested = false; + } + + if ((data[index] & (byte) 0x1F) == (byte) (0x1F)) { + int lastByte = index + 1; + while ((data[lastByte] & (byte) 0x80) == (byte) 0x80) { + ++lastByte; + } + tag = new byte[lastByte - index + 1]; + System.arraycopy(data, index, tag, 0, tag.length); + index += tag.length; + } else { + tag = new byte[1]; + tag[0] = data[index]; + ++index; + + if (tag[0] == 0x00) { + break; + } + } + + if ((data[index] & (byte) 0x80) == (byte) (0x80)) { + int n = (data[index] & (byte) 0x7F) + 1; + length = new byte[n]; + System.arraycopy(data, index, length, 0, length.length); + index += length.length; + } else { + length = new byte[1]; + length[0] = data[index]; + ++index; + } + + int n = getLengthInt(length); + value = new byte[n]; + System.arraycopy(data, index, value, 0, value.length); + index += value.length; + if (isNested) { + getTLVList(value); + }else { + tlv = new TLV(); + tlv.tag = toHexString(tag); + tlv.length = toHexString(length); + tlv.value = toHexString(value); + tlv.isNested = isNested; + tlvList.add(tlv); + } + } + return tlvList; + } + + public static List parseWithoutValue(String tlv) { + try { + return getTLVListWithoutValue(hexToByteArray(tlv)); + } catch (Exception e) { + return null; + } + } + + private static List getTLVListWithoutValue(byte[] data) { + int index = 0; + + ArrayList tlvList = new ArrayList(); + + while (index < data.length) { + + byte[] tag; + byte[] length; + + boolean isNested; + if ((data[index] & (byte) 0x20) == (byte) (0x20)) { + isNested = true; + } else { + isNested = false; + } + + if ((data[index] & (byte) 0x1F) == (byte) (0x1F)) { + int lastByte = index + 1; + while ((data[lastByte] & (byte) 0x80) == (byte) 0x80) { + ++lastByte; + } + tag = new byte[lastByte - index + 1]; + System.arraycopy(data, index, tag, 0, tag.length); + index += tag.length; + } else { + tag = new byte[1]; + tag[0] = data[index]; + ++index; + + if (tag[0] == 0x00) { + break; + } + } + + if ((data[index] & (byte) 0x80) == (byte) (0x80)) { + int n = (data[index] & (byte) 0x7F) + 1; + length = new byte[n]; + System.arraycopy(data, index, length, 0, length.length); + index += length.length; + } else { + length = new byte[1]; + length[0] = data[index]; + ++index; + } + + TLV tlv = new TLV(); + tlv.tag = toHexString(tag); + tlv.length = toHexString(length); + tlv.isNested = isNested; + + tlvList.add(tlv); + } + return tlvList; + } + + private static int getLengthInt(byte[] data) { + if ((data[0] & (byte) 0x80) == (byte) (0x80)) { + int n = data[0] & (byte) 0x7F; + int length = 0; + for (int i = 1; i < n + 1; ++i) { + length <<= 8; + length |= (data[i] & 0xFF); + } + return length; + } else { + return data[0] & 0xFF; + } + } + + + + // Hexadecimal string to byte array conversion + public static byte[] hexToByteArray(String hexStr) { + if (hexStr.length() < 1) + return null; + byte[] result = new byte[hexStr.length() / 2]; + for (int i = 0; i < hexStr.length() / 2; i++) { + int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16); + int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), + 16); + result[i] = (byte) (high * 16 + low); + } + return result; + } + + protected static String toHexString(byte[] b) { + String result = ""; + for (int i = 0; i < b.length; i++) { + result += Integer.toString((b[i] & 0xFF) + 0x100, 16).substring(1); + } + return result; + } + + public static TLV searchTLV(List tlvList, String targetTag) { + for (int i = 0; i < tlvList.size(); ++i) { + TLV tlv = tlvList.get(i); + if (tlv.tag.equalsIgnoreCase(targetTag)) { + return tlv; + } else if (tlv.isNested) { + TLV searchChild = searchTLV(tlv.tlvList, targetTag); + if (searchChild != null) { + return searchChild; + } + } + } + return null; + } + + + public static void main(String[] args) { +// String tlv = "5F201A5052415645454E204B554D41522042204E20202020202020202F4F07A00000000310105F24032311309F160F4243544553543132333435363738009F21031244089A031907109F02060000000000059F03060000000000009F34034203009F120A564953412044454249549F0607A00000000310105F300202269F4E0F616263640000000000000000000000C408414367FFFFFF0912C10A10218083100492E0000CC70836D3E567845F788FC00A10218083100492E0000CC2820198BBA22DE72324CD77FBFE7BCA8343BC2F26719BBC1F4633FB0E10329E35018CB35077D634CD3A84F998F52DFAC4F0442E2CD03A85D89BFF630D8A85727132E12C88664FBE5A664BB8AA21FF0D10A2D79E324D87B4225A5B9AAC68BD1FFCF5DD334B38D128B02E983DBBD32EC35DBE26CFFA01C11C272F99D8095107DE981818534873828880F1091B8BC62FD39C8394B19E7A410CF9C870CF27986D0CB251E0B6B2D364DE7F3EF1453B397B9FD2D181668510BA16DE250BEC7C1C6A3C12F7006B6B7660D7B331D326D2EA4990F899B4D11AC17D3C0FF63AEF482A349CD8849D906F60B320832E41D8349316E55DE764F8C0AF6ACE3AACA43B3994536A231BE2E790471EB559F4B9FAA5370067B7A0EA3FE59421B7AC17FA5383C6BB3159EBDE3718FEC72CC20EC1AE178386B4F7B3948C97A439AB0F70A386B392276B9B30D8398BAFE3D01AEAB03079368EEF05248E5FAE7BAB070E527981BB25F441A9224AC66DAE623BECDD9B0D1BB05A6EBCAE1E9151FB7AE3E5034B57BD6C3D609276B7743176179A801AD1B378B4629D08263148859ADDE1687CB5E9D0104D84851E5733F4C95D71E880EF20607C"; + String tlv = "9F0610000000000000000000000000000000005F2A0204805F3601009F01061234567890129F150212349F160F3131323233333434353536363737389F1A0204809F1C0831323334353637389F1E0831313232333334349F3303E0D8C89F3501219F3901079F40057000B0A0019F4E0F3131323233333434353536363737389F5301529F811701019F811801009F814301019F814501019F81470100D5020001"; + List parse = parse(tlv); + for (TLV tlvcon:parse) { + System.out.println(tlvcon.toString()); + if(tlvcon.tag.equals("d5")){ + System.out.println("9a is "+tlvcon.value); + } + } + System.out.println(searchTLV(parse,"d5")); + + } + + /* + * + * Verify TLV format + * Only take the first tlv for judgment. Once 0 is encountered, it means the end + * tlv is true + * tv is false + * */ + public static boolean VerifyTLV(String emvCfg) { + + if (emvCfg.startsWith("9F06")) + return true; + if (emvCfg.startsWith("00")) + return false; + byte[] data = hexToByteArray(emvCfg); + int index = 0; + byte[] length; + + + if ((data[index] & (byte) 0x20) == (byte) (0x20)) { + return false; + } + + if ((data[index] & (byte) 0x1F) == (byte) (0x1F)) { + int lastByte = index + 1; + while ((data[lastByte] & (byte) 0x80) == (byte) 0x80) { + ++lastByte; + } + index += lastByte - index + 1; + if (index >= data.length) + return false; + + } else { + if (data[index] == 0x00) { + return false; + } + ++index; + } + + if ((data[index] & (byte) 0x80) == (byte) (0x80)) { + int n = (data[index] & (byte) 0x7F) + 1; + length = new byte[n]; + index += length.length; + } else { + length = new byte[1]; + length[0] = data[index]; + ++index; + } + + int n = getLengthInt(length); + if ((n + index) > data.length) + return false; + + return true; + } +} diff --git a/app/src/main/java/com/test/cardreadtest/utils/TRACE.java b/app/src/main/java/com/test/cardreadtest/utils/TRACE.java new file mode 100644 index 0000000..48ca14d --- /dev/null +++ b/app/src/main/java/com/test/cardreadtest/utils/TRACE.java @@ -0,0 +1,59 @@ +package com.test.cardreadtest.utils; + +import android.content.Context; +import android.util.Log; + +public class TRACE { + public static String NEW_LINE = System.getProperty("line.separator"); + + private static String AppName = "POS_LOG"; + private static Boolean isTesting = true; + private static Context mContext; + + public static void setContext(Context context){ + mContext = context; + } + + public static void v(String string) { + if (isTesting) { + Log.v(AppName, string); +// Sentry.captureMessage(string); + + } + } + + public static void i(String string) { + if (isTesting) { + Log.i(AppName, string); +// Sentry.captureMessage(string); + + } + } + + public static void w(String string) { + if (isTesting) { + Log.w(AppName, string); +// Sentry.captureMessage(string); + + } + } + + public static void e(Exception exception) { + if (isTesting) { + Log.e(AppName, exception.toString()); + } + } + + public static void e(String exception) { + if (isTesting) { + Log.e(AppName, exception); + } + } + + public static void d(String string) { + if (isTesting) { + Log.d(AppName, string); + } + } + } + diff --git a/app/src/main/jniLibs/arm64-v8a/libserial_port_pos.so b/app/src/main/jniLibs/arm64-v8a/libserial_port_pos.so new file mode 100644 index 0000000..dedc326 Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libserial_port_pos.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libserial_port_pos.so b/app/src/main/jniLibs/armeabi-v7a/libserial_port_pos.so new file mode 100644 index 0000000..1a4865a Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libserial_port_pos.so differ diff --git a/app/src/main/jniLibs/x86/libserial_port_pos.so b/app/src/main/jniLibs/x86/libserial_port_pos.so new file mode 100644 index 0000000..36e2b36 Binary files /dev/null and b/app/src/main/jniLibs/x86/libserial_port_pos.so differ diff --git a/app/src/main/jniLibs/x86_64/libserial_port_pos.so b/app/src/main/jniLibs/x86_64/libserial_port_pos.so new file mode 100644 index 0000000..a3586b9 Binary files /dev/null and b/app/src/main/jniLibs/x86_64/libserial_port_pos.so differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..32361a4 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,17 @@ + + + +