first commit
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="ms-17" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="ms-17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.CardReadTest"
|
||||||
|
tools:targetApi="31"
|
||||||
|
tools:node="replace">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".payment.PaymentActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.test.cardreadtest.common.enums;
|
||||||
|
|
||||||
|
public enum POS_TYPE {
|
||||||
|
BLUETOOTH, UART, USB, BLUETOOTH_BLE
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> appList) {
|
||||||
|
TRACE.d("onRequestSelectEmvApp():" + appList.toString());
|
||||||
|
// Auto-select first app for testing
|
||||||
|
if (!appList.isEmpty()) {
|
||||||
|
POSManager.getInstance().selectEmvApp(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onQposRequestPinResult(List<String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {}
|
||||||
|
|
||||||
|
}
|
|
@ -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<ConnectionServiceCallback> connectionCallbacks = new CopyOnWriteArrayList<>();
|
||||||
|
private final List<PaymentServiceCallback> 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<String, Object> 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<String, String> getEncryptData() {
|
||||||
|
Log.d(TAG, "Getting encrypted data");
|
||||||
|
return pos != null ? pos.getEncryptData() : new Hashtable<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hashtable<String, String> 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<String, String> 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<ConnectionServiceCallback> 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<PaymentServiceCallback> 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<T> {
|
||||||
|
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<String, String> 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<String> 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<String, String> 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<String> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> 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<String> 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<String, String> cardInfo) {}
|
||||||
|
|
||||||
|
default void onTransactionCompleted(PaymentResult result) {}
|
||||||
|
default void onTransactionFailed(String errorMessage,String data) {}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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包名
|
||||||
|
|
||||||
|
}
|
|
@ -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 <tt>array</tt> that starts at <tt>offset</tt>.
|
||||||
|
*/
|
||||||
|
public static byte[] get(byte[] array, int offset) {
|
||||||
|
return get(array, offset, array.length - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the subarray of length <tt>length</tt> from <tt>array</tt> that
|
||||||
|
* starts at <tt>offset</tt>.
|
||||||
|
*/
|
||||||
|
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鍙栭敊锟<EFBFBD>?");
|
||||||
|
} catch (NullPointerException var6) {
|
||||||
|
throw new Exception("鍏\ue104挜杈撳叆娴佷负锟<EFBFBD>?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildCvmPinBlock(Hashtable<String, String> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<TLV> tlvList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "TLV{" +
|
||||||
|
"tag='" + tag + '\'' +
|
||||||
|
", length='" + length + '\'' +
|
||||||
|
", value='" + value + '\'' +
|
||||||
|
", isNested=" + isNested +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<TLV> tlvList = new ArrayList<TLV>();
|
||||||
|
|
||||||
|
public static List<TLV> parse(String tlv) {
|
||||||
|
try {
|
||||||
|
tlvList.clear();
|
||||||
|
return getTLVList(hexToByteArray(tlv));
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (tlvList.size() > 0)
|
||||||
|
return tlvList;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TLV> 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<TLV> parseWithoutValue(String tlv) {
|
||||||
|
try {
|
||||||
|
return getTLVListWithoutValue(hexToByteArray(tlv));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TLV> getTLVListWithoutValue(byte[] data) {
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
ArrayList<TLV> tlvList = new ArrayList<TLV>();
|
||||||
|
|
||||||
|
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<TLV> 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<TLV> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/main"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnReadCard"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Click to Read POS Card"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:padding="22dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<variable
|
||||||
|
name="viewModel"
|
||||||
|
type="com.test.cardreadtest.payment.PaymentViewModel" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAmount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Amount: Testing..."
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:layout_marginBottom="30dp"
|
||||||
|
tools:text="Amount: $1.00" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status: Ready"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:layout_marginBottom="20dp" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Cancel Transaction"
|
||||||
|
android:layout_marginTop="30dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</layout>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 982 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 7.6 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Base.Theme.CardReadTest" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your dark theme here. -->
|
||||||
|
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">CardReadTest</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Base.Theme.CardReadTest" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your light theme here. -->
|
||||||
|
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.CardReadTest" parent="Base.Theme.CardReadTest" />
|
||||||
|
</resources>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older than API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.test.cardreadtest;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
|
@ -0,0 +1,22 @@
|
||||||
|
[versions]
|
||||||
|
agp = "8.10.0"
|
||||||
|
junit = "4.13.2"
|
||||||
|
junitVersion = "1.3.0"
|
||||||
|
espressoCore = "3.7.0"
|
||||||
|
appcompat = "1.7.1"
|
||||||
|
material = "1.13.0"
|
||||||
|
activity = "1.10.1"
|
||||||
|
constraintlayout = "2.2.1"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
|
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||||
|
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
#Wed Sep 24 09:50:53 MSK 2025
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
|
@ -0,0 +1,23 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "CardReadTest"
|
||||||
|
include ':app'
|