Compare commits
56 Commits
master
...
6f78b6df3f
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f78b6df3f | |||
| da312ec3ae | |||
| 53964211c2 | |||
| b66ef4bb00 | |||
| 7a2ddc3f15 | |||
| 8a73206a76 | |||
| f6650f99d0 | |||
| 8ac97437a2 | |||
| 2b57d35553 | |||
| f2c3de9f5f | |||
| f5d9e53118 | |||
| ece79942c1 | |||
| 0af0e836b1 | |||
| f403358554 | |||
| d43c4bad0c | |||
| 174a1461fd | |||
| f4e5e03077 | |||
| f48e3e64a4 | |||
| 2ea0792d28 | |||
| 9834d4b841 | |||
| 8add903edb | |||
| 124da43a1e | |||
| d7617186a6 | |||
| 93fc410e37 | |||
| 448dfd9835 | |||
| eac3179d8a | |||
| 729bdddad4 | |||
| c56cae64b9 | |||
| d4245c5906 | |||
| eddade3200 | |||
| 13ab6b717e | |||
| 991f77dabe | |||
| da8bcf17cc | |||
| b0ee2e8ee6 | |||
| 4aaa9957e7 | |||
| 99fab68e71 | |||
| 074a4b1f53 | |||
| a1f536b03e | |||
| edca7f92ec | |||
| 3f189f5975 | |||
| 5a03fc3aec | |||
| a30e767adc | |||
| 74f95e0374 | |||
| 1799e7eb0e | |||
| 2a24016637 | |||
| 459d9ab0f1 | |||
| 191966a2e4 | |||
| 46fb81b6a7 | |||
| 290f3015d9 | |||
| f1228db89a | |||
| 810964b4be | |||
| a7fa40d60a | |||
| a07e7a99ac | |||
| c55af6141f | |||
| 6d681f5e41 | |||
| 1ca26371a1 |
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic"
|
||||||
|
}
|
||||||
@@ -5,11 +5,18 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace 'com.example.bdkipoc'
|
namespace 'com.example.bdkipoc'
|
||||||
compileSdk 35
|
compileSdk 35
|
||||||
|
|
||||||
|
// Tambahkan lint options
|
||||||
|
lint {
|
||||||
|
abortOnError false
|
||||||
|
disable 'GoogleAppIndexingWarning'
|
||||||
|
disable 'NonConstantResourceId'
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.example.bdkipoc"
|
applicationId "com.example.bdkipoc"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 30
|
targetSdk 33
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
@@ -22,19 +29,32 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep Java 11 - lebih modern dari referensi
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tambahkan sourceSets untuk native libs jika diperlukan
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
jniLibs.srcDirs = ['libs']
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
|
||||||
implementation libs.appcompat
|
implementation libs.appcompat
|
||||||
implementation libs.material
|
implementation libs.material
|
||||||
implementation libs.activity
|
implementation libs.activity
|
||||||
implementation libs.constraintlayout
|
implementation libs.constraintlayout
|
||||||
implementation libs.cardview
|
implementation libs.cardview
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||||
|
implementation 'com.sunmi:printerlibrary:1.0.15'
|
||||||
|
|
||||||
|
// Test dependencies
|
||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
androidTestImplementation libs.ext.junit
|
androidTestImplementation libs.ext.junit
|
||||||
androidTestImplementation libs.espresso.core
|
androidTestImplementation libs.espresso.core
|
||||||
|
|||||||
BIN
app/libs/PayLib-release-2.0.17-sources.jar
Normal file
BIN
app/libs/PayLib-release-2.0.17.aar
Normal file
BIN
app/libs/armeabi-v7a/libAE_100.so
Normal file
BIN
app/libs/armeabi-v7a/libCPACE_100.so
Normal file
BIN
app/libs/armeabi-v7a/libDPAS_100.so
Normal file
BIN
app/libs/armeabi-v7a/libEFTPOS_001.so
Normal file
BIN
app/libs/armeabi-v7a/libEMVL2Base.so
Normal file
BIN
app/libs/armeabi-v7a/libEMVL2Dirct.so
Normal file
BIN
app/libs/armeabi-v7a/libEMV_100.so
Normal file
BIN
app/libs/armeabi-v7a/libEntry.so
Normal file
BIN
app/libs/armeabi-v7a/libFLASH_001.so
Normal file
BIN
app/libs/armeabi-v7a/libJCB_100.so
Normal file
BIN
app/libs/armeabi-v7a/libMIR_001.so
Normal file
BIN
app/libs/armeabi-v7a/libPAGO_001.so
Normal file
BIN
app/libs/armeabi-v7a/libPURE_001.so
Normal file
BIN
app/libs/armeabi-v7a/libPaypass_100.so
Normal file
BIN
app/libs/armeabi-v7a/libPaywave_100.so
Normal file
BIN
app/libs/armeabi-v7a/libQPBOC_100.so
Normal file
BIN
app/libs/armeabi-v7a/libRupay_001.so
Normal file
BIN
app/libs/armeabi-v7a/libSamsungPay_001.so
Normal file
BIN
app/libs/armeabi-v7a/libsunmiemvl2.so
Normal file
BIN
app/libs/sunmiemvl2split-1.0.1.jar
Normal file
@@ -8,7 +8,23 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission android:name="com.sunmi.perm.LED" />
|
||||||
|
<uses-permission android:name="com.sunmi.perm.MSR" />
|
||||||
|
<uses-permission android:name="com.sunmi.perm.ICC" />
|
||||||
|
<uses-permission android:name="com.sunmi.perm.PINPAD" />
|
||||||
|
<uses-permission android:name="com.sunmi.perm.SECURITY" />
|
||||||
|
<uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".MyApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -17,6 +33,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.BDKIPOC"
|
android:theme="@style/Theme.BDKIPOC"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@@ -28,12 +45,45 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".TransactionActivity"
|
android:name=".cetakulang.ReprintActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".PaymentActivity"
|
android:name=".cetakulang.ReprintAdapterActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity android:name=".QrisResultActivity" />
|
|
||||||
|
<activity
|
||||||
|
android:name=".ReceiptActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".QrisActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".QrisResultActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".SettlementActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".HistoryActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".HistoryDetailActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".transaction.CreateTransactionActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".transaction.ResultTransactionActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity android:name="com.sunmi.emv.l2.view.AppSelectActivity"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
27
app/src/main/java/com/example/bdkipoc/CacheHelper.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
public class CacheHelper {
|
||||||
|
|
||||||
|
private static final String PREFERENCE_FILE_NAME = "sm_pay_demo_obj";
|
||||||
|
|
||||||
|
private static final String KEY_LANGUAGE = "key_language";
|
||||||
|
|
||||||
|
public static void saveCurrentLanguage(int language) {
|
||||||
|
SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);
|
||||||
|
int value = sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO);
|
||||||
|
if (value == language) return;
|
||||||
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
|
editor.putInt(KEY_LANGUAGE, language);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getCurrentLanguage() {
|
||||||
|
SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);
|
||||||
|
return sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
17
app/src/main/java/com/example/bdkipoc/Constant.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
public class Constant {
|
||||||
|
|
||||||
|
public static final String TAG = "SDKTestDemo";
|
||||||
|
|
||||||
|
public static final int LANGUAGE_AUTO = 0;
|
||||||
|
public static final int LANGUAGE_ZH_CN = 1;
|
||||||
|
public static final int LANGUAGE_EN_US = 2;
|
||||||
|
public static final int LANGUAGE_JA_JP = 3;
|
||||||
|
|
||||||
|
public static final int SCAN_MODEL_NONE = 100;
|
||||||
|
public static final int SCAN_MODEL_P2Lite = 101;
|
||||||
|
|
||||||
|
public static final String SCAN_MODEL_NONE_VALUE = "NONE";
|
||||||
|
public static final String SCAN_MODEL_P2Lite_VALUE = "P2Lite";
|
||||||
|
}
|
||||||
@@ -1,41 +1,450 @@
|
|||||||
package com.example.bdkipoc;
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import androidx.activity.EdgeToEdge;
|
import androidx.activity.EdgeToEdge;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.cardview.widget.CardView;
|
import androidx.cardview.widget.CardView;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.button.MaterialButton;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.cetakulang.ReprintActivity;
|
||||||
|
import com.example.bdkipoc.cetakulang.ReprintAdapterActivity;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
|
import com.example.bdkipoc.transaction.CreateTransactionActivity;
|
||||||
|
import com.example.bdkipoc.transaction.ResultTransactionActivity;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private boolean isExpanded = false; // False = showing only 9 main menus, True = showing all 15 menus
|
||||||
|
private MaterialButton btnLainnya;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
// Enable hardware acceleration for smoother scrolling
|
||||||
|
getWindow().setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
|
||||||
|
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
|
||||||
|
);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
EdgeToEdge.enable(this);
|
EdgeToEdge.enable(this);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content), (v, insets) -> {
|
||||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||||
return insets;
|
return insets;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up click listeners for the cards
|
// Initialize views
|
||||||
CardView paymentCard = findViewById(R.id.card_payment);
|
btnLainnya = findViewById(R.id.btn_lainnya);
|
||||||
CardView transactionsCard = findViewById(R.id.card_transactions);
|
|
||||||
|
|
||||||
paymentCard.setOnClickListener(v -> {
|
// Check if we're returning from a completed transaction
|
||||||
// Launch payment activity
|
checkTransactionCompletion();
|
||||||
startActivity(new android.content.Intent(MainActivity.this, PaymentActivity.class));
|
|
||||||
|
// Setup initial state - 9 main menus visible, 6 dummy menus hidden
|
||||||
|
setupInitialMenuState();
|
||||||
|
|
||||||
|
// Setup menu listeners
|
||||||
|
setupMenuListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupInitialMenuState() {
|
||||||
|
// 9 main menus should always be visible
|
||||||
|
CardView cardBantuan = findViewById(R.id.card_bantuan);
|
||||||
|
CardView cardInfoToko = findViewById(R.id.card_info_toko);
|
||||||
|
|
||||||
|
if (cardBantuan != null) {
|
||||||
|
cardBantuan.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
if (cardInfoToko != null) {
|
||||||
|
cardInfoToko.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6 dummy menus should be hidden initially
|
||||||
|
CardView[] dummyCards = {
|
||||||
|
findViewById(R.id.card_bantuan),
|
||||||
|
findViewById(R.id.card_info_toko),
|
||||||
|
findViewById(R.id.card_pengaturan),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (CardView card : dummyCards) {
|
||||||
|
if (card != null) {
|
||||||
|
card.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial button text
|
||||||
|
isExpanded = false;
|
||||||
|
btnLainnya.setText("Lainnya");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkTransactionCompletion() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent != null) {
|
||||||
|
boolean transactionCompleted = intent.getBooleanExtra("transaction_completed", false);
|
||||||
|
String transactionAmount = intent.getStringExtra("transaction_amount");
|
||||||
|
|
||||||
|
if (transactionCompleted) {
|
||||||
|
if (transactionAmount != null) {
|
||||||
|
Toast.makeText(this, "Transaksi berhasil! Jumlah: Rp " + formatCurrency(transactionAmount), Toast.LENGTH_LONG).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Transaksi berhasil diselesaikan!", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCurrency(String amount) {
|
||||||
|
try {
|
||||||
|
long amountValue = Long.parseLong(amount);
|
||||||
|
return String.format("%,d", amountValue).replace(',', '.');
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupMenuListeners() {
|
||||||
|
// Card IDs to set up listeners - Total 15 menu items
|
||||||
|
int[] cardIds = {
|
||||||
|
// Row 1 (Always visible - 3 items)
|
||||||
|
R.id.card_kartu_kredit,
|
||||||
|
R.id.card_kartu_debit,
|
||||||
|
R.id.card_qris,
|
||||||
|
// Row 2 (Always visible - 3 items)
|
||||||
|
R.id.card_transfer,
|
||||||
|
R.id.card_uang_elektronik,
|
||||||
|
R.id.card_cetak_ulang,
|
||||||
|
// Row 3 (Always visible - 3 items)
|
||||||
|
R.id.card_refund,
|
||||||
|
R.id.card_settlement,
|
||||||
|
R.id.card_histori,
|
||||||
|
// Row 4 (Hidden initially - 3 items)
|
||||||
|
R.id.card_bantuan,
|
||||||
|
R.id.card_info_toko,
|
||||||
|
R.id.card_pengaturan,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up click listeners for all cards
|
||||||
|
for (int cardId : cardIds) {
|
||||||
|
CardView cardView = findViewById(cardId);
|
||||||
|
if (cardView != null) {
|
||||||
|
cardView.setOnClickListener(v -> {
|
||||||
|
// ✅ ENHANCED: Navigate with payment type information
|
||||||
|
if (cardId == R.id.card_kartu_kredit) {
|
||||||
|
navigateToCreateTransaction("credit_card", cardId, "Kartu Kredit");
|
||||||
|
} else if (cardId == R.id.card_kartu_debit) {
|
||||||
|
navigateToCreateTransaction("debit_card", cardId, "Kartu Debit");
|
||||||
|
} else if (cardId == R.id.card_qris) {
|
||||||
|
startActivity(new Intent(MainActivity.this, QrisActivity.class));
|
||||||
|
// Col-2
|
||||||
|
} else if (cardId == R.id.card_transfer) {
|
||||||
|
navigateToCreateTransaction("transfer", cardId, "Transfer");
|
||||||
|
} else if (cardId == R.id.card_uang_elektronik) {
|
||||||
|
navigateToCreateTransaction("e_money", cardId, "Uang Elektronik");
|
||||||
|
} else if (cardId == R.id.card_cetak_ulang) {
|
||||||
|
startActivity(new Intent(MainActivity.this, ReprintActivity.class));
|
||||||
|
// Col-3
|
||||||
|
} else if (cardId == R.id.card_refund) {
|
||||||
|
navigateToCreateTransaction("refund", cardId, "Refund");
|
||||||
|
} else if (cardId == R.id.card_settlement) {
|
||||||
|
Toast.makeText(this, "Settlement - Coming Soon", Toast.LENGTH_SHORT).show();
|
||||||
|
} else if (cardId == R.id.card_histori) {
|
||||||
|
startActivity(new Intent(MainActivity.this, HistoryActivity.class));
|
||||||
|
// Col-4
|
||||||
|
} else if (cardId == R.id.card_bantuan) {
|
||||||
|
Toast.makeText(this, "Bantuan - Coming Soon", Toast.LENGTH_SHORT).show();
|
||||||
|
} else if (cardId == R.id.card_info_toko) {
|
||||||
|
Toast.makeText(this, "Info Toko - Coming Soon", Toast.LENGTH_SHORT).show();
|
||||||
|
} else if (cardId == R.id.card_pengaturan) {
|
||||||
|
Toast.makeText(this, "Pengaturan - Coming Soon", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
// Fallback for any other cards
|
||||||
|
navigateToCreateTransaction("credit_card", cardId, "Unknown");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get references to ONLY the dummy cards that need to be toggled
|
||||||
|
CardView[] toggleableCards = {
|
||||||
|
findViewById(R.id.card_bantuan),
|
||||||
|
findViewById(R.id.card_info_toko),
|
||||||
|
findViewById(R.id.card_pengaturan),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up "Lainnya" button click listener
|
||||||
|
btnLainnya.setOnClickListener(v -> {
|
||||||
|
isExpanded = !isExpanded;
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
// Show the 6 dummy menus with animation
|
||||||
|
for (CardView card : toggleableCards) {
|
||||||
|
if (card != null) {
|
||||||
|
card.setVisibility(View.VISIBLE);
|
||||||
|
card.setAlpha(0f);
|
||||||
|
card.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(300)
|
||||||
|
.setInterpolator(new AccelerateDecelerateInterpolator())
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btnLainnya.setText("Tampilkan Lebih Sedikit");
|
||||||
|
} else {
|
||||||
|
// Hide the 6 dummy menus with animation
|
||||||
|
for (CardView card : toggleableCards) {
|
||||||
|
if (card != null) {
|
||||||
|
card.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(300)
|
||||||
|
.setInterpolator(new AccelerateDecelerateInterpolator())
|
||||||
|
.withEndAction(() -> card.setVisibility(View.GONE))
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btnLainnya.setText("Lainnya");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
transactionsCard.setOnClickListener(v -> {
|
// Set up scan dan bayar card click listener
|
||||||
// Launch transactions activity
|
LinearLayout scanBayarContent = findViewById(R.id.scan_bayar_content);
|
||||||
startActivity(new android.content.Intent(MainActivity.this, TransactionActivity.class));
|
if (scanBayarContent != null) {
|
||||||
|
scanBayarContent.setOnClickListener(v -> {
|
||||||
|
// Navigate to QRIS payment activity
|
||||||
|
startActivity(new Intent(MainActivity.this, QrisActivity.class));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Enhanced navigation method with payment type information
|
||||||
|
private void navigateToCreateTransaction(String paymentType, int cardMenuId, String cardName) {
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(MainActivity.this, CreateTransactionActivity.class);
|
||||||
|
|
||||||
|
// ✅ ENHANCED: Pass comprehensive payment information
|
||||||
|
intent.putExtra("PAYMENT_TYPE", paymentType);
|
||||||
|
intent.putExtra("CARD_MENU_ID", cardMenuId);
|
||||||
|
intent.putExtra("CARD_NAME", cardName);
|
||||||
|
intent.putExtra("CALLING_ACTIVITY", "MainActivity");
|
||||||
|
|
||||||
|
// ✅ DEBUG: Log navigation details
|
||||||
|
android.util.Log.d("MainActivity", "=== NAVIGATING TO CREATE TRANSACTION ===");
|
||||||
|
android.util.Log.d("MainActivity", "Payment Type: " + paymentType);
|
||||||
|
android.util.Log.d("MainActivity", "Card Menu ID: " + cardMenuId);
|
||||||
|
android.util.Log.d("MainActivity", "Card Name: " + cardName);
|
||||||
|
android.util.Log.d("MainActivity", "========================================");
|
||||||
|
|
||||||
|
startActivity(intent);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e("MainActivity", "Error navigating to CreateTransaction: " + e.getMessage(), e);
|
||||||
|
Toast.makeText(this, "Error opening transaction: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Helper method to get payment type from card ID (for backward compatibility)
|
||||||
|
private String getPaymentTypeFromCardId(int cardId) {
|
||||||
|
if (cardId == R.id.card_kartu_kredit) {
|
||||||
|
return "credit_card";
|
||||||
|
} else if (cardId == R.id.card_kartu_debit) {
|
||||||
|
return "debit_card";
|
||||||
|
} else if (cardId == R.id.card_qris) {
|
||||||
|
return "qris";
|
||||||
|
} else if (cardId == R.id.card_transfer) {
|
||||||
|
return "transfer";
|
||||||
|
} else if (cardId == R.id.card_uang_elektronik) {
|
||||||
|
return "e_money";
|
||||||
|
} else if (cardId == R.id.card_refund) {
|
||||||
|
return "refund";
|
||||||
|
} else {
|
||||||
|
android.util.Log.w("MainActivity", "Unknown card ID: " + cardId + ", defaulting to credit_card");
|
||||||
|
return "credit_card";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Helper method to get card name from card ID
|
||||||
|
private String getCardNameFromCardId(int cardId) {
|
||||||
|
if (cardId == R.id.card_kartu_kredit) {
|
||||||
|
return "Kartu Kredit";
|
||||||
|
} else if (cardId == R.id.card_kartu_debit) {
|
||||||
|
return "Kartu Debit";
|
||||||
|
} else if (cardId == R.id.card_qris) {
|
||||||
|
return "QRIS";
|
||||||
|
} else if (cardId == R.id.card_transfer) {
|
||||||
|
return "Transfer";
|
||||||
|
} else if (cardId == R.id.card_uang_elektronik) {
|
||||||
|
return "Uang Elektronik";
|
||||||
|
} else if (cardId == R.id.card_refund) {
|
||||||
|
return "Refund";
|
||||||
|
} else if (cardId == R.id.card_settlement) {
|
||||||
|
return "Settlement";
|
||||||
|
} else if (cardId == R.id.card_histori) {
|
||||||
|
return "Histori";
|
||||||
|
} else if (cardId == R.id.card_cetak_ulang) {
|
||||||
|
return "Cetak Ulang";
|
||||||
|
} else if (cardId == R.id.card_bantuan) {
|
||||||
|
return "Bantuan";
|
||||||
|
} else if (cardId == R.id.card_info_toko) {
|
||||||
|
return "Info Toko";
|
||||||
|
} else if (cardId == R.id.card_pengaturan) {
|
||||||
|
return "Pengaturan";
|
||||||
|
} else {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Method to validate payment type compatibility
|
||||||
|
private boolean isPaymentTypeSupported(String paymentType) {
|
||||||
|
String[] supportedTypes = {
|
||||||
|
"credit_card", "debit_card", "e_money", "qris",
|
||||||
|
"transfer", "refund"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String supportedType : supportedTypes) {
|
||||||
|
if (supportedType.equals(paymentType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Method to show payment type selection dialog (for future use)
|
||||||
|
private void showPaymentTypeDialog() {
|
||||||
|
androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this);
|
||||||
|
builder.setTitle("Pilih Jenis Pembayaran");
|
||||||
|
|
||||||
|
String[] paymentTypes = {
|
||||||
|
"Kartu Kredit", "Kartu Debit", "Uang Elektronik",
|
||||||
|
"QRIS", "Transfer", "Refund"
|
||||||
|
};
|
||||||
|
String[] paymentTypeCodes = {
|
||||||
|
"credit_card", "debit_card", "e_money",
|
||||||
|
"qris", "transfer", "refund"
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.setItems(paymentTypes, (dialog, which) -> {
|
||||||
|
String selectedType = paymentTypeCodes[which];
|
||||||
|
String selectedName = paymentTypes[which];
|
||||||
|
|
||||||
|
// Use a generic card ID for dialog selection
|
||||||
|
navigateToCreateTransaction(selectedType, -1, selectedName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.setNegativeButton("Batal", null);
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
// Check for transaction completion when returning to MainActivity
|
||||||
|
checkTransactionCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
// Clear any transaction completion flags to avoid repeated messages
|
||||||
|
getIntent().removeExtra("transaction_completed");
|
||||||
|
getIntent().removeExtra("transaction_amount");
|
||||||
|
|
||||||
|
// ✅ NEW: Log resume for debugging
|
||||||
|
android.util.Log.d("MainActivity", "MainActivity resumed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
android.util.Log.d("MainActivity", "MainActivity paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
android.util.Log.d("MainActivity", "MainActivity destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Method to handle direct payment type launch (for external calls)
|
||||||
|
public static Intent createTransactionIntent(android.content.Context context, String paymentType, String cardName) {
|
||||||
|
Intent intent = new Intent(context, CreateTransactionActivity.class);
|
||||||
|
intent.putExtra("PAYMENT_TYPE", paymentType);
|
||||||
|
intent.putExtra("CARD_NAME", cardName);
|
||||||
|
intent.putExtra("CALLING_ACTIVITY", "External");
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Public method to simulate card click (for testing)
|
||||||
|
public void simulateCardClick(int cardId) {
|
||||||
|
CardView cardView = findViewById(cardId);
|
||||||
|
if (cardView != null) {
|
||||||
|
cardView.performClick();
|
||||||
|
} else {
|
||||||
|
android.util.Log.w("MainActivity", "Card not found for ID: " + cardId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Method to get all available payment types
|
||||||
|
public String[] getAvailablePaymentTypes() {
|
||||||
|
return new String[]{
|
||||||
|
"credit_card", "debit_card", "e_money",
|
||||||
|
"qris", "transfer", "refund"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Method to get payment type display names
|
||||||
|
public String[] getPaymentTypeDisplayNames() {
|
||||||
|
return new String[]{
|
||||||
|
"Kartu Kredit", "Kartu Debit", "Uang Elektronik",
|
||||||
|
"QRIS", "Transfer", "Refund"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Debug method to log all card IDs and their payment types
|
||||||
|
private void debugCardMappings() {
|
||||||
|
android.util.Log.d("MainActivity", "=== CARD PAYMENT TYPE MAPPINGS ===");
|
||||||
|
|
||||||
|
int[] cardIds = {
|
||||||
|
R.id.card_kartu_kredit, R.id.card_kartu_debit, R.id.card_qris,
|
||||||
|
R.id.card_transfer, R.id.card_uang_elektronik, R.id.card_refund
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int cardId : cardIds) {
|
||||||
|
String paymentType = getPaymentTypeFromCardId(cardId);
|
||||||
|
String cardName = getCardNameFromCardId(cardId);
|
||||||
|
android.util.Log.d("MainActivity",
|
||||||
|
"Card ID: " + cardId + " -> Payment Type: " + paymentType + " -> Name: " + cardName);
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("MainActivity", "==================================");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
197
app/src/main/java/com/example/bdkipoc/MyApplication.java
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.emv.EmvTTS;
|
||||||
|
import com.example.bdkipoc.utils.LogUtil;
|
||||||
|
import com.example.bdkipoc.utils.Utility;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.etc.ETCOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.print.PrinterOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.readcard.ReadCardOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.rfid.RFIDOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.security.BiometricManagerV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.security.DevCertManagerV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.security.NoLostKeyManagerV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.system.BasicOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.tax.TaxOptV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.test.TestOptV2;
|
||||||
|
import com.sunmi.pay.hardware.wrapper.HCEManagerV2Wrapper;
|
||||||
|
import com.sunmi.peripheral.printer.InnerPrinterCallback;
|
||||||
|
import com.sunmi.peripheral.printer.InnerPrinterException;
|
||||||
|
import com.sunmi.peripheral.printer.InnerPrinterManager;
|
||||||
|
import com.sunmi.peripheral.printer.SunmiPrinterService;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import sunmi.paylib.SunmiPayKernel;
|
||||||
|
|
||||||
|
public class MyApplication extends Application {
|
||||||
|
public static MyApplication app;
|
||||||
|
|
||||||
|
public BasicOptV2 basicOptV2; // 获取基础操作模块
|
||||||
|
public ReadCardOptV2 readCardOptV2; // 获取读卡模块
|
||||||
|
public PinPadOptV2 pinPadOptV2; // 获取PinPad操作模块
|
||||||
|
public SecurityOptV2 securityOptV2; // 获取安全操作模块
|
||||||
|
public EMVOptV2 emvOptV2; // 获取EMV操作模块
|
||||||
|
public TaxOptV2 taxOptV2; // 获取税控操作模块
|
||||||
|
public ETCOptV2 etcOptV2; // 获取ETC操作模块
|
||||||
|
public PrinterOptV2 printerOptV2; // 获取打印操作模块
|
||||||
|
public TestOptV2 testOptV2; // 获取测试操作模块
|
||||||
|
public DevCertManagerV2 devCertManagerV2; // 设备证书操作模块
|
||||||
|
public NoLostKeyManagerV2 noLostKeyManagerV2; // NoLostKey操作模块
|
||||||
|
public HCEManagerV2Wrapper hceV2Wrapper; // HCE操作模块
|
||||||
|
public RFIDOptV2 rfidOptV2; // RFID操作模块
|
||||||
|
public SunmiPrinterService sunmiPrinterService; // 打印模块
|
||||||
|
//public IScanInterface scanInterface; // 扫码模块 (commented out)
|
||||||
|
public BiometricManagerV2 mBiometricManagerV2; // 生物特征模块
|
||||||
|
|
||||||
|
private boolean connectPaySDK;//是否已连接PaySDK
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
app = this;
|
||||||
|
initLocaleLanguage();
|
||||||
|
initEmvTTS();
|
||||||
|
bindPrintService();
|
||||||
|
bindPaySDKService();
|
||||||
|
//bindScannerService(); // Commented out scanner service binding
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initLocaleLanguage() {
|
||||||
|
Resources resources = app.getResources();
|
||||||
|
DisplayMetrics dm = resources.getDisplayMetrics();
|
||||||
|
Configuration config = resources.getConfiguration();
|
||||||
|
int showLanguage = CacheHelper.getCurrentLanguage();
|
||||||
|
if (showLanguage == Constant.LANGUAGE_AUTO) {
|
||||||
|
LogUtil.e(Constant.TAG, config.locale.getCountry() + "---这是系统语言");
|
||||||
|
config.locale = Resources.getSystem().getConfiguration().locale;
|
||||||
|
} else if (showLanguage == Constant.LANGUAGE_ZH_CN) {
|
||||||
|
LogUtil.e(Constant.TAG, "这是中文");
|
||||||
|
config.locale = Locale.SIMPLIFIED_CHINESE;
|
||||||
|
} else if (showLanguage == Constant.LANGUAGE_EN_US) {
|
||||||
|
LogUtil.e(Constant.TAG, "这是英文");
|
||||||
|
config.locale = Locale.ENGLISH;
|
||||||
|
} else if (showLanguage == Constant.LANGUAGE_JA_JP) {
|
||||||
|
LogUtil.e(Constant.TAG, "这是日文");
|
||||||
|
config.locale = Locale.JAPAN;
|
||||||
|
}
|
||||||
|
resources.updateConfiguration(config, dm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
LogUtil.e(Constant.TAG, "onConfigurationChanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnectPaySDK() {
|
||||||
|
return connectPaySDK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bind PaySDK service
|
||||||
|
*/
|
||||||
|
public void bindPaySDKService() {
|
||||||
|
final SunmiPayKernel payKernel = SunmiPayKernel.getInstance();
|
||||||
|
payKernel.setEmvL2Split(true);
|
||||||
|
payKernel.initPaySDK(this, new SunmiPayKernel.ConnectCallback() {
|
||||||
|
@Override
|
||||||
|
public void onConnectPaySDK() {
|
||||||
|
LogUtil.e(Constant.TAG, "onConnectPaySDK...");
|
||||||
|
emvOptV2 = payKernel.mEMVOptV2;
|
||||||
|
basicOptV2 = payKernel.mBasicOptV2;
|
||||||
|
pinPadOptV2 = payKernel.mPinPadOptV2;
|
||||||
|
readCardOptV2 = payKernel.mReadCardOptV2;
|
||||||
|
securityOptV2 = payKernel.mSecurityOptV2;
|
||||||
|
taxOptV2 = payKernel.mTaxOptV2;
|
||||||
|
etcOptV2 = payKernel.mETCOptV2;
|
||||||
|
printerOptV2 = payKernel.mPrinterOptV2;
|
||||||
|
testOptV2 = payKernel.mTestOptV2;
|
||||||
|
devCertManagerV2 = payKernel.mDevCertManagerV2;
|
||||||
|
noLostKeyManagerV2 = payKernel.mNoLostKeyManagerV2;
|
||||||
|
mBiometricManagerV2 = payKernel.mBiometricManagerV2;
|
||||||
|
hceV2Wrapper = payKernel.mHCEManagerV2Wrapper;
|
||||||
|
rfidOptV2 = payKernel.mRFIDOptV2;
|
||||||
|
connectPaySDK = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnectPaySDK() {
|
||||||
|
LogUtil.e(Constant.TAG, "onDisconnectPaySDK...");
|
||||||
|
connectPaySDK = false;
|
||||||
|
emvOptV2 = null;
|
||||||
|
basicOptV2 = null;
|
||||||
|
pinPadOptV2 = null;
|
||||||
|
readCardOptV2 = null;
|
||||||
|
securityOptV2 = null;
|
||||||
|
taxOptV2 = null;
|
||||||
|
etcOptV2 = null;
|
||||||
|
printerOptV2 = null;
|
||||||
|
devCertManagerV2 = null;
|
||||||
|
noLostKeyManagerV2 = null;
|
||||||
|
mBiometricManagerV2 = null;
|
||||||
|
rfidOptV2 = null;
|
||||||
|
Utility.showToast(R.string.connect_fail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bind printer service
|
||||||
|
*/
|
||||||
|
private void bindPrintService() {
|
||||||
|
try {
|
||||||
|
InnerPrinterManager.getInstance().bindService(this, new InnerPrinterCallback() {
|
||||||
|
@Override
|
||||||
|
protected void onConnected(SunmiPrinterService service) {
|
||||||
|
sunmiPrinterService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDisconnected() {
|
||||||
|
sunmiPrinterService = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (InnerPrinterException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bind scanner service (commented out)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
public void bindScannerService() {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setPackage("com.sunmi.scanner");
|
||||||
|
intent.setAction("com.sunmi.scanner.IScanInterface");
|
||||||
|
bindService(intent, new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
scanInterface = IScanInterface.Stub.asInterface(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
scanInterface = null;
|
||||||
|
}
|
||||||
|
}, Service.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
private void initEmvTTS() {
|
||||||
|
EmvTTS.getInstance().init();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class PaymentActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
private Button initiatePaymentButton;
|
|
||||||
private Button simulatePaymentButton;
|
|
||||||
private ImageView qrCodeImageView;
|
|
||||||
private TextView statusTextView;
|
|
||||||
private EditText editTextAmount;
|
|
||||||
private TextView referenceIdTextView;
|
|
||||||
private View paymentDetailsLayout;
|
|
||||||
private View paymentSuccessLayout;
|
|
||||||
private Button returnToMainButton;
|
|
||||||
|
|
||||||
private String transactionId;
|
|
||||||
private String transactionUuid;
|
|
||||||
private String referenceId;
|
|
||||||
private int amount;
|
|
||||||
private JSONObject midtransResponse;
|
|
||||||
|
|
||||||
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
|
|
||||||
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
|
|
||||||
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; // Replace with your actual key
|
|
||||||
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_payment);
|
|
||||||
|
|
||||||
// Set up the toolbar
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
if (getSupportActionBar() != null) {
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
|
||||||
getSupportActionBar().setTitle("QRIS Payment");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize views
|
|
||||||
progressBar = findViewById(R.id.progressBar);
|
|
||||||
initiatePaymentButton = findViewById(R.id.initiatePaymentButton);
|
|
||||||
simulatePaymentButton = findViewById(R.id.simulatePaymentButton);
|
|
||||||
qrCodeImageView = findViewById(R.id.qrCodeImageView);
|
|
||||||
statusTextView = findViewById(R.id.statusTextView);
|
|
||||||
editTextAmount = findViewById(R.id.editTextAmount);
|
|
||||||
referenceIdTextView = findViewById(R.id.referenceIdTextView);
|
|
||||||
paymentDetailsLayout = findViewById(R.id.paymentDetailsLayout);
|
|
||||||
paymentSuccessLayout = findViewById(R.id.paymentSuccessLayout);
|
|
||||||
returnToMainButton = findViewById(R.id.returnToMainButton);
|
|
||||||
|
|
||||||
// Generate a random amount between 100,000 and 999,999
|
|
||||||
amount = new Random().nextInt(900000) + 100000;
|
|
||||||
|
|
||||||
// Format and display the amount
|
|
||||||
editTextAmount.setText("");
|
|
||||||
editTextAmount.requestFocus();
|
|
||||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
if (imm != null) {
|
|
||||||
imm.showSoftInput(editTextAmount, InputMethodManager.SHOW_IMPLICIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate reference ID
|
|
||||||
referenceId = "ref-" + generateRandomString(8);
|
|
||||||
referenceIdTextView.setText(referenceId);
|
|
||||||
|
|
||||||
// Set up click listeners
|
|
||||||
initiatePaymentButton.setOnClickListener(v -> createTransaction());
|
|
||||||
simulatePaymentButton.setOnClickListener(v -> simulateWebhook());
|
|
||||||
returnToMainButton.setOnClickListener(v -> finish());
|
|
||||||
|
|
||||||
// Initially hide the QR code and payment success views
|
|
||||||
paymentDetailsLayout.setVisibility(View.GONE);
|
|
||||||
paymentSuccessLayout.setVisibility(View.GONE);
|
|
||||||
simulatePaymentButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createTransaction() {
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
initiatePaymentButton.setEnabled(false);
|
|
||||||
statusTextView.setText("Creating transaction...");
|
|
||||||
|
|
||||||
new CreateTransactionTask().execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayQrCode(String qrImageUrl) {
|
|
||||||
new DownloadImageTask().execute(qrImageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void simulateWebhook() {
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
simulatePaymentButton.setEnabled(false);
|
|
||||||
statusTextView.setText("Processing payment...");
|
|
||||||
|
|
||||||
new SimulateWebhookTask().execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showSuccessScreen() {
|
|
||||||
paymentDetailsLayout.setVisibility(View.GONE);
|
|
||||||
paymentSuccessLayout.setVisibility(View.VISIBLE);
|
|
||||||
statusTextView.setText("Payment successful!");
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateRandomString(int length) {
|
|
||||||
String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
Random random = new Random();
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
int index = random.nextInt(chars.length());
|
|
||||||
sb.append(chars.charAt(index));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getServerKey() {
|
|
||||||
// MIDTRANS_AUTH = 'Basic base64string'
|
|
||||||
String base64 = MIDTRANS_AUTH.replace("Basic ", "");
|
|
||||||
String decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT).toString();
|
|
||||||
// Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present.
|
|
||||||
return decoded.replace(":\n", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
|
|
||||||
String input = orderId + statusCode + grossAmount + serverKey;
|
|
||||||
try {
|
|
||||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512");
|
|
||||||
byte[] messageDigest = md.digest(input.getBytes());
|
|
||||||
StringBuilder hexString = new StringBuilder();
|
|
||||||
for (byte b : messageDigest) {
|
|
||||||
String hex = Integer.toHexString(0xff & b);
|
|
||||||
if (hex.length() == 1) hexString.append('0');
|
|
||||||
hexString.append(hex);
|
|
||||||
}
|
|
||||||
return hexString.toString();
|
|
||||||
} catch (java.security.NoSuchAlgorithmException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == android.R.id.home) {
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> {
|
|
||||||
private String errorMessage;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Void... voids) {
|
|
||||||
try {
|
|
||||||
// Generate a UUID for the transaction
|
|
||||||
transactionUuid = UUID.randomUUID().toString();
|
|
||||||
|
|
||||||
// Create transaction JSON payload
|
|
||||||
JSONObject payload = new JSONObject();
|
|
||||||
payload.put("type", "PAYMENT");
|
|
||||||
payload.put("channel_category", "RETAIL_OUTLET");
|
|
||||||
payload.put("channel_code", "QRIS");
|
|
||||||
payload.put("reference_id", referenceId);
|
|
||||||
|
|
||||||
// Read amount from EditText and log it
|
|
||||||
String amountText = editTextAmount.getText().toString().trim();
|
|
||||||
Log.d("MidtransCharge", "Raw amount text: " + amountText);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
|
|
||||||
amount = Integer.parseInt(amountText);
|
|
||||||
Log.d("MidtransCharge", "Parsed amount: " + amount);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
Log.e("MidtransCharge", "Amount parsing error: " + e.getMessage());
|
|
||||||
errorMessage = "Invalid amount format";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.put("amount", amount);
|
|
||||||
payload.put("cashflow", "MONEY_IN");
|
|
||||||
payload.put("status", "INIT");
|
|
||||||
payload.put("device_id", 1);
|
|
||||||
payload.put("transaction_uuid", transactionUuid);
|
|
||||||
payload.put("transaction_time_seconds", 0.0);
|
|
||||||
payload.put("device_code", "PB4K252T00021");
|
|
||||||
payload.put("merchant_name", "Marcel Panjaitan");
|
|
||||||
payload.put("mid", "71000026521");
|
|
||||||
payload.put("tid", "73001500");
|
|
||||||
|
|
||||||
// Make the API call
|
|
||||||
URL url = new URI(BACKEND_BASE + "/transactions").toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = payload.toString().getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200 || responseCode == 201) {
|
|
||||||
// Read the response
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String responseLine;
|
|
||||||
while ((responseLine = br.readLine()) != null) {
|
|
||||||
response.append(responseLine.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the response to get transaction ID
|
|
||||||
JSONObject jsonResponse = new JSONObject(response.toString());
|
|
||||||
JSONObject data = jsonResponse.getJSONObject("data");
|
|
||||||
transactionId = String.valueOf(data.getInt("id"));
|
|
||||||
|
|
||||||
// Now generate QRIS via Midtrans
|
|
||||||
return generateQris(amount);
|
|
||||||
} else {
|
|
||||||
// Read error response
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String responseLine;
|
|
||||||
while ((responseLine = br.readLine()) != null) {
|
|
||||||
response.append(responseLine.trim());
|
|
||||||
}
|
|
||||||
errorMessage = "Error creating transaction: " + response.toString();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
|
|
||||||
errorMessage = "Unexpected error: " + e.getMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean generateQris(int amount) {
|
|
||||||
try {
|
|
||||||
// Create QRIS charge JSON payload
|
|
||||||
JSONObject payload = new JSONObject();
|
|
||||||
payload.put("payment_type", "qris");
|
|
||||||
|
|
||||||
JSONObject transactionDetails = new JSONObject();
|
|
||||||
transactionDetails.put("order_id", transactionUuid);
|
|
||||||
transactionDetails.put("gross_amount", amount);
|
|
||||||
payload.put("transaction_details", transactionDetails);
|
|
||||||
|
|
||||||
// Log the request details
|
|
||||||
Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
|
|
||||||
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
|
|
||||||
Log.d("MidtransCharge", "Accept: application/json");
|
|
||||||
Log.d("MidtransCharge", "Content-Type: application/json");
|
|
||||||
Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL);
|
|
||||||
Log.d("MidtransCharge", "Payload: " + payload.toString());
|
|
||||||
|
|
||||||
// Make the API call to Midtrans
|
|
||||||
URL url = new URI(MIDTRANS_CHARGE_URL).toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
|
|
||||||
conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL);
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = payload.toString().getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200 || responseCode == 201) {
|
|
||||||
InputStream inputStream = conn.getInputStream();
|
|
||||||
if (inputStream != null) {
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String responseLine;
|
|
||||||
while ((responseLine = br.readLine()) != null) {
|
|
||||||
response.append(responseLine.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the response
|
|
||||||
midtransResponse = new JSONObject(response.toString());
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
|
|
||||||
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No input stream available";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
InputStream errorStream = conn.getErrorStream();
|
|
||||||
if (errorStream != null) {
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8"));
|
|
||||||
StringBuilder errorResponse = new StringBuilder();
|
|
||||||
String responseLine;
|
|
||||||
while ((responseLine = br.readLine()) != null) {
|
|
||||||
errorResponse.append(responseLine.trim());
|
|
||||||
}
|
|
||||||
Log.e("MidtransCharge", "HTTP " + responseCode + ": " + errorResponse.toString());
|
|
||||||
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": " + errorResponse.toString();
|
|
||||||
} else {
|
|
||||||
Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available");
|
|
||||||
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available";
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
|
|
||||||
errorMessage = "Unexpected error: " + e.getMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean success) {
|
|
||||||
if (success && midtransResponse != null) {
|
|
||||||
try {
|
|
||||||
// Extract needed values from midtransResponse
|
|
||||||
JSONObject actions = midtransResponse.getJSONArray("actions").getJSONObject(0);
|
|
||||||
String qrImageUrl = actions.getString("url");
|
|
||||||
|
|
||||||
// Extract transaction_id
|
|
||||||
String transactionId = midtransResponse.getString("transaction_id");
|
|
||||||
String transactionTime = midtransResponse.getString("transaction_time");
|
|
||||||
String acquirer = midtransResponse.getString("acquirer");
|
|
||||||
String merchantId = midtransResponse.getString("merchant_id");
|
|
||||||
String exactGrossAmount = midtransResponse.getString("gross_amount");
|
|
||||||
|
|
||||||
// Log everything before launching activity
|
|
||||||
Log.d("MidtransCharge", "Creating QrisResultActivity intent with:");
|
|
||||||
Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl);
|
|
||||||
Log.d("MidtransCharge", "amount: " + amount);
|
|
||||||
Log.d("MidtransCharge", "referenceId: " + referenceId);
|
|
||||||
Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid);
|
|
||||||
Log.d("MidtransCharge", "transaction_id: " + transactionId);
|
|
||||||
Log.d("MidtransCharge", "exactGrossAmount: " + exactGrossAmount);
|
|
||||||
|
|
||||||
// Instead of showing QR inline, launch QrisResultActivity
|
|
||||||
Intent intent = new Intent(PaymentActivity.this, QrisResultActivity.class);
|
|
||||||
intent.putExtra("qrImageUrl", qrImageUrl);
|
|
||||||
intent.putExtra("amount", amount);
|
|
||||||
intent.putExtra("referenceId", referenceId);
|
|
||||||
intent.putExtra("orderId", transactionUuid); // Order ID
|
|
||||||
intent.putExtra("transactionId", transactionId); // Actual Midtrans transaction_id
|
|
||||||
intent.putExtra("grossAmount", exactGrossAmount); // Exact gross amount from response
|
|
||||||
intent.putExtra("transactionTime", transactionTime); // For timestamp
|
|
||||||
intent.putExtra("acquirer", acquirer);
|
|
||||||
intent.putExtra("merchantId", merchantId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e);
|
|
||||||
Toast.makeText(PaymentActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
|
|
||||||
Toast.makeText(PaymentActivity.this, "Error processing QRIS response", Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
|
|
||||||
Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
|
|
||||||
initiatePaymentButton.setEnabled(true);
|
|
||||||
}
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
|
|
||||||
@Override
|
|
||||||
protected Bitmap doInBackground(String... urls) {
|
|
||||||
String urlDisplay = urls[0];
|
|
||||||
Bitmap bitmap = null;
|
|
||||||
try {
|
|
||||||
URL url = new URI(urlDisplay).toURL();
|
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setDoInput(true);
|
|
||||||
connection.connect();
|
|
||||||
java.io.InputStream input = connection.getInputStream();
|
|
||||||
bitmap = android.graphics.BitmapFactory.decodeStream(input);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Bitmap result) {
|
|
||||||
if (result != null) {
|
|
||||||
qrCodeImageView.setImageBitmap(result);
|
|
||||||
} else {
|
|
||||||
Toast.makeText(PaymentActivity.this, "Error loading QR code image", Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SimulateWebhookTask extends AsyncTask<Void, Void, Boolean> {
|
|
||||||
private String errorMessage;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Void... voids) {
|
|
||||||
try {
|
|
||||||
// Wait a moment to simulate real-world timing
|
|
||||||
Thread.sleep(1500);
|
|
||||||
|
|
||||||
// Get server key and prepare signature
|
|
||||||
String serverKey = getServerKey();
|
|
||||||
String grossAmount = String.valueOf(amount) + ".00";
|
|
||||||
String signatureKey = generateSignature(
|
|
||||||
transactionUuid,
|
|
||||||
"200",
|
|
||||||
grossAmount,
|
|
||||||
serverKey
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create webhook payload
|
|
||||||
JSONObject payload = new JSONObject();
|
|
||||||
payload.put("transaction_type", "on-us");
|
|
||||||
payload.put("transaction_time", midtransResponse.getString("transaction_time"));
|
|
||||||
payload.put("transaction_status", "settlement");
|
|
||||||
payload.put("transaction_id", midtransResponse.getString("transaction_id"));
|
|
||||||
payload.put("status_message", "midtrans payment notification");
|
|
||||||
payload.put("status_code", "200");
|
|
||||||
payload.put("signature_key", signatureKey);
|
|
||||||
payload.put("settlement_time", midtransResponse.getString("transaction_time"));
|
|
||||||
payload.put("payment_type", "qris");
|
|
||||||
payload.put("order_id", transactionUuid);
|
|
||||||
payload.put("merchant_id", midtransResponse.getString("merchant_id"));
|
|
||||||
payload.put("issuer", midtransResponse.getString("acquirer"));
|
|
||||||
payload.put("gross_amount", grossAmount);
|
|
||||||
payload.put("fraud_status", "accept");
|
|
||||||
payload.put("currency", "IDR");
|
|
||||||
payload.put("acquirer", midtransResponse.getString("acquirer"));
|
|
||||||
payload.put("shopeepay_reference_number", "");
|
|
||||||
payload.put("reference_id", referenceId);
|
|
||||||
|
|
||||||
// Call the webhook URL
|
|
||||||
URL url = new URI(WEBHOOK_URL).toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = payload.toString().getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200 || responseCode == 201) {
|
|
||||||
// Wait briefly to allow the backend to process
|
|
||||||
Thread.sleep(2000);
|
|
||||||
return checkTransactionStatus();
|
|
||||||
} else {
|
|
||||||
// Read error response
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String responseLine;
|
|
||||||
while ((responseLine = br.readLine()) != null) {
|
|
||||||
response.append(responseLine.trim());
|
|
||||||
}
|
|
||||||
errorMessage = "Error simulating payment: " + response.toString();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
errorMessage = "Error: " + e.getMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkTransactionStatus() {
|
|
||||||
try {
|
|
||||||
// Check transaction status
|
|
||||||
URL url = new URI(BACKEND_BASE + "/transactions/" + transactionId).toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200) {
|
|
||||||
// Read the response
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String responseLine;
|
|
||||||
while ((responseLine = br.readLine()) != null) {
|
|
||||||
response.append(responseLine.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the response
|
|
||||||
JSONObject jsonResponse = new JSONObject(response.toString());
|
|
||||||
JSONObject data = jsonResponse.getJSONObject("data");
|
|
||||||
String status = data.getString("status");
|
|
||||||
|
|
||||||
return status.equalsIgnoreCase("SUCCESS");
|
|
||||||
} else {
|
|
||||||
errorMessage = "Error checking transaction status. HTTP response code: " + responseCode;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
errorMessage = "Error checking transaction status: " + e.getMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean success) {
|
|
||||||
if (success) {
|
|
||||||
showSuccessScreen();
|
|
||||||
} else {
|
|
||||||
String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
|
|
||||||
Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
|
|
||||||
simulatePaymentButton.setEnabled(true);
|
|
||||||
}
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
public class QrisResultActivity extends AppCompatActivity {
|
|
||||||
private ImageView qrImageView;
|
|
||||||
private TextView amountTextView;
|
|
||||||
private TextView referenceTextView;
|
|
||||||
private Button downloadQrisButton;
|
|
||||||
private Button checkStatusButton;
|
|
||||||
private TextView statusTextView;
|
|
||||||
private Button returnMainButton;
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
private String orderId;
|
|
||||||
private String grossAmount;
|
|
||||||
private String referenceId;
|
|
||||||
private String transactionId;
|
|
||||||
private String transactionTime;
|
|
||||||
private String acquirer;
|
|
||||||
private String merchantId;
|
|
||||||
private String backendBase = "https://be-edc.msvc.app";
|
|
||||||
private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_qris_result);
|
|
||||||
|
|
||||||
qrImageView = findViewById(R.id.qrImageView);
|
|
||||||
amountTextView = findViewById(R.id.amountTextView);
|
|
||||||
referenceTextView = findViewById(R.id.referenceTextView);
|
|
||||||
downloadQrisButton = findViewById(R.id.downloadQrisButton);
|
|
||||||
checkStatusButton = findViewById(R.id.checkStatusButton);
|
|
||||||
statusTextView = findViewById(R.id.statusTextView);
|
|
||||||
returnMainButton = findViewById(R.id.returnMainButton);
|
|
||||||
progressBar = findViewById(R.id.progressBar);
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
String qrImageUrl = intent.getStringExtra("qrImageUrl");
|
|
||||||
int amount = intent.getIntExtra("amount", 0);
|
|
||||||
referenceId = intent.getStringExtra("referenceId");
|
|
||||||
orderId = intent.getStringExtra("orderId");
|
|
||||||
grossAmount = intent.getStringExtra("grossAmount");
|
|
||||||
transactionId = intent.getStringExtra("transactionId");
|
|
||||||
transactionTime = intent.getStringExtra("transactionTime");
|
|
||||||
acquirer = intent.getStringExtra("acquirer");
|
|
||||||
merchantId = intent.getStringExtra("merchantId");
|
|
||||||
|
|
||||||
if (orderId == null || transactionId == null) {
|
|
||||||
Log.e("QrisResultFlow", "orderId or transactionId is null! Intent extras: " + intent.getExtras());
|
|
||||||
android.widget.Toast.makeText(this, "Missing transaction details!", android.widget.Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the exact amount from the grossAmount string value instead of the integer
|
|
||||||
String amountStr = "Amount: " + grossAmount;
|
|
||||||
amountTextView.setText(amountStr);
|
|
||||||
referenceTextView.setText("Reference ID: " + referenceId);
|
|
||||||
|
|
||||||
// Load QR image
|
|
||||||
new DownloadImageTask(qrImageView).execute(qrImageUrl);
|
|
||||||
|
|
||||||
// Disable check status button initially
|
|
||||||
checkStatusButton.setEnabled(false);
|
|
||||||
// Start polling for pending payment log
|
|
||||||
pollPendingPaymentLog(orderId);
|
|
||||||
|
|
||||||
// Download QRIS button
|
|
||||||
downloadQrisButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
qrImageView.setDrawingCacheEnabled(true);
|
|
||||||
Bitmap bitmap = qrImageView.getDrawingCache();
|
|
||||||
if (bitmap != null) {
|
|
||||||
saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
qrImageView.setDrawingCacheEnabled(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check Payment Status button
|
|
||||||
checkStatusButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
simulateWebhook();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return to Main Screen button
|
|
||||||
returnMainButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
finishAffinity();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
|
|
||||||
ImageView bmImage;
|
|
||||||
DownloadImageTask(ImageView bmImage) {
|
|
||||||
this.bmImage = bmImage;
|
|
||||||
}
|
|
||||||
protected Bitmap doInBackground(String... urls) {
|
|
||||||
String urlDisplay = urls[0];
|
|
||||||
Bitmap bitmap = null;
|
|
||||||
try {
|
|
||||||
URL url = new URI(urlDisplay).toURL();
|
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setDoInput(true);
|
|
||||||
connection.connect();
|
|
||||||
InputStream input = connection.getInputStream();
|
|
||||||
bitmap = BitmapFactory.decodeStream(input);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
protected void onPostExecute(Bitmap result) {
|
|
||||||
if (result != null) {
|
|
||||||
bmImage.setImageBitmap(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save bitmap to gallery
|
|
||||||
private void saveImageToGallery(Bitmap bitmap, String fileName) {
|
|
||||||
try {
|
|
||||||
String savedImageURL = android.provider.MediaStore.Images.Media.insertImage(
|
|
||||||
getContentResolver(), bitmap, fileName, "QRIS Payment QR Code");
|
|
||||||
if (savedImageURL != null) {
|
|
||||||
android.widget.Toast.makeText(this, "QRIS saved to gallery", android.widget.Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
android.widget.Toast.makeText(this, "Failed to save QRIS", android.widget.Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
android.widget.Toast.makeText(this, "Error saving QRIS: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pollPendingPaymentLog(final String orderId) {
|
|
||||||
Log.d("QrisResultFlow", "Polling for orderId (transaction_uuid): " + orderId);
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
new Thread(() -> {
|
|
||||||
int maxAttempts = 10;
|
|
||||||
int intervalMs = 1500;
|
|
||||||
int attempt = 0;
|
|
||||||
boolean found = false;
|
|
||||||
while (attempt < maxAttempts && !found) {
|
|
||||||
try {
|
|
||||||
String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}";
|
|
||||||
Log.d("QrisResultFlow", "Polling URL: " + urlStr);
|
|
||||||
URL url = new URL(urlStr);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200) {
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
}
|
|
||||||
JSONObject json = new JSONObject(response.toString());
|
|
||||||
JSONArray results = json.optJSONArray("results");
|
|
||||||
if (results != null && results.length() > 0) {
|
|
||||||
for (int i = 0; i < results.length(); i++) {
|
|
||||||
JSONObject log = results.getJSONObject(i);
|
|
||||||
JSONObject reqBody = log.optJSONObject("request_body");
|
|
||||||
if (reqBody != null && "pending".equals(reqBody.optString("transaction_status"))) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResultFlow", "Polling error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
attempt++;
|
|
||||||
try { Thread.sleep(intervalMs); } catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final boolean logFound = found;
|
|
||||||
new Handler(Looper.getMainLooper()).post(() -> {
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
if (logFound) {
|
|
||||||
checkStatusButton.setEnabled(true);
|
|
||||||
android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log found!", android.widget.Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log NOT found.", android.widget.Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate webhook callback
|
|
||||||
private void simulateWebhook() {
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
JSONObject payload = new JSONObject();
|
|
||||||
payload.put("transaction_type", "on-us");
|
|
||||||
payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
|
|
||||||
payload.put("transaction_status", "settlement");
|
|
||||||
payload.put("transaction_id", transactionId); // Use the actual transaction_id
|
|
||||||
payload.put("status_message", "midtrans payment notification");
|
|
||||||
payload.put("status_code", "200");
|
|
||||||
payload.put("signature_key", "dummy_signature");
|
|
||||||
payload.put("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
|
|
||||||
payload.put("payment_type", "qris");
|
|
||||||
payload.put("order_id", orderId); // Use order_id
|
|
||||||
payload.put("merchant_id", merchantId != null ? merchantId : "DUMMY_MERCHANT_ID");
|
|
||||||
payload.put("issuer", acquirer != null ? acquirer : "gopay");
|
|
||||||
payload.put("gross_amount", grossAmount); // Use exact gross amount
|
|
||||||
payload.put("fraud_status", "accept");
|
|
||||||
payload.put("currency", "IDR");
|
|
||||||
payload.put("acquirer", acquirer != null ? acquirer : "gopay");
|
|
||||||
payload.put("shopeepay_reference_number", "");
|
|
||||||
payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID");
|
|
||||||
Log.d("QrisResultFlow", "Webhook payload: " + payload.toString());
|
|
||||||
|
|
||||||
URL url = new URL(webhookUrl);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
OutputStream os = conn.getOutputStream();
|
|
||||||
os.write(payload.toString().getBytes());
|
|
||||||
os.flush();
|
|
||||||
os.close();
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(
|
|
||||||
responseCode < 400 ? conn.getInputStream() : conn.getErrorStream()));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
}
|
|
||||||
Log.d("QrisResultFlow", "Webhook response: " + response.toString());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResultFlow", "Webhook error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
new Handler(Looper.getMainLooper()).post(() -> {
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
// Proceed to show status/result
|
|
||||||
qrImageView.setVisibility(View.GONE);
|
|
||||||
amountTextView.setVisibility(View.GONE);
|
|
||||||
referenceTextView.setVisibility(View.GONE);
|
|
||||||
downloadQrisButton.setVisibility(View.GONE);
|
|
||||||
checkStatusButton.setVisibility(View.GONE);
|
|
||||||
statusTextView.setVisibility(View.VISIBLE);
|
|
||||||
returnMainButton.setVisibility(View.VISIBLE);
|
|
||||||
});
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1200
app/src/main/java/com/example/bdkipoc/ReceiptActivity.java
Normal file
106
app/src/main/java/com/example/bdkipoc/StyleHelper.java
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
public class StyleHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create rounded rectangle drawable programmatically
|
||||||
|
*/
|
||||||
|
public static GradientDrawable createRoundedDrawable(int color, int strokeColor, int strokeWidth, int radius) {
|
||||||
|
GradientDrawable drawable = new GradientDrawable();
|
||||||
|
drawable.setShape(GradientDrawable.RECTANGLE);
|
||||||
|
drawable.setColor(color);
|
||||||
|
drawable.setStroke(strokeWidth, strokeColor);
|
||||||
|
drawable.setCornerRadius(radius);
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply search input styling
|
||||||
|
*/
|
||||||
|
public static void applySearchInputStyle(View view, Context context) {
|
||||||
|
int white = ContextCompat.getColor(context, android.R.color.white);
|
||||||
|
int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray);
|
||||||
|
// ✅ IMPROVED: Larger corner radius and lighter border like in the image
|
||||||
|
GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border
|
||||||
|
view.setBackground(drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply filter button styling
|
||||||
|
*/
|
||||||
|
public static void applyFilterButtonStyle(View view, Context context) {
|
||||||
|
int white = ContextCompat.getColor(context, android.R.color.white);
|
||||||
|
int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray);
|
||||||
|
// ✅ IMPROVED: Larger corner radius like in the image
|
||||||
|
GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border
|
||||||
|
view.setBackground(drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply pagination button styling (simple version)
|
||||||
|
*/
|
||||||
|
public static void applyPaginationButtonStyle(View view, Context context, boolean isActive) {
|
||||||
|
int backgroundColor, strokeColor;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
backgroundColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
|
||||||
|
strokeColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
|
||||||
|
} else {
|
||||||
|
backgroundColor = ContextCompat.getColor(context, android.R.color.white);
|
||||||
|
strokeColor = ContextCompat.getColor(context, android.R.color.transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ IMPROVED: Larger corner radius for modern look (like in the image)
|
||||||
|
GradientDrawable drawable = createRoundedDrawable(backgroundColor, strokeColor, 0, 48); // 16dp radius
|
||||||
|
view.setBackground(drawable);
|
||||||
|
|
||||||
|
// Set text color if it's a TextView
|
||||||
|
if (view instanceof TextView) {
|
||||||
|
int textColor = isActive ?
|
||||||
|
ContextCompat.getColor(context, android.R.color.white) :
|
||||||
|
ContextCompat.getColor(context, android.R.color.black);
|
||||||
|
((TextView) view).setTextColor(textColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply status text color only (no background badge)
|
||||||
|
*/
|
||||||
|
public static void applyStatusTextColor(TextView textView, Context context, String status) {
|
||||||
|
String statusLower = status != null ? status.toLowerCase() : "";
|
||||||
|
int textColor;
|
||||||
|
|
||||||
|
if (statusLower.equals("failed") || statusLower.equals("failure") ||
|
||||||
|
statusLower.equals("error") || statusLower.equals("declined") ||
|
||||||
|
statusLower.equals("expire") || statusLower.equals("cancel")) {
|
||||||
|
// Red text for failed/error statuses
|
||||||
|
textColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
|
||||||
|
} else if (statusLower.equals("success") || statusLower.equals("paid") ||
|
||||||
|
statusLower.equals("settlement") || statusLower.equals("completed") ||
|
||||||
|
statusLower.equals("capture")) {
|
||||||
|
// Green text for successful statuses
|
||||||
|
textColor = ContextCompat.getColor(context, android.R.color.holo_green_dark);
|
||||||
|
} else if (statusLower.equals("pending") || statusLower.equals("processing") ||
|
||||||
|
statusLower.equals("waiting") || statusLower.equals("checking...") ||
|
||||||
|
statusLower.equals("checking")) {
|
||||||
|
// Orange text for pending/processing statuses
|
||||||
|
textColor = ContextCompat.getColor(context, android.R.color.holo_orange_dark);
|
||||||
|
} else if (statusLower.equals("init")) {
|
||||||
|
// Blue text for init status
|
||||||
|
textColor = ContextCompat.getColor(context, android.R.color.holo_blue_dark);
|
||||||
|
} else {
|
||||||
|
// Default gray text for unknown statuses
|
||||||
|
textColor = ContextCompat.getColor(context, android.R.color.darker_gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
textView.setTextColor(textColor);
|
||||||
|
textView.setBackground(null); // Remove any background
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class TransactionActivity extends AppCompatActivity {
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private TransactionAdapter adapter;
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
private FloatingActionButton refreshButton;
|
|
||||||
private int page = 0;
|
|
||||||
private final int limit = 10;
|
|
||||||
private boolean isLoading = false;
|
|
||||||
private boolean isLastPage = false;
|
|
||||||
private List<Transaction> transactionList = new ArrayList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_transaction);
|
|
||||||
|
|
||||||
// Set up the toolbar as the action bar
|
|
||||||
androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
|
|
||||||
// Enable the back button in the action bar
|
|
||||||
if (getSupportActionBar() != null) {
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
recyclerView = findViewById(R.id.recyclerView);
|
|
||||||
progressBar = findViewById(R.id.progressBar);
|
|
||||||
refreshButton = findViewById(R.id.refreshButton);
|
|
||||||
|
|
||||||
adapter = new TransactionAdapter(transactionList);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
|
|
||||||
refreshButton.setOnClickListener(v -> refreshTransactions());
|
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
|
||||||
@Override
|
|
||||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy);
|
|
||||||
if (!recyclerView.canScrollVertically(1) && !isLoading && !isLastPage) {
|
|
||||||
loadTransactions(page + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loadTransactions(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshTransactions() {
|
|
||||||
page = 0;
|
|
||||||
isLastPage = false;
|
|
||||||
transactionList.clear();
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
loadTransactions(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadTransactions(int pageToLoad) {
|
|
||||||
isLoading = true;
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
new FetchTransactionsTask(pageToLoad).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FetchTransactionsTask extends AsyncTask<Void, Void, List<Transaction>> {
|
|
||||||
private int pageToLoad;
|
|
||||||
private boolean error = false;
|
|
||||||
private int total = 0;
|
|
||||||
|
|
||||||
FetchTransactionsTask(int page) {
|
|
||||||
this.pageToLoad = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Transaction> doInBackground(Void... voids) {
|
|
||||||
List<Transaction> result = new ArrayList<>();
|
|
||||||
try {
|
|
||||||
String urlString = "https://be-edc.msvc.app/transactions?page=" + pageToLoad + "&limit=" + limit + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=id";
|
|
||||||
URI uri = new URI(urlString);
|
|
||||||
URL url = uri.toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("accept", "*/*");
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200) {
|
|
||||||
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = in.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
}
|
|
||||||
in.close();
|
|
||||||
JSONObject jsonObject = new JSONObject(response.toString());
|
|
||||||
JSONObject results = jsonObject.getJSONObject("results");
|
|
||||||
total = results.getInt("total");
|
|
||||||
JSONArray data = results.getJSONArray("data");
|
|
||||||
for (int i = 0; i < data.length(); i++) {
|
|
||||||
JSONObject t = data.getJSONObject(i);
|
|
||||||
Transaction tx = new Transaction(
|
|
||||||
t.getInt("id"),
|
|
||||||
t.getString("type"),
|
|
||||||
t.getString("channel_category"),
|
|
||||||
t.getString("channel_code"),
|
|
||||||
t.getString("reference_id"),
|
|
||||||
t.getString("amount"),
|
|
||||||
t.getString("cashflow"),
|
|
||||||
t.getString("status"),
|
|
||||||
t.getString("created_at"),
|
|
||||||
t.getString("merchant_name")
|
|
||||||
);
|
|
||||||
result.add(tx);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error = true;
|
|
||||||
}
|
|
||||||
} catch (IOException | JSONException | URISyntaxException e) {
|
|
||||||
error = true;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(List<Transaction> transactions) {
|
|
||||||
isLoading = false;
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
if (error) {
|
|
||||||
Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pageToLoad == 0) {
|
|
||||||
transactionList.clear();
|
|
||||||
}
|
|
||||||
transactionList.addAll(transactions);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
page = pageToLoad;
|
|
||||||
if (transactionList.size() >= total) {
|
|
||||||
isLastPage = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(android.view.MenuItem item) {
|
|
||||||
if (item.getItemId() == android.R.id.home) {
|
|
||||||
// Handle the back button click
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Transaction {
|
|
||||||
int id;
|
|
||||||
String type;
|
|
||||||
String channelCategory;
|
|
||||||
String channelCode;
|
|
||||||
String referenceId;
|
|
||||||
String amount;
|
|
||||||
String cashflow;
|
|
||||||
String status;
|
|
||||||
String createdAt;
|
|
||||||
String merchantName;
|
|
||||||
|
|
||||||
Transaction(int id, String type, String channelCategory, String channelCode, String referenceId, String amount, String cashflow, String status, String createdAt, String merchantName) {
|
|
||||||
this.id = id;
|
|
||||||
this.type = type;
|
|
||||||
this.channelCategory = channelCategory;
|
|
||||||
this.channelCode = channelCode;
|
|
||||||
this.referenceId = referenceId;
|
|
||||||
this.amount = amount;
|
|
||||||
this.cashflow = cashflow;
|
|
||||||
this.status = status;
|
|
||||||
this.createdAt = createdAt;
|
|
||||||
this.merchantName = merchantName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.text.NumberFormat;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.TransactionViewHolder> {
|
|
||||||
private List<TransactionActivity.Transaction> transactionList;
|
|
||||||
|
|
||||||
public TransactionAdapter(List<TransactionActivity.Transaction> transactionList) {
|
|
||||||
this.transactionList = transactionList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
|
|
||||||
return new TransactionViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
|
|
||||||
TransactionActivity.Transaction t = transactionList.get(position);
|
|
||||||
|
|
||||||
// Format the amount as Indonesian Rupiah
|
|
||||||
try {
|
|
||||||
double amountValue = Double.parseDouble(t.amount);
|
|
||||||
NumberFormat rupiahFormat = NumberFormat.getCurrencyInstance(new Locale.Builder().setLanguage("id").setRegion("ID").build());
|
|
||||||
holder.amount.setText(rupiahFormat.format(amountValue));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
holder.amount.setText("Rp " + t.amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.status.setText(t.status);
|
|
||||||
holder.referenceId.setText(t.referenceId);
|
|
||||||
holder.merchantName.setText(t.merchantName);
|
|
||||||
holder.createdAt.setText(t.createdAt.replace("T", " ").substring(0, 19));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return transactionList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class TransactionViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
TextView amount, status, referenceId, merchantName, createdAt;
|
|
||||||
public TransactionViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
amount = itemView.findViewById(R.id.textAmount);
|
|
||||||
status = itemView.findViewById(R.id.textStatus);
|
|
||||||
referenceId = itemView.findViewById(R.id.textReferenceId);
|
|
||||||
merchantName = itemView.findViewById(R.id.textMerchantName);
|
|
||||||
createdAt = itemView.findViewById(R.id.textCreatedAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
package com.example.bdkipoc.cetakulang;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream; // ✅ ADDED: Missing import
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.StyleHelper;
|
||||||
|
|
||||||
|
public class ReprintAdapterActivity extends RecyclerView.Adapter<ReprintAdapterActivity.TransactionViewHolder> {
|
||||||
|
private List<ReprintActivity.Transaction> transactionList;
|
||||||
|
private OnPrintClickListener printClickListener;
|
||||||
|
|
||||||
|
public interface OnPrintClickListener {
|
||||||
|
void onPrintClick(ReprintActivity.Transaction transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReprintAdapterActivity(List<ReprintActivity.Transaction> transactionList) {
|
||||||
|
this.transactionList = transactionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrintClickListener(OnPrintClickListener listener) {
|
||||||
|
this.printClickListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update data without numbering (removed as per request)
|
||||||
|
*/
|
||||||
|
public void updateData(List<ReprintActivity.Transaction> newData, int startIndex) {
|
||||||
|
this.transactionList = newData;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "📋 Data updated: " + newData.size() + " items");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_reprint, parent, false);
|
||||||
|
return new TransactionViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
|
||||||
|
ReprintActivity.Transaction t = transactionList.get(position);
|
||||||
|
|
||||||
|
// ✅ STRIPE TABLE: Set alternating row colors
|
||||||
|
LinearLayout itemContainer = holder.itemView.findViewById(R.id.itemContainer);
|
||||||
|
if (position % 2 == 0) {
|
||||||
|
// Even rows - white background
|
||||||
|
itemContainer.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.white));
|
||||||
|
} else {
|
||||||
|
// Odd rows - light gray background
|
||||||
|
itemContainer.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.background_light));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "📋 Binding transaction " + position + ":");
|
||||||
|
Log.d("ReprintAdapterActivity", " Reference: " + t.referenceId);
|
||||||
|
Log.d("ReprintAdapterActivity", " Status: " + t.status);
|
||||||
|
Log.d("ReprintAdapterActivity", " Amount: " + t.amount);
|
||||||
|
|
||||||
|
// Set reference ID
|
||||||
|
holder.referenceId.setText(t.referenceId);
|
||||||
|
|
||||||
|
// Format the amount as Indonesian Rupiah
|
||||||
|
try {
|
||||||
|
String cleanAmount = cleanAmountString(t.amount);
|
||||||
|
long amountValue = Long.parseLong(cleanAmount);
|
||||||
|
String formattedAmount = formatRupiah(amountValue);
|
||||||
|
holder.amount.setText(formattedAmount);
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'");
|
||||||
|
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Log.e("ReprintAdapterActivity", "❌ Amount format error: " + t.amount, e);
|
||||||
|
String fallback = t.amount.startsWith("Rp") ? t.amount : "Rp " + t.amount;
|
||||||
|
holder.amount.setText(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENHANCED STATUS HANDLING dengan comprehensive checking
|
||||||
|
String displayStatus = t.status;
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "🔍 Checking status for: " + t.referenceId + " (current: " + displayStatus + ")");
|
||||||
|
|
||||||
|
// Jika status adalah INIT atau PENDING, lakukan comprehensive check
|
||||||
|
if ("INIT".equalsIgnoreCase(t.status) || "PENDING".equalsIgnoreCase(t.status)) {
|
||||||
|
if (t.referenceId != null && !t.referenceId.isEmpty()) {
|
||||||
|
// Show checking state
|
||||||
|
holder.status.setText("CHECKING...");
|
||||||
|
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), "CHECKING");
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "🔄 Starting comprehensive check for: " + t.referenceId);
|
||||||
|
|
||||||
|
// Check real status dari semua kemungkinan sources
|
||||||
|
checkMidtransStatus(t.referenceId, holder.status);
|
||||||
|
} else {
|
||||||
|
// No reference ID to check
|
||||||
|
holder.status.setText(displayStatus.toUpperCase());
|
||||||
|
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
|
||||||
|
Log.w("ReprintAdapterActivity", "⚠️ No reference ID for status check");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use existing status yang sudah confirmed
|
||||||
|
holder.status.setText(displayStatus.toUpperCase());
|
||||||
|
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
|
||||||
|
Log.d("ReprintAdapterActivity", "✅ Using confirmed status: " + displayStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set payment method
|
||||||
|
String paymentMethod = getPaymentMethodName(t.channelCode, t.channelCategory);
|
||||||
|
holder.paymentMethod.setText(paymentMethod);
|
||||||
|
|
||||||
|
// ✅ FORMAT AND DISPLAY CREATED AT
|
||||||
|
String formattedDate = formatCreatedAtDate(t.createdAt);
|
||||||
|
holder.createdAt.setText(formattedDate);
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "📅 Created at: " + t.createdAt + " -> " + formattedDate);
|
||||||
|
|
||||||
|
// Set click listeners
|
||||||
|
holder.itemView.setOnClickListener(v -> {
|
||||||
|
if (printClickListener != null) {
|
||||||
|
printClickListener.onPrintClick(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.printSection.setOnClickListener(v -> {
|
||||||
|
if (printClickListener != null) {
|
||||||
|
printClickListener.onPrintClick(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "✅ Transaction binding complete for: " + t.referenceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cleanAmountString(String amount) {
|
||||||
|
if (amount == null || amount.isEmpty()) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "Cleaning amount: '" + amount + "'");
|
||||||
|
|
||||||
|
// Remove currency symbols and spaces
|
||||||
|
String cleaned = amount
|
||||||
|
.replace("Rp. ", "")
|
||||||
|
.replace("Rp ", "")
|
||||||
|
.replace("IDR ", "")
|
||||||
|
.replace(" ", "")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Handle dots properly
|
||||||
|
if (cleaned.contains(".")) {
|
||||||
|
// Split by dots
|
||||||
|
String[] parts = cleaned.split("\\.");
|
||||||
|
|
||||||
|
if (parts.length == 2) {
|
||||||
|
String beforeDot = parts[0];
|
||||||
|
String afterDot = parts[1];
|
||||||
|
|
||||||
|
// Check if it's decimal format (like "1000.00") or thousand separator (like "1.000")
|
||||||
|
if (afterDot.length() <= 2 && (afterDot.equals("00") || afterDot.equals("0"))) {
|
||||||
|
// It's decimal format - keep only the integer part
|
||||||
|
cleaned = beforeDot;
|
||||||
|
} else if (afterDot.length() == 3) {
|
||||||
|
// It's thousand separator format - combine parts
|
||||||
|
cleaned = beforeDot + afterDot;
|
||||||
|
} else {
|
||||||
|
// Ambiguous case - assume thousand separator if beforeDot is short
|
||||||
|
if (beforeDot.length() <= 3) {
|
||||||
|
cleaned = beforeDot + afterDot;
|
||||||
|
} else {
|
||||||
|
cleaned = beforeDot; // Assume decimal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parts.length > 2) {
|
||||||
|
// Multiple dots - assume all are thousand separators
|
||||||
|
cleaned = String.join("", parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any commas
|
||||||
|
cleaned = cleaned.replace(",", "");
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "Cleaned result: '" + cleaned + "'");
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format long amount to Indonesian Rupiah format
|
||||||
|
*/
|
||||||
|
private String formatRupiah(long amount) {
|
||||||
|
// Use dots as thousand separators (Indonesian format)
|
||||||
|
String formatted = String.format("%,d", amount).replace(',', '.');
|
||||||
|
return "Rp. " + formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ FIXED: Enhanced status checking with comprehensive search
|
||||||
|
private void checkMidtransStatus(String referenceId, TextView statusTextView) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Log.d("ReprintAdapterActivity", "🔍 Comprehensive status check for reference: " + referenceId);
|
||||||
|
|
||||||
|
// STEP 1: Query webhook logs untuk semua order_id yang terkait
|
||||||
|
String queryUrl = "https://be-edc.msvc.app/api-logs?limit=200&sortOrder=DESC&sortColumn=created_at";
|
||||||
|
|
||||||
|
URL url = new URL(queryUrl);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject json = new JSONObject(response.toString());
|
||||||
|
JSONArray results = json.optJSONArray("results");
|
||||||
|
|
||||||
|
String finalStatus = "INIT"; // Default
|
||||||
|
String foundOrderId = null;
|
||||||
|
String foundAcquirer = null;
|
||||||
|
|
||||||
|
if (results != null && results.length() > 0) {
|
||||||
|
Log.d("ReprintAdapterActivity", "📊 Processing " + results.length() + " log entries");
|
||||||
|
|
||||||
|
// STEP 2: Comprehensive search dengan multiple matching strategies
|
||||||
|
for (int i = 0; i < results.length(); i++) {
|
||||||
|
JSONObject log = results.getJSONObject(i);
|
||||||
|
JSONObject reqBody = log.optJSONObject("request_body");
|
||||||
|
|
||||||
|
if (reqBody != null) {
|
||||||
|
String logOrderId = reqBody.optString("order_id", "");
|
||||||
|
String logTransactionStatus = reqBody.optString("transaction_status", "");
|
||||||
|
String logReferenceId = reqBody.optString("reference_id", "");
|
||||||
|
String logAcquirer = reqBody.optString("acquirer", "");
|
||||||
|
|
||||||
|
// ✅ METHOD 1: Direct reference_id match
|
||||||
|
boolean isDirectMatch = referenceId.equals(logReferenceId);
|
||||||
|
|
||||||
|
// ✅ METHOD 2: Check custom_field1 untuk QR refresh tracking
|
||||||
|
boolean isRefreshMatch = false;
|
||||||
|
String customField1 = reqBody.optString("custom_field1", "");
|
||||||
|
if (!customField1.isEmpty()) {
|
||||||
|
try {
|
||||||
|
JSONObject customData = new JSONObject(customField1);
|
||||||
|
String originalReference = customData.optString("original_reference", "");
|
||||||
|
String appReferenceId = customData.optString("app_reference_id", "");
|
||||||
|
if (referenceId.equals(originalReference) || referenceId.equals(appReferenceId)) {
|
||||||
|
isRefreshMatch = true;
|
||||||
|
Log.d("ReprintAdapterActivity", "🔄 Found refresh match: " + logOrderId);
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
// Ignore custom field parsing errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ METHOD 3: Check item details untuk backup tracking
|
||||||
|
boolean isItemMatch = false;
|
||||||
|
JSONArray itemDetails = reqBody.optJSONArray("item_details");
|
||||||
|
if (itemDetails != null && itemDetails.length() > 0) {
|
||||||
|
for (int j = 0; j < itemDetails.length(); j++) {
|
||||||
|
JSONObject item = itemDetails.optJSONObject(j);
|
||||||
|
if (item != null) {
|
||||||
|
String itemName = item.optString("name", "");
|
||||||
|
if (itemName.contains("(Ref: " + referenceId + ")") ||
|
||||||
|
itemName.contains("- " + referenceId)) {
|
||||||
|
isItemMatch = true;
|
||||||
|
Log.d("ReprintAdapterActivity", "📦 Found item match: " + logOrderId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ COMPREHENSIVE MATCH: Any of the three methods
|
||||||
|
boolean isRelatedTransaction = isDirectMatch || isRefreshMatch || isItemMatch;
|
||||||
|
|
||||||
|
if (isRelatedTransaction) {
|
||||||
|
Log.d("ReprintAdapterActivity", "🎯 MATCH FOUND!");
|
||||||
|
Log.d("ReprintAdapterActivity", " Order ID: " + logOrderId);
|
||||||
|
Log.d("ReprintAdapterActivity", " Status: " + logTransactionStatus);
|
||||||
|
Log.d("ReprintAdapterActivity", " Acquirer: " + logAcquirer);
|
||||||
|
Log.d("ReprintAdapterActivity", " Match Type: " +
|
||||||
|
(isDirectMatch ? "DIRECT " : "") +
|
||||||
|
(isRefreshMatch ? "REFRESH " : "") +
|
||||||
|
(isItemMatch ? "ITEM" : ""));
|
||||||
|
|
||||||
|
// ✅ PRIORITY SYSTEM: settlement > capture > success > pending > init
|
||||||
|
if (logTransactionStatus.equals("settlement") ||
|
||||||
|
logTransactionStatus.equals("capture") ||
|
||||||
|
logTransactionStatus.equals("success")) {
|
||||||
|
finalStatus = "PAID";
|
||||||
|
foundOrderId = logOrderId;
|
||||||
|
foundAcquirer = logAcquirer;
|
||||||
|
Log.d("ReprintAdapterActivity", "✅ PAYMENT CONFIRMED: " + logOrderId + " -> " + logTransactionStatus);
|
||||||
|
break; // Found paid status, stop searching
|
||||||
|
} else if (logTransactionStatus.equals("pending") && finalStatus.equals("INIT")) {
|
||||||
|
finalStatus = "PENDING";
|
||||||
|
foundOrderId = logOrderId;
|
||||||
|
foundAcquirer = logAcquirer;
|
||||||
|
Log.d("ReprintAdapterActivity", "⏳ PENDING found: " + logOrderId);
|
||||||
|
} else if (logTransactionStatus.equals("expire") || logTransactionStatus.equals("cancel")) {
|
||||||
|
if (finalStatus.equals("INIT")) { // Only update if no better status found
|
||||||
|
finalStatus = "FAILED";
|
||||||
|
foundOrderId = logOrderId;
|
||||||
|
foundAcquirer = logAcquirer;
|
||||||
|
Log.d("ReprintAdapterActivity", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "🔍 FINAL RESULT for " + referenceId + ":");
|
||||||
|
Log.d("ReprintAdapterActivity", " Status: " + finalStatus);
|
||||||
|
Log.d("ReprintAdapterActivity", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A"));
|
||||||
|
Log.d("ReprintAdapterActivity", " Acquirer: " + (foundAcquirer != null ? foundAcquirer : "N/A"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 3: Update UI di main thread
|
||||||
|
final String displayStatus = finalStatus;
|
||||||
|
final String detectedAcquirer = foundAcquirer;
|
||||||
|
|
||||||
|
statusTextView.post(() -> {
|
||||||
|
statusTextView.setText(displayStatus);
|
||||||
|
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), displayStatus);
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "🎨 UI UPDATED:");
|
||||||
|
Log.d("ReprintAdapterActivity", " Reference: " + referenceId);
|
||||||
|
Log.d("ReprintAdapterActivity", " Display Status: " + displayStatus);
|
||||||
|
Log.d("ReprintAdapterActivity", " Detected Acquirer: " + (detectedAcquirer != null ? detectedAcquirer : "Unknown"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ BONUS: Update backend jika status berubah ke PAID
|
||||||
|
if (finalStatus.equals("PAID")) {
|
||||||
|
updateBackendTransactionStatus(referenceId, finalStatus, foundOrderId, detectedAcquirer);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log.w("ReprintAdapterActivity", "⚠️ API call failed with code: " + conn.getResponseCode());
|
||||||
|
statusTextView.post(() -> {
|
||||||
|
statusTextView.setText("ERROR");
|
||||||
|
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "ERROR");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | JSONException e) {
|
||||||
|
Log.e("ReprintAdapterActivity", "❌ Comprehensive status check error: " + e.getMessage(), e);
|
||||||
|
statusTextView.post(() -> {
|
||||||
|
statusTextView.setText("INIT");
|
||||||
|
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "INIT");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ NEW METHOD: Update backend transaction status when payment confirmed
|
||||||
|
*/
|
||||||
|
private void updateBackendTransactionStatus(String referenceId, String status, String orderId, String acquirer) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Log.d("ReprintAdapterActivity", "🔄 Updating backend status for reference: " + referenceId);
|
||||||
|
|
||||||
|
JSONObject updatePayload = new JSONObject();
|
||||||
|
updatePayload.put("status", status);
|
||||||
|
updatePayload.put("payment_status", status);
|
||||||
|
updatePayload.put("paid_order_id", orderId);
|
||||||
|
updatePayload.put("detected_acquirer", acquirer);
|
||||||
|
updatePayload.put("updated_at", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date()));
|
||||||
|
updatePayload.put("settlement_time", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date()));
|
||||||
|
|
||||||
|
String updateUrl = "https://be-edc.msvc.app/transactions/update-by-reference";
|
||||||
|
URL url = new URL(updateUrl);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(15000);
|
||||||
|
conn.setReadTimeout(15000);
|
||||||
|
|
||||||
|
JSONObject requestBody = new JSONObject();
|
||||||
|
requestBody.put("reference_id", referenceId);
|
||||||
|
requestBody.put("update_data", updatePayload);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
byte[] input = requestBody.toString().getBytes("utf-8");
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
Log.d("ReprintAdapterActivity", "📥 Backend update response: " + responseCode);
|
||||||
|
|
||||||
|
if (responseCode == 200 || responseCode == 201) {
|
||||||
|
Log.d("ReprintAdapterActivity", "✅ Backend status updated successfully");
|
||||||
|
} else {
|
||||||
|
Log.e("ReprintAdapterActivity", "❌ Backend update failed: " + responseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("ReprintAdapterActivity", "❌ Backend update error: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format created_at date to readable format
|
||||||
|
*/
|
||||||
|
private String formatCreatedAtDate(String rawDate) {
|
||||||
|
if (rawDate == null || rawDate.isEmpty()) {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "📅 Input date: '" + rawDate + "'");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle different possible input formats from API
|
||||||
|
SimpleDateFormat inputFormat;
|
||||||
|
String cleanedDate = rawDate;
|
||||||
|
|
||||||
|
if (rawDate.contains("T")) {
|
||||||
|
// ISO format: "2025-06-10T04:31:19.565Z"
|
||||||
|
cleanedDate = rawDate.replace("T", " ").replace("Z", "");
|
||||||
|
// Remove microseconds if present
|
||||||
|
if (cleanedDate.contains(".")) {
|
||||||
|
cleanedDate = cleanedDate.substring(0, cleanedDate.indexOf("."));
|
||||||
|
}
|
||||||
|
inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||||
|
} else if (rawDate.length() > 19 && rawDate.contains(".")) {
|
||||||
|
// Format with microseconds: "2025-06-10 04:31:19.565"
|
||||||
|
cleanedDate = rawDate.substring(0, 19); // Cut off microseconds
|
||||||
|
inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||||
|
} else {
|
||||||
|
// Standard format: "2025-06-10 04:31:19"
|
||||||
|
inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "📅 Cleaned date: '" + cleanedDate + "'");
|
||||||
|
|
||||||
|
// Output format: d/M/yyyy H:mm:ss
|
||||||
|
SimpleDateFormat outputFormat = new SimpleDateFormat("d/M/yyyy H:mm:ss", Locale.getDefault());
|
||||||
|
|
||||||
|
Date date = inputFormat.parse(cleanedDate);
|
||||||
|
if (date != null) {
|
||||||
|
String formatted = outputFormat.format(date);
|
||||||
|
Log.d("ReprintAdapterActivity", "📅 Date formatted: " + rawDate + " -> " + formatted);
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("ReprintAdapterActivity", "❌ Date formatting error for: " + rawDate, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Manual parsing
|
||||||
|
try {
|
||||||
|
// Handle format like "2025-06-10T04:31:19.565Z" manually
|
||||||
|
String workingDate = rawDate.replace("T", " ").replace("Z", "");
|
||||||
|
|
||||||
|
// Remove microseconds if present
|
||||||
|
if (workingDate.contains(".")) {
|
||||||
|
workingDate = workingDate.substring(0, workingDate.indexOf("."));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ReprintAdapterActivity", "📅 Manual parsing attempt: '" + workingDate + "'");
|
||||||
|
|
||||||
|
// Split into date and time parts
|
||||||
|
String[] parts = workingDate.split(" ");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
String datePart = parts[0]; // "2025-06-10"
|
||||||
|
String timePart = parts[1]; // "04:31:19"
|
||||||
|
|
||||||
|
String[] dateComponents = datePart.split("-");
|
||||||
|
if (dateComponents.length == 3) {
|
||||||
|
String year = dateComponents[0];
|
||||||
|
String month = dateComponents[1];
|
||||||
|
String day = dateComponents[2];
|
||||||
|
|
||||||
|
// Remove leading zeros and format as d/M/yyyy H:mm:ss
|
||||||
|
int dayInt = Integer.parseInt(day);
|
||||||
|
int monthInt = Integer.parseInt(month);
|
||||||
|
|
||||||
|
// Parse time to remove leading zeros from hour
|
||||||
|
String[] timeComponents = timePart.split(":");
|
||||||
|
if (timeComponents.length >= 3) {
|
||||||
|
int hour = Integer.parseInt(timeComponents[0]);
|
||||||
|
String minute = timeComponents[1];
|
||||||
|
String second = timeComponents[2];
|
||||||
|
|
||||||
|
String result = dayInt + "/" + monthInt + "/" + year + " " + hour + ":" + minute + ":" + second;
|
||||||
|
Log.d("ReprintAdapterActivity", "📅 Manual format result: " + result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w("ReprintAdapterActivity", "❌ Manual date formatting failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w("ReprintAdapterActivity", "📅 Using fallback - returning original date: " + rawDate);
|
||||||
|
return rawDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPaymentMethodName(String channelCode, String channelCategory) {
|
||||||
|
// Convert channel code to readable payment method name
|
||||||
|
if (channelCode == null) return "Unknown";
|
||||||
|
|
||||||
|
switch (channelCode.toUpperCase()) {
|
||||||
|
case "QRIS":
|
||||||
|
return "QRIS";
|
||||||
|
case "DEBIT":
|
||||||
|
return "Kartu Debit";
|
||||||
|
case "CREDIT":
|
||||||
|
return "Kartu Kredit";
|
||||||
|
case "BCA":
|
||||||
|
return "BCA";
|
||||||
|
case "MANDIRI":
|
||||||
|
return "Mandiri";
|
||||||
|
case "BNI":
|
||||||
|
return "BNI";
|
||||||
|
case "BRI":
|
||||||
|
return "BRI";
|
||||||
|
case "CASH":
|
||||||
|
return "Tunai";
|
||||||
|
case "EDC":
|
||||||
|
return "EDC";
|
||||||
|
default:
|
||||||
|
// If channel category is available, use it as fallback
|
||||||
|
if (channelCategory != null && !channelCategory.isEmpty()) {
|
||||||
|
return channelCategory.toUpperCase();
|
||||||
|
}
|
||||||
|
return channelCode.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return transactionList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TransactionViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView amount, referenceId, status, paymentMethod, createdAt; // ✅ Added createdAt
|
||||||
|
LinearLayout printSection;
|
||||||
|
|
||||||
|
public TransactionViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
amount = itemView.findViewById(R.id.textAmount);
|
||||||
|
referenceId = itemView.findViewById(R.id.textReferenceId);
|
||||||
|
status = itemView.findViewById(R.id.textStatus);
|
||||||
|
paymentMethod = itemView.findViewById(R.id.textPaymentMethod);
|
||||||
|
createdAt = itemView.findViewById(R.id.textCreatedAt); // ✅ Added createdAt
|
||||||
|
printSection = itemView.findViewById(R.id.printSection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
app/src/main/java/com/example/bdkipoc/emv/EmvTTS.java
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package com.example.bdkipoc.emv;
|
||||||
|
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
|
import android.speech.tts.UtteranceProgressListener;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.MyApplication;
|
||||||
|
import com.example.bdkipoc.utils.LogUtil;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class EmvTTS extends UtteranceProgressListener {
|
||||||
|
private static final String TAG = "EmvTTS";
|
||||||
|
private TextToSpeech textToSpeech;
|
||||||
|
private boolean supportTTS;
|
||||||
|
private ITTSProgressListener listener;
|
||||||
|
|
||||||
|
private EmvTTS() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EmvTTS getInstance() {
|
||||||
|
return SingletonHolder.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTTSListener(ITTSProgressListener l) {
|
||||||
|
listener = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeTTSListener() {
|
||||||
|
listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SingletonHolder {
|
||||||
|
private static final EmvTTS INSTANCE = new EmvTTS();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
//初始化TTS对象
|
||||||
|
destroy();
|
||||||
|
textToSpeech = new TextToSpeech(MyApplication.app, this::onTTSInit);
|
||||||
|
textToSpeech.setOnUtteranceProgressListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void play(String text) {
|
||||||
|
play(text, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void play(String text, String utteranceId) {
|
||||||
|
if (!supportTTS) {
|
||||||
|
Log.e(TAG, "PinPadTTS: play TTS failed, TTS not support...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (textToSpeech == null) {
|
||||||
|
Log.e(TAG, "PinPadTTS: play TTS slipped, textToSpeech not init..");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.e(TAG, "play() text: [" + text + "]");
|
||||||
|
textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart(String utteranceId) {
|
||||||
|
Log.e(TAG, "播放开始,utteranceId:" + utteranceId);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onStart(utteranceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDone(String utteranceId) {
|
||||||
|
Log.e(TAG, "播放结束,utteranceId:" + utteranceId);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onDone(utteranceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String utteranceId) {
|
||||||
|
Log.e(TAG, "播放出错,utteranceId:" + utteranceId);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onError(utteranceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop(String utteranceId, boolean interrupted) {
|
||||||
|
Log.e(TAG, "播放停止,utteranceId:" + utteranceId + ",interrupted:" + interrupted);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onStop(utteranceId, interrupted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
if (textToSpeech != null) {
|
||||||
|
int code = textToSpeech.stop();
|
||||||
|
Log.e(TAG, "tts stop() code:" + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSpeaking() {
|
||||||
|
if (textToSpeech != null) {
|
||||||
|
return textToSpeech.isSpeaking();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy() {
|
||||||
|
if (textToSpeech != null) {
|
||||||
|
textToSpeech.stop();
|
||||||
|
textToSpeech.shutdown();
|
||||||
|
textToSpeech = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TTS初始化回调 */
|
||||||
|
private void onTTSInit(int status) {
|
||||||
|
if (status != TextToSpeech.SUCCESS) {
|
||||||
|
LogUtil.e(TAG, "PinPadTTS: init TTS failed, status:" + status);
|
||||||
|
supportTTS = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateTtsLanguage();
|
||||||
|
if (supportTTS) {
|
||||||
|
textToSpeech.setPitch(1.0f);
|
||||||
|
textToSpeech.setSpeechRate(1.0f);
|
||||||
|
LogUtil.e(TAG, "onTTSInit() success,locale:" + textToSpeech.getVoice().getLocale());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新TTS语言 */
|
||||||
|
private void updateTtsLanguage() {
|
||||||
|
Locale locale = Locale.ENGLISH;
|
||||||
|
int result = textToSpeech.setLanguage(locale);
|
||||||
|
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||||
|
supportTTS = false; //系统不支持当前Locale对应的语音播报
|
||||||
|
LogUtil.e(TAG, "updateTtsLanguage() failed, TTS not support in locale:" + locale);
|
||||||
|
} else {
|
||||||
|
supportTTS = true;
|
||||||
|
LogUtil.e(TAG, "updateTtsLanguage() success, TTS locale:" + locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.bdkipoc.emv;
|
||||||
|
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
|
|
||||||
|
public interface ITTSProgressListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an utterance "starts" as perceived by the caller. This will
|
||||||
|
* be soon before audio is played back in the case of a {@link TextToSpeech#speak}
|
||||||
|
* or before the first bytes of a file are written to the file system in the case
|
||||||
|
* of {@link TextToSpeech#synthesizeToFile}.
|
||||||
|
*
|
||||||
|
* @param utteranceId The utterance ID of the utterance.
|
||||||
|
*/
|
||||||
|
void onStart(String utteranceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an utterance has successfully completed processing.
|
||||||
|
* All audio will have been played back by this point for audible output, and all
|
||||||
|
* output will have been written to disk for file synthesis requests.
|
||||||
|
* <p>
|
||||||
|
* This request is guaranteed to be called after {@link #onStart(String)}.
|
||||||
|
*
|
||||||
|
* @param utteranceId The utterance ID of the utterance.
|
||||||
|
*/
|
||||||
|
void onDone(String utteranceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an error has occurred during processing. This can be called
|
||||||
|
* at any point in the synthesis process. Note that there might be calls
|
||||||
|
* to {@link #onStart(String)} for specified utteranceId but there will never
|
||||||
|
* be a call to both {@link #onDone(String)} and {@link #onError(String)} for
|
||||||
|
* the same utterance.
|
||||||
|
*
|
||||||
|
* @param utteranceId The utterance ID of the utterance.
|
||||||
|
* @deprecated Use {@link #onError(String, int)} instead
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link #onError(String, int)} instead
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
void onError(String utteranceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an utterance has been stopped while in progress or flushed from the
|
||||||
|
* synthesis queue. This can happen if a client calls {@link TextToSpeech#stop()}
|
||||||
|
* or uses {@link TextToSpeech#QUEUE_FLUSH} as an argument with the
|
||||||
|
* {@link TextToSpeech#speak} or {@link TextToSpeech#synthesizeToFile} methods.
|
||||||
|
*
|
||||||
|
* @param utteranceId The utterance ID of the utterance.
|
||||||
|
* @param interrupted If true, then the utterance was interrupted while being synthesized
|
||||||
|
* and its output is incomplete. If false, then the utterance was flushed
|
||||||
|
* before the synthesis started.
|
||||||
|
*/
|
||||||
|
void onStop(String utteranceId, boolean interrupted);
|
||||||
|
}
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class HistoryActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private TextView tvTotalAmount;
|
||||||
|
private TextView tvTotalTransactions;
|
||||||
|
private TextView btnLihatDetailTop;
|
||||||
|
private Button btnLihatDetailBottom;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private HistoryAdapter adapter;
|
||||||
|
private List<HistoryItem> historyList;
|
||||||
|
private ImageView btnBack;
|
||||||
|
|
||||||
|
// Store full data for detail view
|
||||||
|
private static List<HistoryItem> fullHistoryData = new ArrayList<>();
|
||||||
|
|
||||||
|
private String API_URL = "https://be-edc.msvc.app/transactions?page=0&limit=50&sortOrder=DESC&from_date=2025-05-10&to_date=2025-05-21&location_id=0&merchant_id=0&sortColumn=id";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_history);
|
||||||
|
|
||||||
|
initViews();
|
||||||
|
setupRecyclerView();
|
||||||
|
fetchApiData();
|
||||||
|
setupClickListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews() {
|
||||||
|
tvTotalAmount = findViewById(R.id.tv_total_amount);
|
||||||
|
tvTotalTransactions = findViewById(R.id.tv_total_transactions);
|
||||||
|
btnLihatDetailTop = findViewById(R.id.btn_lihat_detail);
|
||||||
|
btnLihatDetailBottom = findViewById(R.id.btn_lihat_detail_bottom);
|
||||||
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
btnBack = findViewById(R.id.btn_back);
|
||||||
|
|
||||||
|
historyList = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupRecyclerView() {
|
||||||
|
adapter = new HistoryAdapter(historyList);
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupClickListeners() {
|
||||||
|
btnBack.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
View.OnClickListener detailClickListener = new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(HistoryActivity.this, HistoryDetailActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Toast.makeText(HistoryActivity.this, "Error opening detail", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
btnLihatDetailTop.setOnClickListener(detailClickListener);
|
||||||
|
btnLihatDetailBottom.setOnClickListener(detailClickListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchApiData() {
|
||||||
|
new ApiTask().execute(API_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processApiData(JSONArray dataArray) {
|
||||||
|
try {
|
||||||
|
historyList.clear();
|
||||||
|
fullHistoryData.clear(); // Clear static data
|
||||||
|
|
||||||
|
final long[] totalAmountArray = {0};
|
||||||
|
final int[] totalTransactionsArray = {0};
|
||||||
|
|
||||||
|
for (int i = 0; i < dataArray.length(); i++) {
|
||||||
|
JSONObject item = dataArray.getJSONObject(i);
|
||||||
|
|
||||||
|
String channelCode = item.getString("channel_code");
|
||||||
|
String amount = item.getString("amount");
|
||||||
|
String status = item.getString("status");
|
||||||
|
String transactionDate = item.getString("transaction_date");
|
||||||
|
String referenceId = item.getString("reference_id");
|
||||||
|
|
||||||
|
// Parse amount safely
|
||||||
|
double amountValue = 0;
|
||||||
|
try {
|
||||||
|
amountValue = Double.parseDouble(amount);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
amountValue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create history item
|
||||||
|
HistoryItem historyItem = new HistoryItem();
|
||||||
|
historyItem.setTime(formatTime(transactionDate));
|
||||||
|
historyItem.setDate(formatDate(transactionDate));
|
||||||
|
historyItem.setAmount((long) amountValue);
|
||||||
|
historyItem.setChannelName(formatChannelName(channelCode));
|
||||||
|
historyItem.setStatus(status);
|
||||||
|
historyItem.setReferenceId(referenceId);
|
||||||
|
historyItem.setFullDate(transactionDate);
|
||||||
|
historyItem.setChannelCode(channelCode);
|
||||||
|
|
||||||
|
// Add to full data
|
||||||
|
fullHistoryData.add(historyItem);
|
||||||
|
|
||||||
|
// Add first 10 to display list
|
||||||
|
if (i < 10) {
|
||||||
|
historyList.add(historyItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalAmountArray[0] += (long) amountValue;
|
||||||
|
totalTransactionsArray[0]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateSummary(totalAmountArray[0], totalTransactionsArray[0]);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(HistoryActivity.this, "Error parsing data", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSummary(long totalAmount, int totalTransactions) {
|
||||||
|
tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
|
||||||
|
tvTotalTransactions.setText(String.valueOf(totalTransactions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSampleData() {
|
||||||
|
historyList.clear();
|
||||||
|
fullHistoryData.clear();
|
||||||
|
|
||||||
|
// Create sample data
|
||||||
|
HistoryItem[] sampleData = {
|
||||||
|
new HistoryItem("03:44", "11-05-2025", 2018619, "Kredit", "FAILED", "197870"),
|
||||||
|
new HistoryItem("03:10", "12-05-2025", 3974866, "QRIS", "SUCCESS", "053059"),
|
||||||
|
new HistoryItem("15:17", "13-05-2025", 2418167, "QRIS", "FAILED", "668320"),
|
||||||
|
new HistoryItem("12:09", "11-05-2025", 3429230, "Debit", "FAILED", "454790"),
|
||||||
|
new HistoryItem("08:39", "10-05-2025", 4656447, "QRIS", "FAILED", "454248"),
|
||||||
|
new HistoryItem("00:35", "12-05-2025", 3507704, "QRIS", "FAILED", "301644"),
|
||||||
|
new HistoryItem("22:43", "13-05-2025", 4277904, "Debit", "SUCCESS", "388709"),
|
||||||
|
new HistoryItem("18:16", "11-05-2025", 4456904, "Debit", "FAILED", "986861")
|
||||||
|
};
|
||||||
|
|
||||||
|
for (HistoryItem item : sampleData) {
|
||||||
|
historyList.add(item);
|
||||||
|
fullHistoryData.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
tvTotalAmount.setText("RP 36.166.829");
|
||||||
|
tvTotalTransactions.setText("10");
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatChannelName(String channelCode) {
|
||||||
|
switch (channelCode) {
|
||||||
|
case "DEBIT":
|
||||||
|
return "Debit";
|
||||||
|
case "QRIS":
|
||||||
|
return "QRIS";
|
||||||
|
case "OTHER":
|
||||||
|
return "Kredit";
|
||||||
|
default:
|
||||||
|
return channelCode.substring(0, 1).toUpperCase() +
|
||||||
|
channelCode.substring(1).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTime(String isoDate) {
|
||||||
|
try {
|
||||||
|
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
||||||
|
SimpleDateFormat outputFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||||
|
Date date = inputFormat.parse(isoDate);
|
||||||
|
return outputFormat.format(date);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return "00:00";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDate(String isoDate) {
|
||||||
|
try {
|
||||||
|
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
||||||
|
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
|
||||||
|
Date date = inputFormat.parse(isoDate);
|
||||||
|
return outputFormat.format(date);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return "01-01-2025";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCurrency(long amount) {
|
||||||
|
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||||
|
return formatter.format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public static method to get full data for detail activity
|
||||||
|
public static List<HistoryItem> getFullHistoryData() {
|
||||||
|
return new ArrayList<>(fullHistoryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsyncTask for API call
|
||||||
|
private class ApiTask extends AsyncTask<String, Void, String> {
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(String... urls) {
|
||||||
|
try {
|
||||||
|
URL url = new URL(urls[0]);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setConnectTimeout(10000);
|
||||||
|
connection.setReadTimeout(10000);
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String result) {
|
||||||
|
if (result != null) {
|
||||||
|
try {
|
||||||
|
JSONObject jsonResponse = new JSONObject(result);
|
||||||
|
if (jsonResponse.getInt("status") == 200) {
|
||||||
|
JSONObject results = jsonResponse.getJSONObject("results");
|
||||||
|
JSONArray dataArray = results.getJSONArray("data");
|
||||||
|
processApiData(dataArray);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(HistoryActivity.this, "API Error", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData();
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Toast.makeText(HistoryActivity.this, "JSON Parse Error", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(HistoryActivity.this, "Network Error", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryItem class - enhanced with more fields
|
||||||
|
class HistoryItem {
|
||||||
|
private String time;
|
||||||
|
private String date;
|
||||||
|
private long amount;
|
||||||
|
private String channelName;
|
||||||
|
private String status;
|
||||||
|
private String referenceId;
|
||||||
|
private String fullDate;
|
||||||
|
private String channelCode;
|
||||||
|
|
||||||
|
public HistoryItem() {}
|
||||||
|
|
||||||
|
public HistoryItem(String time, String date, long amount, String channelName, String status, String referenceId) {
|
||||||
|
this.time = time;
|
||||||
|
this.date = date;
|
||||||
|
this.amount = amount;
|
||||||
|
this.channelName = channelName;
|
||||||
|
this.status = status;
|
||||||
|
this.referenceId = referenceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getTime() { return time; }
|
||||||
|
public void setTime(String time) { this.time = time; }
|
||||||
|
public String getDate() { return date; }
|
||||||
|
public void setDate(String date) { this.date = date; }
|
||||||
|
public long getAmount() { return amount; }
|
||||||
|
public void setAmount(long amount) { this.amount = amount; }
|
||||||
|
public String getChannelName() { return channelName; }
|
||||||
|
public void setChannelName(String channelName) { this.channelName = channelName; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
public String getReferenceId() { return referenceId; }
|
||||||
|
public void setReferenceId(String referenceId) { this.referenceId = referenceId; }
|
||||||
|
public String getFullDate() { return fullDate; }
|
||||||
|
public void setFullDate(String fullDate) { this.fullDate = fullDate; }
|
||||||
|
public String getChannelCode() { return channelCode; }
|
||||||
|
public void setChannelCode(String channelCode) { this.channelCode = channelCode; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryAdapter class - simplified and stable
|
||||||
|
class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.HistoryViewHolder> {
|
||||||
|
|
||||||
|
private List<HistoryItem> historyList;
|
||||||
|
|
||||||
|
public HistoryAdapter(List<HistoryItem> historyList) {
|
||||||
|
this.historyList = historyList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public HistoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_history, parent, false);
|
||||||
|
return new HistoryViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull HistoryViewHolder holder, int position) {
|
||||||
|
HistoryItem item = historyList.get(position);
|
||||||
|
|
||||||
|
holder.tvTime.setText(item.getTime() + ", " + item.getDate());
|
||||||
|
holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
|
||||||
|
holder.tvChannel.setText(item.getChannelName());
|
||||||
|
|
||||||
|
// Set status color
|
||||||
|
String status = item.getStatus();
|
||||||
|
if ("SUCCESS".equals(status)) {
|
||||||
|
holder.tvStatus.setText("Berhasil");
|
||||||
|
holder.tvStatus.setTextColor(0xFF4CAF50); // Green
|
||||||
|
} else if ("FAILED".equals(status)) {
|
||||||
|
holder.tvStatus.setText("Gagal");
|
||||||
|
holder.tvStatus.setTextColor(0xFFF44336); // Red
|
||||||
|
} else {
|
||||||
|
holder.tvStatus.setText("Tertunda");
|
||||||
|
holder.tvStatus.setTextColor(0xFFFF9800); // Orange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return historyList != null ? historyList.size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCurrency(long amount) {
|
||||||
|
try {
|
||||||
|
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||||
|
return formatter.format(amount);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return String.valueOf(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class HistoryViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView tvTime;
|
||||||
|
TextView tvAmount;
|
||||||
|
TextView tvChannel;
|
||||||
|
TextView tvStatus;
|
||||||
|
|
||||||
|
public HistoryViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
tvTime = itemView.findViewById(R.id.tv_time);
|
||||||
|
tvAmount = itemView.findViewById(R.id.tv_amount);
|
||||||
|
tvChannel = itemView.findViewById(R.id.tv_channel);
|
||||||
|
tvStatus = itemView.findViewById(R.id.tv_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class HistoryDetailActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private HistoryDetailAdapter adapter;
|
||||||
|
private List<HistoryItem> detailList;
|
||||||
|
private ImageView btnBack;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_history_detail);
|
||||||
|
|
||||||
|
initViews();
|
||||||
|
setupRecyclerView();
|
||||||
|
loadData();
|
||||||
|
setupClickListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews() {
|
||||||
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
btnBack = findViewById(R.id.btn_back);
|
||||||
|
detailList = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupRecyclerView() {
|
||||||
|
adapter = new HistoryDetailAdapter(detailList);
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupClickListeners() {
|
||||||
|
btnBack.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadData() {
|
||||||
|
try {
|
||||||
|
// Get data from HistoryActivity
|
||||||
|
List<HistoryItem> fullData = HistoryActivity.getFullHistoryData();
|
||||||
|
|
||||||
|
if (fullData != null && !fullData.isEmpty()) {
|
||||||
|
detailList.clear();
|
||||||
|
detailList.addAll(fullData);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
} else {
|
||||||
|
loadSampleDetailData();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
loadSampleDetailData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSampleDetailData() {
|
||||||
|
detailList.clear();
|
||||||
|
|
||||||
|
// Create sample detail data
|
||||||
|
HistoryItem[] sampleData = {
|
||||||
|
new HistoryItem("03:44", "11-05-2025", 2018619, "Kredit", "FAILED", "197870"),
|
||||||
|
new HistoryItem("03:10", "12-05-2025", 3974866, "QRIS", "SUCCESS", "053059"),
|
||||||
|
new HistoryItem("15:17", "13-05-2025", 2418167, "QRIS", "FAILED", "668320"),
|
||||||
|
new HistoryItem("12:09", "11-05-2025", 3429230, "Debit", "FAILED", "454790"),
|
||||||
|
new HistoryItem("08:39", "10-05-2025", 4656447, "QRIS", "FAILED", "454248"),
|
||||||
|
new HistoryItem("00:35", "12-05-2025", 3507704, "QRIS", "FAILED", "301644"),
|
||||||
|
new HistoryItem("22:43", "13-05-2025", 4277904, "Debit", "SUCCESS", "388709"),
|
||||||
|
new HistoryItem("18:16", "11-05-2025", 4456904, "Debit", "FAILED", "986861"),
|
||||||
|
new HistoryItem("12:51", "10-05-2025", 3027953, "Kredit", "SUCCESS", "771339"),
|
||||||
|
new HistoryItem("19:50", "14-05-2025", 4399035, "QRIS", "FAILED", "103478")
|
||||||
|
};
|
||||||
|
|
||||||
|
for (HistoryItem item : sampleData) {
|
||||||
|
detailList.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryDetailAdapter class - simplified for stability
|
||||||
|
class HistoryDetailAdapter extends RecyclerView.Adapter<HistoryDetailAdapter.DetailViewHolder> {
|
||||||
|
|
||||||
|
private List<HistoryItem> detailList;
|
||||||
|
|
||||||
|
public HistoryDetailAdapter(List<HistoryItem> detailList) {
|
||||||
|
this.detailList = detailList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DetailViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_history_detail, parent, false);
|
||||||
|
return new DetailViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull DetailViewHolder holder, int position) {
|
||||||
|
HistoryItem item = detailList.get(position);
|
||||||
|
|
||||||
|
try {
|
||||||
|
holder.tvReferenceId.setText("Ref: " + item.getReferenceId());
|
||||||
|
holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
|
||||||
|
holder.tvChannel.setText(item.getChannelName());
|
||||||
|
holder.tvMerchant.setText("TEST MERCHANT");
|
||||||
|
holder.tvTime.setText(formatDateTime(item.getTime(), item.getDate()));
|
||||||
|
holder.tvIssuer.setText("BANK MANDIRI");
|
||||||
|
|
||||||
|
// Set status color
|
||||||
|
String status = item.getStatus();
|
||||||
|
if ("SUCCESS".equals(status)) {
|
||||||
|
holder.tvStatus.setText("Berhasil");
|
||||||
|
holder.tvStatus.setTextColor(0xFF4CAF50); // Green
|
||||||
|
} else if ("FAILED".equals(status)) {
|
||||||
|
holder.tvStatus.setText("Gagal");
|
||||||
|
holder.tvStatus.setTextColor(0xFFF44336); // Red
|
||||||
|
} else {
|
||||||
|
holder.tvStatus.setText("Tertunda");
|
||||||
|
holder.tvStatus.setTextColor(0xFFFF9800); // Orange
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
// Set default values if error occurs
|
||||||
|
holder.tvReferenceId.setText("Ref: " + position);
|
||||||
|
holder.tvAmount.setText("Rp. 0");
|
||||||
|
holder.tvChannel.setText("Unknown");
|
||||||
|
holder.tvMerchant.setText("TEST MERCHANT");
|
||||||
|
holder.tvTime.setText("00:00, 01-01-2025");
|
||||||
|
holder.tvIssuer.setText("UNKNOWN");
|
||||||
|
holder.tvStatus.setText("Tidak Diketahui");
|
||||||
|
holder.tvStatus.setTextColor(0xFF666666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return detailList != null ? detailList.size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCurrency(long amount) {
|
||||||
|
try {
|
||||||
|
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||||
|
return formatter.format(amount);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return String.valueOf(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDateTime(String time, String date) {
|
||||||
|
try {
|
||||||
|
return time + ", " + date;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "00:00, 01-01-2025";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class DetailViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView tvReferenceId;
|
||||||
|
TextView tvAmount;
|
||||||
|
TextView tvChannel;
|
||||||
|
TextView tvMerchant;
|
||||||
|
TextView tvTime;
|
||||||
|
TextView tvIssuer;
|
||||||
|
TextView tvStatus;
|
||||||
|
|
||||||
|
public DetailViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
try {
|
||||||
|
tvReferenceId = itemView.findViewById(R.id.tv_reference_id);
|
||||||
|
tvAmount = itemView.findViewById(R.id.tv_amount);
|
||||||
|
tvChannel = itemView.findViewById(R.id.tv_channel);
|
||||||
|
tvMerchant = itemView.findViewById(R.id.tv_merchant);
|
||||||
|
tvTime = itemView.findViewById(R.id.tv_time);
|
||||||
|
tvIssuer = itemView.findViewById(R.id.tv_issuer);
|
||||||
|
tvStatus = itemView.findViewById(R.id.tv_status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
894
app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java
Normal file
@@ -0,0 +1,894 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class QrisActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private Button initiatePaymentButton;
|
||||||
|
private TextView statusTextView;
|
||||||
|
private EditText editTextAmount;
|
||||||
|
private TextView referenceIdTextView;
|
||||||
|
private LinearLayout backNavigation;
|
||||||
|
|
||||||
|
// Numpad buttons
|
||||||
|
private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000, btnDelete;
|
||||||
|
private TextView descriptionText;
|
||||||
|
|
||||||
|
private String transactionId;
|
||||||
|
private String transactionUuid;
|
||||||
|
private String referenceId;
|
||||||
|
private int amount;
|
||||||
|
private JSONObject midtransResponse;
|
||||||
|
|
||||||
|
private StringBuilder currentAmount = new StringBuilder();
|
||||||
|
|
||||||
|
// ✅ FRONTEND DEDUPLICATION: Add SharedPreferences for tracking
|
||||||
|
private SharedPreferences transactionPrefs;
|
||||||
|
private static final String PREF_RECENT_REFERENCES = "recent_references";
|
||||||
|
private static final String PREF_LAST_TRANSACTION_TIME = "last_transaction_time";
|
||||||
|
private static final String PREF_CURRENT_REFERENCE = "current_reference";
|
||||||
|
private static final String PREF_LAST_SUCCESSFUL_TX = "last_successful_tx";
|
||||||
|
private static final long REFERENCE_COOLDOWN_MS = 60000; // 1 minute cooldown
|
||||||
|
private static final long TRANSACTION_COOLDOWN_MS = 5000; // 5 second cooldown
|
||||||
|
|
||||||
|
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
|
||||||
|
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
|
||||||
|
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=";
|
||||||
|
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_qris);
|
||||||
|
|
||||||
|
// ✅ Initialize SharedPreferences for duplicate prevention
|
||||||
|
transactionPrefs = getSharedPreferences("qris_transactions", MODE_PRIVATE);
|
||||||
|
|
||||||
|
// Initialize views
|
||||||
|
progressBar = findViewById(R.id.progressBar);
|
||||||
|
initiatePaymentButton = findViewById(R.id.initiatePaymentButton);
|
||||||
|
statusTextView = findViewById(R.id.statusTextView);
|
||||||
|
editTextAmount = findViewById(R.id.editTextAmount);
|
||||||
|
referenceIdTextView = findViewById(R.id.referenceIdTextView);
|
||||||
|
backNavigation = findViewById(R.id.back_navigation);
|
||||||
|
descriptionText = findViewById(R.id.descriptionText);
|
||||||
|
|
||||||
|
// Initialize numpad buttons
|
||||||
|
btn1 = findViewById(R.id.btn1);
|
||||||
|
btn2 = findViewById(R.id.btn2);
|
||||||
|
btn3 = findViewById(R.id.btn3);
|
||||||
|
btn4 = findViewById(R.id.btn4);
|
||||||
|
btn5 = findViewById(R.id.btn5);
|
||||||
|
btn6 = findViewById(R.id.btn6);
|
||||||
|
btn7 = findViewById(R.id.btn7);
|
||||||
|
btn8 = findViewById(R.id.btn8);
|
||||||
|
btn9 = findViewById(R.id.btn9);
|
||||||
|
btn0 = findViewById(R.id.btn0);
|
||||||
|
btn000 = findViewById(R.id.btn000);
|
||||||
|
btnDelete = findViewById(R.id.btnDelete);
|
||||||
|
|
||||||
|
// ✅ Generate unique reference ID with duplicate prevention
|
||||||
|
referenceId = generateUniqueReferenceId();
|
||||||
|
referenceIdTextView.setText(referenceId);
|
||||||
|
|
||||||
|
// Set up click listeners
|
||||||
|
initiatePaymentButton.setOnClickListener(v -> createTransaction());
|
||||||
|
backNavigation.setOnClickListener(v -> finish());
|
||||||
|
|
||||||
|
// Set up numpad listeners
|
||||||
|
setupNumpadListeners();
|
||||||
|
|
||||||
|
// Initially disable the button
|
||||||
|
initiatePaymentButton.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ FRONTEND DEDUPLICATION: Generate unique reference ID with local tracking
|
||||||
|
*/
|
||||||
|
private String generateUniqueReferenceId() {
|
||||||
|
Log.d("QrisActivity", "🔄 Generating unique reference ID...");
|
||||||
|
|
||||||
|
String baseRef = "ref-" + generateRandomString(8);
|
||||||
|
|
||||||
|
// Check if this reference was recently created
|
||||||
|
String recentRefs = transactionPrefs.getString(PREF_RECENT_REFERENCES, "");
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Clean up old references (older than cooldown period)
|
||||||
|
StringBuilder validRefs = new StringBuilder();
|
||||||
|
if (!recentRefs.isEmpty()) {
|
||||||
|
String[] refs = recentRefs.split(",");
|
||||||
|
|
||||||
|
for (String refEntry : refs) {
|
||||||
|
if (refEntry.contains(":")) {
|
||||||
|
String[] parts = refEntry.split(":");
|
||||||
|
if (parts.length == 2) {
|
||||||
|
try {
|
||||||
|
long timestamp = Long.parseLong(parts[1]);
|
||||||
|
if (currentTime - timestamp < REFERENCE_COOLDOWN_MS) {
|
||||||
|
// Reference is still in cooldown period
|
||||||
|
if (validRefs.length() > 0) validRefs.append(",");
|
||||||
|
validRefs.append(refEntry);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Skip invalid entries
|
||||||
|
Log.w("QrisActivity", "Invalid reference entry: " + refEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if baseRef already exists in recent references
|
||||||
|
if (validRefs.length() > 0) {
|
||||||
|
String[] validRefArray = validRefs.toString().split(",");
|
||||||
|
for (String refEntry : validRefArray) {
|
||||||
|
if (refEntry.startsWith(baseRef + ":")) {
|
||||||
|
// Reference already exists, generate a new one
|
||||||
|
Log.w("QrisActivity", "⚠️ Reference " + baseRef + " recently used, generating new one");
|
||||||
|
return generateUniqueReferenceId(); // Recursive call with new random string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this reference to recent references
|
||||||
|
if (validRefs.length() > 0) validRefs.append(",");
|
||||||
|
validRefs.append(baseRef).append(":").append(currentTime);
|
||||||
|
|
||||||
|
// Save updated references
|
||||||
|
transactionPrefs.edit()
|
||||||
|
.putString(PREF_RECENT_REFERENCES, validRefs.toString())
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
Log.d("QrisActivity", "✅ Generated unique reference: " + baseRef);
|
||||||
|
return baseRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ FRONTEND DEDUPLICATION: Check if transaction is currently being processed
|
||||||
|
*/
|
||||||
|
private boolean isTransactionInProgress() {
|
||||||
|
long lastTransactionTime = transactionPrefs.getLong(PREF_LAST_TRANSACTION_TIME, 0);
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// If last transaction was less than cooldown period, consider it in progress
|
||||||
|
boolean inProgress = (currentTime - lastTransactionTime) < TRANSACTION_COOLDOWN_MS;
|
||||||
|
|
||||||
|
if (inProgress) {
|
||||||
|
Log.w("QrisActivity", "⏸️ Transaction in progress, cooldown active");
|
||||||
|
}
|
||||||
|
|
||||||
|
return inProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ FRONTEND DEDUPLICATION: Mark transaction processing status
|
||||||
|
*/
|
||||||
|
private void markTransactionInProgress(boolean inProgress) {
|
||||||
|
SharedPreferences.Editor editor = transactionPrefs.edit();
|
||||||
|
|
||||||
|
if (inProgress) {
|
||||||
|
editor.putLong(PREF_LAST_TRANSACTION_TIME, System.currentTimeMillis())
|
||||||
|
.putString(PREF_CURRENT_REFERENCE, referenceId);
|
||||||
|
Log.d("QrisActivity", "🔒 Marked transaction in progress: " + referenceId);
|
||||||
|
} else {
|
||||||
|
editor.remove(PREF_CURRENT_REFERENCE);
|
||||||
|
Log.d("QrisActivity", "🔓 Cleared transaction progress status");
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ FRONTEND DEDUPLICATION: Save successful transaction for future reference
|
||||||
|
*/
|
||||||
|
private void saveSuccessfulTransaction() {
|
||||||
|
try {
|
||||||
|
JSONObject txData = new JSONObject();
|
||||||
|
txData.put("reference_id", referenceId);
|
||||||
|
txData.put("transaction_uuid", transactionUuid);
|
||||||
|
txData.put("amount", amount);
|
||||||
|
txData.put("created_at", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// Save to SharedPreferences
|
||||||
|
transactionPrefs.edit()
|
||||||
|
.putString(PREF_LAST_SUCCESSFUL_TX, txData.toString())
|
||||||
|
.putLong("last_success_time", System.currentTimeMillis())
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
Log.d("QrisActivity", "💾 Saved successful transaction: " + referenceId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w("QrisActivity", "Failed to save transaction data: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ FRONTEND DEDUPLICATION: Create client info for better backend tracking
|
||||||
|
*/
|
||||||
|
private JSONObject createClientInfo() {
|
||||||
|
try {
|
||||||
|
JSONObject clientInfo = new JSONObject();
|
||||||
|
clientInfo.put("app_version", "1.0.0");
|
||||||
|
clientInfo.put("platform", "android");
|
||||||
|
clientInfo.put("timestamp", System.currentTimeMillis());
|
||||||
|
clientInfo.put("session_id", generateRandomString(16));
|
||||||
|
clientInfo.put("reference_generation_time", System.currentTimeMillis());
|
||||||
|
return clientInfo;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.w("QrisActivity", "Failed to create client info: " + e.getMessage());
|
||||||
|
return new JSONObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupNumpadListeners() {
|
||||||
|
View.OnClickListener numberClickListener = v -> {
|
||||||
|
TextView button = (TextView) v;
|
||||||
|
String number = button.getText().toString();
|
||||||
|
appendNumber(number);
|
||||||
|
};
|
||||||
|
|
||||||
|
btn1.setOnClickListener(numberClickListener);
|
||||||
|
btn2.setOnClickListener(numberClickListener);
|
||||||
|
btn3.setOnClickListener(numberClickListener);
|
||||||
|
btn4.setOnClickListener(numberClickListener);
|
||||||
|
btn5.setOnClickListener(numberClickListener);
|
||||||
|
btn6.setOnClickListener(numberClickListener);
|
||||||
|
btn7.setOnClickListener(numberClickListener);
|
||||||
|
btn8.setOnClickListener(numberClickListener);
|
||||||
|
btn9.setOnClickListener(numberClickListener);
|
||||||
|
btn0.setOnClickListener(numberClickListener);
|
||||||
|
btn000.setOnClickListener(numberClickListener);
|
||||||
|
|
||||||
|
btnDelete.setOnClickListener(v -> deleteLastDigit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendNumber(String number) {
|
||||||
|
currentAmount.append(number);
|
||||||
|
updateAmountDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteLastDigit() {
|
||||||
|
if (currentAmount.length() > 0) {
|
||||||
|
currentAmount.deleteCharAt(currentAmount.length() - 1);
|
||||||
|
updateAmountDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAmountDisplay() {
|
||||||
|
String amountStr = currentAmount.toString();
|
||||||
|
|
||||||
|
if (amountStr.isEmpty()) {
|
||||||
|
editTextAmount.setVisibility(View.GONE);
|
||||||
|
descriptionText.setText("Pastikan kembali nominal pembayaran pelanggan Anda");
|
||||||
|
initiatePaymentButton.setEnabled(false);
|
||||||
|
} else {
|
||||||
|
editTextAmount.setVisibility(View.VISIBLE);
|
||||||
|
editTextAmount.setText(formatAmount(amountStr));
|
||||||
|
descriptionText.setText("Tekan Konfirmasi untuk melanjutkan");
|
||||||
|
|
||||||
|
// Enable button if amount is valid
|
||||||
|
try {
|
||||||
|
int amt = Integer.parseInt(amountStr);
|
||||||
|
initiatePaymentButton.setEnabled(amt >= 1000);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
initiatePaymentButton.setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatAmount(String amount) {
|
||||||
|
if (amount.isEmpty()) return "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
long num = Long.parseLong(amount);
|
||||||
|
return String.format("%,d", num);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ ENHANCED: Modified createTransaction with comprehensive duplicate prevention
|
||||||
|
*/
|
||||||
|
private void createTransaction() {
|
||||||
|
if (currentAmount.length() == 0) {
|
||||||
|
Toast.makeText(this, "Masukkan jumlah pembayaran", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ FRONTEND CHECK: Prevent rapid duplicate submissions
|
||||||
|
if (isTransactionInProgress()) {
|
||||||
|
Toast.makeText(this, "Transaksi sedang diproses, harap tunggu...", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("QrisActivity", "🚀 Starting transaction creation process");
|
||||||
|
Log.d("QrisActivity", " Reference ID: " + referenceId);
|
||||||
|
Log.d("QrisActivity", " Amount: " + currentAmount.toString());
|
||||||
|
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
initiatePaymentButton.setEnabled(false);
|
||||||
|
statusTextView.setVisibility(View.VISIBLE);
|
||||||
|
statusTextView.setText("Creating transaction...");
|
||||||
|
|
||||||
|
// Mark transaction as in progress
|
||||||
|
markTransactionInProgress(true);
|
||||||
|
|
||||||
|
new CreateTransactionTask().execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRandomString(int length) {
|
||||||
|
String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Random random = new Random();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int index = random.nextInt(chars.length());
|
||||||
|
sb.append(chars.charAt(index));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getServerKey() {
|
||||||
|
try {
|
||||||
|
// MIDTRANS_AUTH = 'Basic base64string'
|
||||||
|
String base64 = MIDTRANS_AUTH.replace("Basic ", "");
|
||||||
|
byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
|
||||||
|
String decodedString = new String(decoded);
|
||||||
|
// Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present.
|
||||||
|
return decodedString.replace(":", "");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("MidtransCharge", "Error decoding server key: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidServerKey(String serverKey) {
|
||||||
|
return serverKey != null &&
|
||||||
|
serverKey.startsWith("SB-Mid-server-") &&
|
||||||
|
serverKey.length() > 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
|
||||||
|
String input = orderId + statusCode + grossAmount + serverKey;
|
||||||
|
try {
|
||||||
|
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512");
|
||||||
|
byte[] messageDigest = md.digest(input.getBytes());
|
||||||
|
StringBuilder hexString = new StringBuilder();
|
||||||
|
for (byte b : messageDigest) {
|
||||||
|
String hex = Integer.toHexString(0xff & b);
|
||||||
|
if (hex.length() == 1) hexString.append('0');
|
||||||
|
hexString.append(hex);
|
||||||
|
}
|
||||||
|
return hexString.toString();
|
||||||
|
} catch (java.security.NoSuchAlgorithmException e) {
|
||||||
|
Log.e("MidtransCharge", "Error generating signature: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
if (item.getItemId() == android.R.id.home) {
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... voids) {
|
||||||
|
try {
|
||||||
|
// Generate a UUID for the transaction
|
||||||
|
transactionUuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
// ✅ ENHANCED LOGGING: Better tracking for debugging
|
||||||
|
Log.d("MidtransCharge", "=== TRANSACTION CREATION START ===");
|
||||||
|
Log.d("MidtransCharge", "Reference ID: " + referenceId);
|
||||||
|
Log.d("MidtransCharge", "Transaction UUID: " + transactionUuid);
|
||||||
|
Log.d("MidtransCharge", "Timestamp: " + System.currentTimeMillis());
|
||||||
|
|
||||||
|
// Create transaction JSON payload
|
||||||
|
JSONObject payload = new JSONObject();
|
||||||
|
payload.put("type", "PAYMENT");
|
||||||
|
payload.put("channel_category", "RETAIL_OUTLET");
|
||||||
|
payload.put("channel_code", "QRIS");
|
||||||
|
payload.put("reference_id", referenceId);
|
||||||
|
|
||||||
|
// ✅ FRONTEND ENHANCEMENT: Add client-side metadata for better tracking
|
||||||
|
payload.put("client_info", createClientInfo());
|
||||||
|
payload.put("is_initial_creation", true); // Mark as initial creation
|
||||||
|
|
||||||
|
// Get amount from current input
|
||||||
|
String amountText = currentAmount.toString();
|
||||||
|
Log.d("MidtransCharge", "Raw amount text: " + amountText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
|
||||||
|
amount = Integer.parseInt(amountText);
|
||||||
|
|
||||||
|
// Validate minimum amount
|
||||||
|
if (amount < 1000) {
|
||||||
|
errorMessage = "Minimum amount is IDR 1,000";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate maximum amount for testing
|
||||||
|
if (amount > 10000000) {
|
||||||
|
errorMessage = "Maximum amount is IDR 10,000,000";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "Parsed amount: " + amount);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Log.e("MidtransCharge", "Amount parsing error: " + e.getMessage());
|
||||||
|
errorMessage = "Invalid amount format. Please enter numbers only.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.put("amount", amount);
|
||||||
|
payload.put("cashflow", "MONEY_IN");
|
||||||
|
payload.put("status", "INIT");
|
||||||
|
payload.put("device_id", 1);
|
||||||
|
payload.put("transaction_uuid", transactionUuid);
|
||||||
|
payload.put("transaction_time_seconds", 0.0);
|
||||||
|
payload.put("device_code", "PB4K252T00021");
|
||||||
|
payload.put("merchant_name", "Marcel Panjaitan");
|
||||||
|
payload.put("mid", "71000026521");
|
||||||
|
payload.put("tid", "73001500");
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "Backend transaction payload: " + payload.toString());
|
||||||
|
|
||||||
|
// Make the API call
|
||||||
|
URL url = new URI(BACKEND_BASE + "/transactions").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
|
||||||
|
|
||||||
|
// ✅ FRONTEND ENHANCEMENT: Add client headers for better backend tracking
|
||||||
|
conn.setRequestProperty("X-Client-Reference", referenceId);
|
||||||
|
conn.setRequestProperty("X-Client-Timestamp", String.valueOf(System.currentTimeMillis()));
|
||||||
|
conn.setRequestProperty("X-Client-Version", "1.0.0");
|
||||||
|
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
byte[] input = payload.toString().getBytes("utf-8");
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
Log.d("MidtransCharge", "Backend response code: " + responseCode);
|
||||||
|
|
||||||
|
if (responseCode == 200 || responseCode == 201) {
|
||||||
|
// Success - process response
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String responseLine;
|
||||||
|
while ((responseLine = br.readLine()) != null) {
|
||||||
|
response.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "Backend success response: " + response.toString());
|
||||||
|
|
||||||
|
// Parse the response to get transaction ID
|
||||||
|
JSONObject jsonResponse = new JSONObject(response.toString());
|
||||||
|
JSONObject data = jsonResponse.getJSONObject("data");
|
||||||
|
transactionId = String.valueOf(data.getInt("id"));
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "✅ Created transaction ID: " + transactionId);
|
||||||
|
|
||||||
|
// ✅ FRONTEND SUCCESS: Save successful transaction info
|
||||||
|
saveSuccessfulTransaction();
|
||||||
|
|
||||||
|
// Now generate QRIS via Midtrans
|
||||||
|
return generateQris(amount);
|
||||||
|
|
||||||
|
} else if (responseCode == 409 || responseCode == 400) {
|
||||||
|
// ✅ ENHANCED DUPLICATE HANDLING: Handle gracefully
|
||||||
|
Log.w("MidtransCharge", "⚠️ Potential duplicate detected (HTTP " + responseCode + ")");
|
||||||
|
|
||||||
|
// Try to read and parse error response
|
||||||
|
try {
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
||||||
|
StringBuilder errorResponse = new StringBuilder();
|
||||||
|
String responseLine;
|
||||||
|
while ((responseLine = br.readLine()) != null) {
|
||||||
|
errorResponse.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
String errorResponseStr = errorResponse.toString();
|
||||||
|
Log.d("MidtransCharge", "Error response: " + errorResponseStr);
|
||||||
|
|
||||||
|
// Check if it's actually a duplicate reference error
|
||||||
|
if (errorResponseStr.toLowerCase().contains("duplicate") ||
|
||||||
|
errorResponseStr.toLowerCase().contains("already exists") ||
|
||||||
|
errorResponseStr.toLowerCase().contains("reference") ||
|
||||||
|
responseCode == 409) {
|
||||||
|
|
||||||
|
Log.i("MidtransCharge", "✅ Confirmed duplicate reference - proceeding with QRIS generation");
|
||||||
|
|
||||||
|
// For duplicates, we can still generate QRIS with existing reference
|
||||||
|
return generateQris(amount);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w("MidtransCharge", "Could not parse error response: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't determine the exact error, try QRIS generation anyway
|
||||||
|
Log.i("MidtransCharge", "🔄 Proceeding with QRIS generation despite backend error");
|
||||||
|
return generateQris(amount);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Other HTTP errors
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String responseLine;
|
||||||
|
while ((responseLine = br.readLine()) != null) {
|
||||||
|
response.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
String errorResponse = response.toString();
|
||||||
|
Log.e("MidtransCharge", "❌ Backend error (HTTP " + responseCode + "): " + errorResponse);
|
||||||
|
errorMessage = "Backend error (" + responseCode + "): " + errorResponse;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("MidtransCharge", "❌ Backend transaction exception: " + e.getMessage(), e);
|
||||||
|
errorMessage = "Network error: " + e.getMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean generateQris(int amount) {
|
||||||
|
try {
|
||||||
|
// Validate server key first
|
||||||
|
String serverKey = getServerKey();
|
||||||
|
if (!isValidServerKey(serverKey)) {
|
||||||
|
Log.e("MidtransCharge", "Invalid server key format");
|
||||||
|
errorMessage = "Invalid server key configuration";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "Using server key: " + serverKey.substring(0, Math.min(20, serverKey.length())) + "...");
|
||||||
|
|
||||||
|
// Create QRIS charge JSON payload
|
||||||
|
JSONObject payload = new JSONObject();
|
||||||
|
payload.put("payment_type", "qris");
|
||||||
|
|
||||||
|
JSONObject transactionDetails = new JSONObject();
|
||||||
|
transactionDetails.put("order_id", transactionUuid);
|
||||||
|
transactionDetails.put("gross_amount", amount);
|
||||||
|
payload.put("transaction_details", transactionDetails);
|
||||||
|
|
||||||
|
// Add customer details (recommended for better success rate)
|
||||||
|
JSONObject customerDetails = new JSONObject();
|
||||||
|
customerDetails.put("first_name", "Test");
|
||||||
|
customerDetails.put("last_name", "Customer");
|
||||||
|
customerDetails.put("email", "test@example.com");
|
||||||
|
customerDetails.put("phone", "081234567890");
|
||||||
|
payload.put("customer_details", customerDetails);
|
||||||
|
|
||||||
|
// Add item details (optional but recommended)
|
||||||
|
JSONArray itemDetails = new JSONArray();
|
||||||
|
JSONObject item = new JSONObject();
|
||||||
|
item.put("id", "item1");
|
||||||
|
item.put("price", amount);
|
||||||
|
item.put("quantity", 1);
|
||||||
|
item.put("name", "QRIS Payment - " + referenceId);
|
||||||
|
itemDetails.put(item);
|
||||||
|
payload.put("item_details", itemDetails);
|
||||||
|
|
||||||
|
// ✅ FRONTEND ENHANCEMENT: Add tracking info for reference linkage
|
||||||
|
JSONObject customField1 = new JSONObject();
|
||||||
|
customField1.put("app_reference_id", referenceId);
|
||||||
|
customField1.put("creation_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date()));
|
||||||
|
customField1.put("client_version", "1.0.0");
|
||||||
|
payload.put("custom_field1", customField1.toString());
|
||||||
|
|
||||||
|
// Log the request details
|
||||||
|
Log.d("MidtransCharge", "=== MIDTRANS QRIS REQUEST ===");
|
||||||
|
Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
|
||||||
|
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
|
||||||
|
Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL);
|
||||||
|
Log.d("MidtransCharge", "Reference ID: " + referenceId);
|
||||||
|
Log.d("MidtransCharge", "Order ID: " + transactionUuid);
|
||||||
|
Log.d("MidtransCharge", "Amount: " + amount);
|
||||||
|
Log.d("MidtransCharge", "================================");
|
||||||
|
|
||||||
|
// Make the API call to Midtrans
|
||||||
|
URL url = new URI(MIDTRANS_CHARGE_URL).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
|
||||||
|
conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL);
|
||||||
|
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(30000); // 30 seconds
|
||||||
|
conn.setReadTimeout(30000); // 30 seconds
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
byte[] input = payload.toString().getBytes("utf-8");
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
Log.d("MidtransCharge", "Midtrans HTTP Response Code: " + responseCode);
|
||||||
|
|
||||||
|
if (responseCode == 200 || responseCode == 201) {
|
||||||
|
InputStream inputStream = conn.getInputStream();
|
||||||
|
if (inputStream != null) {
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String responseLine;
|
||||||
|
while ((responseLine = br.readLine()) != null) {
|
||||||
|
response.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "Midtrans Success Response: " + response.toString());
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
midtransResponse = new JSONObject(response.toString());
|
||||||
|
|
||||||
|
// Check if response contains error within success response
|
||||||
|
if (midtransResponse.has("status_code")) {
|
||||||
|
String statusCode = midtransResponse.getString("status_code");
|
||||||
|
if (!statusCode.equals("201")) {
|
||||||
|
String statusMessage = midtransResponse.optString("status_message", "Unknown error");
|
||||||
|
Log.e("MidtransCharge", "Midtrans Error in response: " + statusCode + " - " + statusMessage);
|
||||||
|
errorMessage = "Midtrans Error: " + statusMessage + " (Code: " + statusCode + ")";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response has required fields
|
||||||
|
if (!midtransResponse.has("actions") ||
|
||||||
|
!midtransResponse.has("transaction_id") ||
|
||||||
|
!midtransResponse.has("gross_amount")) {
|
||||||
|
Log.e("MidtransCharge", "Missing required fields in Midtrans response");
|
||||||
|
errorMessage = "Invalid response from Midtrans - missing required fields";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "✅ QRIS generation successful!");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
|
||||||
|
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No input stream available";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InputStream errorStream = conn.getErrorStream();
|
||||||
|
if (errorStream != null) {
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8"));
|
||||||
|
StringBuilder errorResponse = new StringBuilder();
|
||||||
|
String responseLine;
|
||||||
|
while ((responseLine = br.readLine()) != null) {
|
||||||
|
errorResponse.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e("MidtransCharge", "Midtrans HTTP " + responseCode + ": " + errorResponse.toString());
|
||||||
|
|
||||||
|
// Try to parse error JSON for better error message
|
||||||
|
try {
|
||||||
|
JSONObject errorJson = new JSONObject(errorResponse.toString());
|
||||||
|
|
||||||
|
// Handle different error response formats
|
||||||
|
String errorMessage = "";
|
||||||
|
if (errorJson.has("error_messages")) {
|
||||||
|
errorMessage = errorJson.optString("error_messages", "Unknown error");
|
||||||
|
} else if (errorJson.has("status_message")) {
|
||||||
|
errorMessage = errorJson.optString("status_message", "Unknown error");
|
||||||
|
} else if (errorJson.has("message")) {
|
||||||
|
errorMessage = errorJson.optString("message", "Unknown error");
|
||||||
|
} else {
|
||||||
|
errorMessage = errorResponse.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorMessage = "Midtrans Error: " + errorMessage;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
this.errorMessage = "HTTP " + responseCode + ": " + errorResponse.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available");
|
||||||
|
this.errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("MidtransCharge", "Midtrans QRIS generation exception: " + e.getMessage(), e);
|
||||||
|
errorMessage = "Network error: " + e.getMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean success) {
|
||||||
|
// ✅ FRONTEND CLEANUP: Always clear in-progress status
|
||||||
|
markTransactionInProgress(false);
|
||||||
|
|
||||||
|
if (success && midtransResponse != null) {
|
||||||
|
try {
|
||||||
|
// Extract needed values from midtransResponse
|
||||||
|
JSONArray actionsArray = midtransResponse.getJSONArray("actions");
|
||||||
|
if (actionsArray.length() == 0) {
|
||||||
|
Log.e("MidtransCharge", "No actions found in Midtrans response");
|
||||||
|
Toast.makeText(QrisActivity.this, "Error: No QR code URL found in response", Toast.LENGTH_LONG).show();
|
||||||
|
initiatePaymentButton.setEnabled(true);
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
statusTextView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject actions = actionsArray.getJSONObject(0);
|
||||||
|
String qrImageUrl = actions.getString("url");
|
||||||
|
|
||||||
|
// Extract transaction_id
|
||||||
|
String transactionId = midtransResponse.getString("transaction_id");
|
||||||
|
String transactionTime = midtransResponse.getString("transaction_time");
|
||||||
|
String acquirer = midtransResponse.getString("acquirer");
|
||||||
|
String merchantId = midtransResponse.getString("merchant_id");
|
||||||
|
|
||||||
|
// Send raw amount as string without decimal conversion
|
||||||
|
String rawAmountString = String.valueOf(amount); // Keep original integer amount
|
||||||
|
|
||||||
|
// Log everything before launching activity
|
||||||
|
Log.d("MidtransCharge", "=== LAUNCHING QRIS RESULT ACTIVITY ===");
|
||||||
|
Log.d("MidtransCharge", "✅ Transaction created successfully!");
|
||||||
|
Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl);
|
||||||
|
Log.d("MidtransCharge", "amount (raw): " + amount);
|
||||||
|
Log.d("MidtransCharge", "rawAmountString: " + rawAmountString);
|
||||||
|
Log.d("MidtransCharge", "referenceId: " + referenceId);
|
||||||
|
Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid);
|
||||||
|
Log.d("MidtransCharge", "transaction_id: " + transactionId);
|
||||||
|
Log.d("MidtransCharge", "transactionTime: " + transactionTime);
|
||||||
|
Log.d("MidtransCharge", "acquirer: " + acquirer);
|
||||||
|
Log.d("MidtransCharge", "merchantId: " + merchantId);
|
||||||
|
Log.d("MidtransCharge", "========================================");
|
||||||
|
|
||||||
|
// ✅ FINAL SUCCESS: Update transaction status in preferences
|
||||||
|
transactionPrefs.edit()
|
||||||
|
.putString("last_qris_url", qrImageUrl)
|
||||||
|
.putString("last_qris_reference", referenceId)
|
||||||
|
.putLong("last_qris_time", System.currentTimeMillis())
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
// Launch QrisResultActivity
|
||||||
|
Intent intent = new Intent(QrisActivity.this, QrisResultActivity.class);
|
||||||
|
intent.putExtra("qrImageUrl", qrImageUrl);
|
||||||
|
intent.putExtra("amount", amount); // Keep as int
|
||||||
|
intent.putExtra("referenceId", referenceId);
|
||||||
|
intent.putExtra("orderId", transactionUuid); // Order ID
|
||||||
|
intent.putExtra("transactionId", transactionId); // Actual Midtrans transaction_id
|
||||||
|
intent.putExtra("grossAmount", rawAmountString); // Raw amount as string (no decimals)
|
||||||
|
intent.putExtra("transactionTime", transactionTime); // For timestamp
|
||||||
|
intent.putExtra("acquirer", acquirer);
|
||||||
|
intent.putExtra("merchantId", merchantId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(intent);
|
||||||
|
finish(); // Close QrisActivity
|
||||||
|
|
||||||
|
Log.d("MidtransCharge", "🎉 Successfully launched QrisResultActivity");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e);
|
||||||
|
Toast.makeText(QrisActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
// Re-enable button on error
|
||||||
|
initiatePaymentButton.setEnabled(true);
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
statusTextView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
|
||||||
|
Toast.makeText(QrisActivity.this, "Error processing QRIS response: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle error case
|
||||||
|
String message = (errorMessage != null && !errorMessage.isEmpty()) ?
|
||||||
|
errorMessage : "Unknown error occurred. Please check your connection and try again.";
|
||||||
|
|
||||||
|
Log.e("MidtransCharge", "❌ Transaction failed: " + message);
|
||||||
|
Toast.makeText(QrisActivity.this, message, Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
// Re-enable button for retry
|
||||||
|
initiatePaymentButton.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always hide progress indicators
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
statusTextView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
// ✅ CLEANUP: Clear any in-progress status when activity is destroyed
|
||||||
|
markTransactionInProgress(false);
|
||||||
|
Log.d("QrisActivity", "🧹 QrisActivity destroyed, cleared progress status");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
// Keep progress status when paused (user might come back)
|
||||||
|
Log.d("QrisActivity", "⏸️ QrisActivity paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
Log.d("QrisActivity", "▶️ QrisActivity resumed");
|
||||||
|
|
||||||
|
// Check if there's a recent successful transaction
|
||||||
|
String lastSuccessfulTx = transactionPrefs.getString(PREF_LAST_SUCCESSFUL_TX, "");
|
||||||
|
if (!lastSuccessfulTx.isEmpty()) {
|
||||||
|
try {
|
||||||
|
JSONObject txData = new JSONObject(lastSuccessfulTx);
|
||||||
|
String lastRef = txData.getString("reference_id");
|
||||||
|
long lastTime = txData.getLong("created_at");
|
||||||
|
|
||||||
|
// If last successful transaction was recent (within 5 minutes) and same reference
|
||||||
|
if (System.currentTimeMillis() - lastTime < 300000 && lastRef.equals(referenceId)) {
|
||||||
|
Log.d("QrisActivity", "🔄 Recent successful transaction detected for same reference");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w("QrisActivity", "Could not parse last successful transaction: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
// ✅ CLEANUP: Clear progress status when user goes back
|
||||||
|
markTransactionInProgress(false);
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
1448
app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
package com.example.bdkipoc;
|
||||||
|
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class SettlementActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private TextView tvTotalAmount;
|
||||||
|
private TextView tvTotalTransactions;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private SettlementAdapter adapter;
|
||||||
|
private List<SettlementItem> settlementList;
|
||||||
|
private ImageView btnBack;
|
||||||
|
|
||||||
|
private String API_URL = "https://be-edc.msvc.app/transactions/performa-chanel-pembayaran?from_date=2025-01-01&to_date=2025-06-04&location_id=0&merchant_id=0";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settlement);
|
||||||
|
|
||||||
|
initViews();
|
||||||
|
setupRecyclerView();
|
||||||
|
fetchApiData();
|
||||||
|
setupClickListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchApiData() {
|
||||||
|
// Execute network call in background thread
|
||||||
|
new ApiTask().execute(API_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processApiData(JSONArray dataArray) {
|
||||||
|
try {
|
||||||
|
settlementList.clear();
|
||||||
|
|
||||||
|
final long[] totalAmountArray = {0}; // Using array to make it effectively final
|
||||||
|
final int[] totalTransactionsArray = {0}; // Using array to make it effectively final
|
||||||
|
|
||||||
|
// Process each channel individually (no grouping)
|
||||||
|
for (int i = 0; i < dataArray.length(); i++) {
|
||||||
|
JSONObject item = dataArray.getJSONObject(i);
|
||||||
|
|
||||||
|
String channelCode = item.getString("channel_code");
|
||||||
|
int transactions = item.getInt("total_transactions");
|
||||||
|
long maxAmount = item.getLong("max_transastions");
|
||||||
|
|
||||||
|
// Use channel code directly as display name with some formatting
|
||||||
|
String displayName = formatChannelName(channelCode);
|
||||||
|
int iconResource = getChannelIcon(channelCode);
|
||||||
|
|
||||||
|
settlementList.add(new SettlementItem(
|
||||||
|
displayName,
|
||||||
|
maxAmount,
|
||||||
|
transactions,
|
||||||
|
iconResource
|
||||||
|
));
|
||||||
|
|
||||||
|
totalAmountArray[0] += maxAmount;
|
||||||
|
totalTransactionsArray[0] += transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI on main thread
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateSummary(totalAmountArray[0], totalTransactionsArray[0]);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(SettlementActivity.this, "Error parsing data", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData(); // Fallback to sample data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSummary(long totalAmount, int totalTransactions) {
|
||||||
|
tvTotalAmount.setText(formatCurrency(totalAmount));
|
||||||
|
tvTotalTransactions.setText(String.valueOf(totalTransactions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews() {
|
||||||
|
tvTotalAmount = findViewById(R.id.tv_total_amount);
|
||||||
|
tvTotalTransactions = findViewById(R.id.tv_total_transactions);
|
||||||
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
btnBack = findViewById(R.id.btn_back);
|
||||||
|
|
||||||
|
settlementList = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupRecyclerView() {
|
||||||
|
adapter = new SettlementAdapter(settlementList);
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupClickListeners() {
|
||||||
|
btnBack.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSampleData() {
|
||||||
|
// Sample data as fallback
|
||||||
|
settlementList.clear();
|
||||||
|
|
||||||
|
settlementList.add(new SettlementItem("Kartu Kredit", 200000, 13, android.R.drawable.ic_menu_recent_history));
|
||||||
|
settlementList.add(new SettlementItem("Kartu Debit", 200000, 13, android.R.drawable.ic_menu_manage));
|
||||||
|
settlementList.add(new SettlementItem("Transfer", 200000, 13, android.R.drawable.ic_menu_send));
|
||||||
|
settlementList.add(new SettlementItem("Uang Elektronik", 200000, 13, android.R.drawable.ic_menu_gallery));
|
||||||
|
settlementList.add(new SettlementItem("QRIS", 200000, 13, android.R.drawable.ic_menu_camera));
|
||||||
|
|
||||||
|
// Update summary
|
||||||
|
tvTotalAmount.setText(formatCurrency(3506500));
|
||||||
|
tvTotalTransactions.setText("65");
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatChannelName(String channelCode) {
|
||||||
|
// Format channel code to be more readable
|
||||||
|
switch (channelCode) {
|
||||||
|
case "GO-PAY":
|
||||||
|
return "GoPay";
|
||||||
|
case "SHOPEEPAY":
|
||||||
|
return "ShopeePay";
|
||||||
|
case "LINKAJA":
|
||||||
|
return "LinkAja";
|
||||||
|
case "MASTERCARD":
|
||||||
|
return "Mastercard";
|
||||||
|
case "VISA":
|
||||||
|
return "Visa";
|
||||||
|
case "QRIS":
|
||||||
|
return "QRIS";
|
||||||
|
case "DANA":
|
||||||
|
return "Dana";
|
||||||
|
case "OVO":
|
||||||
|
return "OVO";
|
||||||
|
case "DEBIT":
|
||||||
|
return "Kartu Debit";
|
||||||
|
case "GPN":
|
||||||
|
return "GPN";
|
||||||
|
case "OTHER":
|
||||||
|
return "Lainnya";
|
||||||
|
default:
|
||||||
|
// Capitalize first letter and make rest lowercase
|
||||||
|
return channelCode.substring(0, 1).toUpperCase() +
|
||||||
|
channelCode.substring(1).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getChannelDisplayName(String channelCode) {
|
||||||
|
// Deprecated - keeping for backward compatibility
|
||||||
|
return formatChannelName(channelCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getChannelIcon(String channelCode) {
|
||||||
|
// Dynamic icon assignment based on channel type
|
||||||
|
switch (channelCode) {
|
||||||
|
case "DEBIT":
|
||||||
|
return android.R.drawable.ic_menu_manage;
|
||||||
|
case "VISA":
|
||||||
|
case "MASTERCARD":
|
||||||
|
return android.R.drawable.ic_menu_recent_history;
|
||||||
|
case "QRIS":
|
||||||
|
return android.R.drawable.ic_menu_camera;
|
||||||
|
case "DANA":
|
||||||
|
case "GO-PAY":
|
||||||
|
case "OVO":
|
||||||
|
case "SHOPEEPAY":
|
||||||
|
case "LINKAJA":
|
||||||
|
return android.R.drawable.ic_menu_gallery;
|
||||||
|
case "GPN":
|
||||||
|
return android.R.drawable.ic_menu_send;
|
||||||
|
case "OTHER":
|
||||||
|
return android.R.drawable.ic_menu_info_details;
|
||||||
|
default:
|
||||||
|
return android.R.drawable.ic_menu_help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCurrency(long amount) {
|
||||||
|
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||||
|
return formatter.format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated helper class - no longer needed
|
||||||
|
// private static class ChannelData { ... }
|
||||||
|
|
||||||
|
// AsyncTask for API call
|
||||||
|
private class ApiTask extends AsyncTask<String, Void, String> {
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(String... urls) {
|
||||||
|
try {
|
||||||
|
URL url = new URL(urls[0]);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setConnectTimeout(5000);
|
||||||
|
connection.setReadTimeout(5000);
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String result) {
|
||||||
|
if (result != null) {
|
||||||
|
try {
|
||||||
|
JSONObject jsonResponse = new JSONObject(result);
|
||||||
|
if (jsonResponse.getInt("status") == 200) {
|
||||||
|
JSONArray dataArray = jsonResponse.getJSONArray("data");
|
||||||
|
processApiData(dataArray);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(SettlementActivity.this, "API Error", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData();
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Toast.makeText(SettlementActivity.this, "JSON Parse Error", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(SettlementActivity.this, "Network Error", Toast.LENGTH_SHORT).show();
|
||||||
|
loadSampleData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettlementItem class - combined in same file
|
||||||
|
class SettlementItem {
|
||||||
|
private String channelName;
|
||||||
|
private long amount;
|
||||||
|
private int transactionCount;
|
||||||
|
private int iconResource;
|
||||||
|
|
||||||
|
public SettlementItem() {}
|
||||||
|
|
||||||
|
public SettlementItem(String channelName, long amount, int transactionCount, int iconResource) {
|
||||||
|
this.channelName = channelName;
|
||||||
|
this.amount = amount;
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
|
this.iconResource = iconResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getChannelName() { return channelName; }
|
||||||
|
public void setChannelName(String channelName) { this.channelName = channelName; }
|
||||||
|
public long getAmount() { return amount; }
|
||||||
|
public void setAmount(long amount) { this.amount = amount; }
|
||||||
|
public int getTransactionCount() { return transactionCount; }
|
||||||
|
public void setTransactionCount(int transactionCount) { this.transactionCount = transactionCount; }
|
||||||
|
public int getIconResource() { return iconResource; }
|
||||||
|
public void setIconResource(int iconResource) { this.iconResource = iconResource; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettlementAdapter class - combined in same file
|
||||||
|
class SettlementAdapter extends RecyclerView.Adapter<SettlementAdapter.SettlementViewHolder> {
|
||||||
|
|
||||||
|
private List<SettlementItem> settlementList;
|
||||||
|
|
||||||
|
public SettlementAdapter(List<SettlementItem> settlementList) {
|
||||||
|
this.settlementList = settlementList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public SettlementViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_settlement, parent, false);
|
||||||
|
return new SettlementViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull SettlementViewHolder holder, int position) {
|
||||||
|
SettlementItem item = settlementList.get(position);
|
||||||
|
|
||||||
|
holder.ivIcon.setImageResource(item.getIconResource());
|
||||||
|
holder.tvChannelName.setText(item.getChannelName());
|
||||||
|
holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
|
||||||
|
holder.tvTransactionCount.setText(item.getTransactionCount() + " Transaksi");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return settlementList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCurrency(long amount) {
|
||||||
|
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||||
|
return formatter.format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SettlementViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
ImageView ivIcon;
|
||||||
|
TextView tvChannelName;
|
||||||
|
TextView tvAmount;
|
||||||
|
TextView tvTransactionCount;
|
||||||
|
|
||||||
|
public SettlementViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
ivIcon = itemView.findViewById(R.id.iv_icon);
|
||||||
|
tvChannelName = itemView.findViewById(R.id.tv_channel_name);
|
||||||
|
tvAmount = itemView.findViewById(R.id.tv_amount);
|
||||||
|
tvTransactionCount = itemView.findViewById(R.id.tv_transaction_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,711 @@
|
|||||||
|
package com.example.bdkipoc.transaction;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResultTransactionActivity - Enhanced Receipt-style Display using activity_receipt.xml
|
||||||
|
* Shows EMV/Card transaction results using the same layout as QRIS receipts
|
||||||
|
*/
|
||||||
|
public class ResultTransactionActivity extends AppCompatActivity {
|
||||||
|
private static final String TAG = "ResultTransaction";
|
||||||
|
|
||||||
|
// ✅ UI Components using activity_receipt.xml IDs
|
||||||
|
private LinearLayout backNavigation;
|
||||||
|
private ImageView backArrow;
|
||||||
|
private TextView toolbarTitle;
|
||||||
|
|
||||||
|
// Receipt details
|
||||||
|
private TextView merchantName;
|
||||||
|
private TextView merchantLocation;
|
||||||
|
private TextView midText;
|
||||||
|
private TextView tidText;
|
||||||
|
private TextView transactionNumber;
|
||||||
|
private TextView transactionDate;
|
||||||
|
private TextView paymentMethod;
|
||||||
|
private TextView cardType;
|
||||||
|
private TextView transactionTotal;
|
||||||
|
private TextView taxPercentage;
|
||||||
|
private TextView serviceFee;
|
||||||
|
private TextView finalTotal;
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
private LinearLayout printButton;
|
||||||
|
private LinearLayout emailButton;
|
||||||
|
private Button finishButton;
|
||||||
|
|
||||||
|
// Data from intent
|
||||||
|
private String transactionAmount;
|
||||||
|
private String cardTypeFromIntent;
|
||||||
|
private boolean emvMode;
|
||||||
|
private String referenceId;
|
||||||
|
private String cardNo;
|
||||||
|
private String midtransResponse;
|
||||||
|
private boolean paymentSuccess;
|
||||||
|
private String emvCardholderName;
|
||||||
|
private String emvAid;
|
||||||
|
private String emvExpiry;
|
||||||
|
|
||||||
|
// Internal data
|
||||||
|
private JSONObject responseJsonData;
|
||||||
|
private boolean isNavigating = false;
|
||||||
|
|
||||||
|
// Receipt calculation data
|
||||||
|
private long subtotalAmount = 0;
|
||||||
|
private long taxAmount = 0;
|
||||||
|
private long serviceFeeAmount = 500; // Default service fee
|
||||||
|
private double taxPercentageValue = 0.11; // 11% tax
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// ✅ CRITICAL: Use the same layout as ReceiptActivity
|
||||||
|
setContentView(R.layout.activity_receipt);
|
||||||
|
|
||||||
|
Log.d(TAG, "=== RESULT TRANSACTION ACTIVITY STARTED ===");
|
||||||
|
Log.d(TAG, "✅ Using activity_receipt.xml layout");
|
||||||
|
|
||||||
|
initViews();
|
||||||
|
extractIntentData();
|
||||||
|
debugAllDataSources();
|
||||||
|
setupListeners();
|
||||||
|
calculateAmounts();
|
||||||
|
displayReceiptData();
|
||||||
|
|
||||||
|
logTransactionDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews() {
|
||||||
|
// ✅ Initialize views using activity_receipt.xml IDs
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
backNavigation = findViewById(R.id.back_navigation);
|
||||||
|
backArrow = findViewById(R.id.backArrow);
|
||||||
|
toolbarTitle = findViewById(R.id.toolbarTitle);
|
||||||
|
|
||||||
|
// Receipt details
|
||||||
|
merchantName = findViewById(R.id.merchant_name);
|
||||||
|
merchantLocation = findViewById(R.id.merchant_location);
|
||||||
|
midText = findViewById(R.id.mid_text);
|
||||||
|
tidText = findViewById(R.id.tid_text);
|
||||||
|
transactionNumber = findViewById(R.id.transaction_number);
|
||||||
|
transactionDate = findViewById(R.id.transaction_date);
|
||||||
|
paymentMethod = findViewById(R.id.payment_method);
|
||||||
|
cardType = findViewById(R.id.card_type);
|
||||||
|
transactionTotal = findViewById(R.id.transaction_total);
|
||||||
|
taxPercentage = findViewById(R.id.tax_percentage);
|
||||||
|
serviceFee = findViewById(R.id.service_fee);
|
||||||
|
finalTotal = findViewById(R.id.final_total);
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
printButton = findViewById(R.id.print_button);
|
||||||
|
emailButton = findViewById(R.id.email_button);
|
||||||
|
finishButton = findViewById(R.id.finish_button);
|
||||||
|
|
||||||
|
Log.d(TAG, "✅ All views initialized using activity_receipt.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractIntentData() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
|
||||||
|
transactionAmount = intent.getStringExtra("TRANSACTION_AMOUNT");
|
||||||
|
cardTypeFromIntent = intent.getStringExtra("CARD_TYPE");
|
||||||
|
emvMode = intent.getBooleanExtra("EMV_MODE", false);
|
||||||
|
referenceId = intent.getStringExtra("REFERENCE_ID");
|
||||||
|
cardNo = intent.getStringExtra("CARD_NO");
|
||||||
|
midtransResponse = intent.getStringExtra("MIDTRANS_RESPONSE");
|
||||||
|
paymentSuccess = intent.getBooleanExtra("PAYMENT_SUCCESS", true);
|
||||||
|
emvCardholderName = intent.getStringExtra("EMV_CARDHOLDER_NAME");
|
||||||
|
emvAid = intent.getStringExtra("EMV_AID");
|
||||||
|
emvExpiry = intent.getStringExtra("EMV_EXPIRY");
|
||||||
|
|
||||||
|
Log.d(TAG, "=== EXTRACTING INTENT DATA ===");
|
||||||
|
Log.d(TAG, "Card Type: " + cardTypeFromIntent);
|
||||||
|
Log.d(TAG, "EMV Mode: " + emvMode);
|
||||||
|
Log.d(TAG, "Transaction Amount: " + transactionAmount);
|
||||||
|
Log.d(TAG, "Reference ID: " + referenceId);
|
||||||
|
Log.d(TAG, "Midtrans Response Length: " + (midtransResponse != null ? midtransResponse.length() : 0));
|
||||||
|
|
||||||
|
// Parse Midtrans response if available
|
||||||
|
if (midtransResponse != null && !midtransResponse.isEmpty()) {
|
||||||
|
try {
|
||||||
|
responseJsonData = new JSONObject(midtransResponse);
|
||||||
|
Log.d(TAG, "✅ Midtrans Response parsed successfully!");
|
||||||
|
|
||||||
|
// Check for bank field specifically
|
||||||
|
if (responseJsonData.has("bank")) {
|
||||||
|
String bankValue = responseJsonData.getString("bank");
|
||||||
|
Log.d(TAG, "✅ Bank field found: '" + bankValue + "'");
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "⚠️ No 'bank' field in Midtrans response");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "❌ Error parsing Midtrans response: " + e.getMessage());
|
||||||
|
responseJsonData = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "⚠️ No Midtrans response data available");
|
||||||
|
responseJsonData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "===============================");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupListeners() {
|
||||||
|
// Back navigation
|
||||||
|
backNavigation.setOnClickListener(v -> {
|
||||||
|
if (isNavigating) return;
|
||||||
|
navigateBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
backArrow.setOnClickListener(v -> {
|
||||||
|
if (isNavigating) return;
|
||||||
|
navigateBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbarTitle.setOnClickListener(v -> {
|
||||||
|
if (isNavigating) return;
|
||||||
|
navigateBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Print button
|
||||||
|
printButton.setOnClickListener(v -> {
|
||||||
|
showToast("Mencetak struk...");
|
||||||
|
printReceipt();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Email button
|
||||||
|
emailButton.setOnClickListener(v -> {
|
||||||
|
showToast("Mengirim email...");
|
||||||
|
emailReceipt();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Finish button - Navigate to new transaction
|
||||||
|
finishButton.setOnClickListener(v -> {
|
||||||
|
if (isNavigating) return;
|
||||||
|
navigateToNewTransaction();
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.d(TAG, "✅ All click listeners setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculateAmounts() {
|
||||||
|
try {
|
||||||
|
if (transactionAmount != null && !transactionAmount.isEmpty()) {
|
||||||
|
subtotalAmount = Long.parseLong(transactionAmount);
|
||||||
|
} else {
|
||||||
|
subtotalAmount = 3500000; // Default amount for demo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate tax (11%)
|
||||||
|
taxAmount = Math.round(subtotalAmount * taxPercentageValue);
|
||||||
|
|
||||||
|
// Service fee is fixed
|
||||||
|
serviceFeeAmount = 500;
|
||||||
|
|
||||||
|
Log.d(TAG, "Amounts calculated - Subtotal: " + subtotalAmount +
|
||||||
|
", Tax: " + taxAmount + ", Service: " + serviceFeeAmount);
|
||||||
|
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Log.e(TAG, "Error calculating amounts: " + e.getMessage());
|
||||||
|
// Set default values
|
||||||
|
subtotalAmount = 3500000;
|
||||||
|
taxAmount = 385000;
|
||||||
|
serviceFeeAmount = 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayReceiptData() {
|
||||||
|
Log.d(TAG, "=== DISPLAYING RECEIPT DATA ===");
|
||||||
|
debugAllDataSources();
|
||||||
|
|
||||||
|
// ✅ 1. Set merchant data
|
||||||
|
merchantName.setText("TOKO KLONTONG PAK EKO");
|
||||||
|
merchantLocation.setText("Ciputat Baru, Tangsel");
|
||||||
|
|
||||||
|
// ✅ 2. Set MID and TID
|
||||||
|
String tid = extractTidFromResponse();
|
||||||
|
midText.setText("MID: " + tid);
|
||||||
|
tidText.setText("TID: " + tid);
|
||||||
|
|
||||||
|
// ✅ 3. Set transaction number
|
||||||
|
String displayTransactionNumber = extractTransactionNumberFromResponse();
|
||||||
|
transactionNumber.setText(displayTransactionNumber);
|
||||||
|
|
||||||
|
// ✅ 4. Set transaction date
|
||||||
|
String displayDate = formatTransactionDate();
|
||||||
|
transactionDate.setText(displayDate);
|
||||||
|
|
||||||
|
// ✅ 5. Set payment method
|
||||||
|
String displayPaymentMethod = getPaymentMethodDisplay();
|
||||||
|
paymentMethod.setText(displayPaymentMethod);
|
||||||
|
|
||||||
|
// ✅ 6. ENHANCED: Set card type with comprehensive detection
|
||||||
|
String displayCardType = getCardTypeDisplay();
|
||||||
|
cardType.setText(displayCardType);
|
||||||
|
|
||||||
|
// ✅ 7. Set amount details
|
||||||
|
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||||
|
|
||||||
|
transactionTotal.setText(formatter.format(subtotalAmount));
|
||||||
|
taxPercentage.setText(Math.round(taxPercentageValue * 100) + "%");
|
||||||
|
serviceFee.setText(formatter.format(serviceFeeAmount));
|
||||||
|
|
||||||
|
// Final total
|
||||||
|
long finalTotalAmount = subtotalAmount + taxAmount + serviceFeeAmount;
|
||||||
|
finalTotal.setText(formatter.format(finalTotalAmount));
|
||||||
|
|
||||||
|
Log.d(TAG, "✅ Receipt data displayed successfully");
|
||||||
|
Log.d(TAG, " Payment Method: " + displayPaymentMethod);
|
||||||
|
Log.d(TAG, " Card Type: " + displayCardType);
|
||||||
|
Log.d(TAG, " Final Total: " + formatter.format(finalTotalAmount));
|
||||||
|
Log.d(TAG, "================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractTidFromResponse() {
|
||||||
|
if (responseJsonData != null && responseJsonData.has("tid")) {
|
||||||
|
try {
|
||||||
|
return responseJsonData.getString("tid");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "Error extracting TID: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "123456789901"; // Default TID
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractTransactionNumberFromResponse() {
|
||||||
|
if (responseJsonData != null) {
|
||||||
|
try {
|
||||||
|
if (responseJsonData.has("transaction_id")) {
|
||||||
|
String fullTransactionId = responseJsonData.getString("transaction_id");
|
||||||
|
// Extract last 10 digits for display
|
||||||
|
if (fullTransactionId.length() > 10) {
|
||||||
|
return fullTransactionId.substring(fullTransactionId.length() - 10);
|
||||||
|
}
|
||||||
|
return fullTransactionId;
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "Error extracting transaction number: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate from reference ID or use default
|
||||||
|
if (referenceId != null && referenceId.length() > 10) {
|
||||||
|
return referenceId.substring(referenceId.length() - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.valueOf(System.currentTimeMillis() % 10000000000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTransactionDate() {
|
||||||
|
if (responseJsonData != null) {
|
||||||
|
try {
|
||||||
|
if (responseJsonData.has("transaction_time")) {
|
||||||
|
String transactionTime = responseJsonData.getString("transaction_time");
|
||||||
|
return formatDateForDisplay(transactionTime);
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "Error extracting transaction time: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use current date and time
|
||||||
|
return formatDateForDisplay(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDateForDisplay(String dateString) {
|
||||||
|
try {
|
||||||
|
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||||
|
SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMMM yyyy HH:mm", new Locale("id", "ID"));
|
||||||
|
Date date = inputFormat.parse(dateString);
|
||||||
|
return outputFormat.format(date);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error formatting date: " + e.getMessage());
|
||||||
|
return formatDateForDisplay(new Date());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDateForDisplay(Date date) {
|
||||||
|
SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMMM yyyy HH:mm", new Locale("id", "ID"));
|
||||||
|
return outputFormat.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPaymentMethodDisplay() {
|
||||||
|
if (cardTypeFromIntent == null) return "Kartu Kredit";
|
||||||
|
|
||||||
|
switch (cardTypeFromIntent.toUpperCase()) {
|
||||||
|
case "EMV_MIDTRANS":
|
||||||
|
case "IC":
|
||||||
|
case "NFC":
|
||||||
|
return emvMode ? "Kartu Kredit (EMV)" : "Kartu Kredit";
|
||||||
|
case "DEBIT":
|
||||||
|
return emvMode ? "Kartu Debit (EMV)" : "Kartu Debit";
|
||||||
|
case "MAGNETIC":
|
||||||
|
return "Kartu Kredit";
|
||||||
|
default:
|
||||||
|
return "Kartu Kredit";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENHANCED: Comprehensive bank detection for EMV transactions
|
||||||
|
private String getCardTypeDisplay() {
|
||||||
|
Log.d(TAG, "=== DETERMINING CARD TYPE DISPLAY (EMV) ===");
|
||||||
|
|
||||||
|
// Priority 1: Get bank from Midtrans response (most accurate)
|
||||||
|
if (responseJsonData != null) {
|
||||||
|
Log.d(TAG, "✅ Midtrans response available, checking for bank...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String bankFromResponse = null;
|
||||||
|
|
||||||
|
if (responseJsonData.has("bank")) {
|
||||||
|
bankFromResponse = responseJsonData.getString("bank");
|
||||||
|
Log.d(TAG, "Found 'bank' field: '" + bankFromResponse + "'");
|
||||||
|
} else if (responseJsonData.has("issuer")) {
|
||||||
|
bankFromResponse = responseJsonData.getString("issuer");
|
||||||
|
Log.d(TAG, "Found 'issuer' field: '" + bankFromResponse + "'");
|
||||||
|
} else if (responseJsonData.has("acquiring_bank")) {
|
||||||
|
bankFromResponse = responseJsonData.getString("acquiring_bank");
|
||||||
|
Log.d(TAG, "Found 'acquiring_bank' field: '" + bankFromResponse + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bankFromResponse != null && !bankFromResponse.trim().isEmpty()) {
|
||||||
|
String formattedBank = formatBankName(bankFromResponse);
|
||||||
|
Log.d(TAG, "✅ Bank from Midtrans response: '" + bankFromResponse + "' -> '" + formattedBank + "'");
|
||||||
|
return formattedBank;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "❌ Error extracting bank from response: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: EMV AID detection
|
||||||
|
Log.d(TAG, "Trying EMV AID detection...");
|
||||||
|
if (emvAid != null && !emvAid.trim().isEmpty()) {
|
||||||
|
String bankFromAid = getBankFromAid(emvAid);
|
||||||
|
if (!bankFromAid.equals("BCA")) { // If not default
|
||||||
|
Log.d(TAG, "✅ Bank from EMV AID: " + bankFromAid);
|
||||||
|
return bankFromAid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Card BIN detection
|
||||||
|
Log.d(TAG, "Trying Card BIN detection...");
|
||||||
|
if (cardNo != null && cardNo.length() >= 6) {
|
||||||
|
String cardBin = cardNo.substring(0, 6);
|
||||||
|
String bankFromBin = getBankFromComprehensiveBin(cardBin);
|
||||||
|
Log.d(TAG, "✅ Bank from BIN (" + cardBin + "): " + bankFromBin);
|
||||||
|
return bankFromBin;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "⚠️ Using default bank: BCA");
|
||||||
|
Log.d(TAG, "====================================");
|
||||||
|
return "BCA"; // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENHANCED: Better bank name formatting
|
||||||
|
private String formatBankName(String bankName) {
|
||||||
|
if (bankName == null || bankName.trim().isEmpty()) {
|
||||||
|
return "BCA"; // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatted = bankName.trim().toUpperCase();
|
||||||
|
|
||||||
|
// Handle common bank name variations
|
||||||
|
switch (formatted) {
|
||||||
|
case "BCA":
|
||||||
|
case "BANK BCA":
|
||||||
|
case "BANK CENTRAL ASIA":
|
||||||
|
return "BCA";
|
||||||
|
|
||||||
|
case "MANDIRI":
|
||||||
|
case "BANK MANDIRI":
|
||||||
|
return "Mandiri";
|
||||||
|
|
||||||
|
case "BNI":
|
||||||
|
case "BANK BNI":
|
||||||
|
case "BANK NEGARA INDONESIA":
|
||||||
|
return "BNI";
|
||||||
|
|
||||||
|
case "BRI":
|
||||||
|
case "BANK BRI":
|
||||||
|
case "BANK RAKYAT INDONESIA":
|
||||||
|
return "BRI";
|
||||||
|
|
||||||
|
case "CIMB":
|
||||||
|
case "CIMB NIAGA":
|
||||||
|
case "BANK CIMB NIAGA":
|
||||||
|
return "CIMB Niaga";
|
||||||
|
|
||||||
|
case "DANAMON":
|
||||||
|
case "BANK DANAMON":
|
||||||
|
return "Danamon";
|
||||||
|
|
||||||
|
case "PERMATA":
|
||||||
|
case "BANK PERMATA":
|
||||||
|
return "Permata";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return capitalizeFirstLetter(bankName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBankFromAid(String aid) {
|
||||||
|
// AID to Indonesian bank mapping
|
||||||
|
if (aid.contains("A0000000031010")) {
|
||||||
|
// VISA - check if we have card number for better detection
|
||||||
|
if (cardNo != null && cardNo.length() >= 6) {
|
||||||
|
return getBankFromComprehensiveBin(cardNo.substring(0, 6));
|
||||||
|
}
|
||||||
|
return "BCA"; // Default for VISA
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aid.contains("A0000000041010")) {
|
||||||
|
// MASTERCARD
|
||||||
|
if (cardNo != null && cardNo.length() >= 6) {
|
||||||
|
return getBankFromComprehensiveBin(cardNo.substring(0, 6));
|
||||||
|
}
|
||||||
|
return "Mandiri"; // Default for Mastercard
|
||||||
|
}
|
||||||
|
|
||||||
|
return "BCA"; // Ultimate fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENHANCED: Comprehensive Indonesian bank BIN mapping
|
||||||
|
private String getBankFromComprehensiveBin(String bin) {
|
||||||
|
if (bin == null || bin.length() < 4) {
|
||||||
|
return "BCA"; // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
String bin4 = bin.substring(0, 4);
|
||||||
|
String bin6 = bin.length() >= 6 ? bin.substring(0, 6) : bin4;
|
||||||
|
|
||||||
|
// BCA patterns
|
||||||
|
if (bin4.equals("4621") || bin4.equals("4699") || bin4.equals("5221") || bin4.equals("6277")) {
|
||||||
|
return "BCA";
|
||||||
|
}
|
||||||
|
|
||||||
|
// MANDIRI patterns
|
||||||
|
if (bin4.equals("4313") || bin4.equals("5573") || bin4.equals("6011") || bin4.equals("6234")) {
|
||||||
|
return "Mandiri";
|
||||||
|
}
|
||||||
|
|
||||||
|
// BNI patterns
|
||||||
|
if (bin4.equals("4603") || bin4.equals("1946") || bin4.equals("5264")) {
|
||||||
|
return "BNI";
|
||||||
|
}
|
||||||
|
|
||||||
|
// BRI patterns
|
||||||
|
if (bin4.equals("4578") || bin4.equals("4479") || bin4.equals("5208")) {
|
||||||
|
return "BRI";
|
||||||
|
}
|
||||||
|
|
||||||
|
// CIMB NIAGA patterns
|
||||||
|
if (bin4.equals("4599") || bin4.equals("5249")) {
|
||||||
|
return "CIMB Niaga";
|
||||||
|
}
|
||||||
|
|
||||||
|
// DANAMON patterns
|
||||||
|
if (bin4.equals("4055") || bin4.equals("5108")) {
|
||||||
|
return "Danamon";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
Log.d(TAG, "Unknown BIN pattern: " + bin6 + ", using default BCA");
|
||||||
|
return "BCA";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String capitalizeFirstLetter(String input) {
|
||||||
|
if (input == null || input.isEmpty()) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Debug methods
|
||||||
|
private void debugAllDataSources() {
|
||||||
|
Log.d(TAG, "=== DEBUGGING ALL DATA SOURCES ===");
|
||||||
|
|
||||||
|
if (responseJsonData != null) {
|
||||||
|
Log.d(TAG, "Midtrans Response Available:");
|
||||||
|
try {
|
||||||
|
java.util.Iterator<String> keys = responseJsonData.keys();
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
String key = keys.next();
|
||||||
|
Object value = responseJsonData.get(key);
|
||||||
|
Log.d(TAG, " " + key + ": " + value);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error iterating response: " + e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "❌ No Midtrans Response Data");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "EMV Data:");
|
||||||
|
Log.d(TAG, " Card Number: " + (cardNo != null ? maskCardNumber(cardNo) : "null"));
|
||||||
|
Log.d(TAG, " EMV AID: " + emvAid);
|
||||||
|
Log.d(TAG, " EMV Cardholder: " + emvCardholderName);
|
||||||
|
Log.d(TAG, " EMV Mode: " + emvMode);
|
||||||
|
|
||||||
|
Log.d(TAG, "==================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logTransactionDetails() {
|
||||||
|
Log.d(TAG, "=== RECEIPT DETAILS ===");
|
||||||
|
Log.d(TAG, "Reference ID: " + referenceId);
|
||||||
|
Log.d(TAG, "Card Number: " + (cardNo != null ? maskCardNumber(cardNo) : "N/A"));
|
||||||
|
Log.d(TAG, "Subtotal: " + subtotalAmount);
|
||||||
|
Log.d(TAG, "Tax: " + taxAmount);
|
||||||
|
Log.d(TAG, "Service Fee: " + serviceFeeAmount);
|
||||||
|
Log.d(TAG, "Final Total: " + (subtotalAmount + taxAmount + serviceFeeAmount));
|
||||||
|
Log.d(TAG, "======================");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Methods
|
||||||
|
private void printReceipt() {
|
||||||
|
Log.d(TAG, "Print receipt requested");
|
||||||
|
showToast("Fitur cetak akan segera tersedia");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emailReceipt() {
|
||||||
|
Log.d(TAG, "Email receipt requested");
|
||||||
|
|
||||||
|
Intent emailIntent = new Intent(Intent.ACTION_SEND);
|
||||||
|
emailIntent.setType("text/plain");
|
||||||
|
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Struk Pembayaran - " + extractTransactionNumberFromResponse());
|
||||||
|
emailIntent.putExtra(Intent.EXTRA_TEXT, generateEmailContent());
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(Intent.createChooser(emailIntent, "Kirim Email"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error sending email: " + e.getMessage());
|
||||||
|
showToast("Tidak dapat mengirim email");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateEmailContent() {
|
||||||
|
StringBuilder content = new StringBuilder();
|
||||||
|
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||||
|
|
||||||
|
content.append("STRUK PEMBAYARAN EMV/CARD\n");
|
||||||
|
content.append("==========================\n\n");
|
||||||
|
content.append("TOKO KLONTONG PAK EKO\n");
|
||||||
|
content.append("Ciputat Baru, Tangsel\n\n");
|
||||||
|
content.append("TID: ").append(extractTidFromResponse()).append("\n");
|
||||||
|
content.append("Nomor Transaksi: ").append(extractTransactionNumberFromResponse()).append("\n");
|
||||||
|
content.append("Tanggal: ").append(formatTransactionDate()).append("\n");
|
||||||
|
content.append("Metode: ").append(getPaymentMethodDisplay()).append("\n");
|
||||||
|
content.append("Jenis Kartu: ").append(getCardTypeDisplay()).append("\n\n");
|
||||||
|
|
||||||
|
if (emvMode && emvCardholderName != null) {
|
||||||
|
content.append("DETAIL EMV:\n");
|
||||||
|
content.append("Cardholder: ").append(emvCardholderName).append("\n");
|
||||||
|
content.append("AID: ").append(emvAid).append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.append("RINCIAN PEMBAYARAN:\n");
|
||||||
|
content.append("Total Transaksi: Rp ").append(formatter.format(subtotalAmount)).append("\n");
|
||||||
|
content.append("Pajak (11%): Rp ").append(formatter.format(taxAmount)).append("\n");
|
||||||
|
content.append("Biaya Layanan: Rp ").append(formatter.format(serviceFeeAmount)).append("\n");
|
||||||
|
content.append("------------------------\n");
|
||||||
|
content.append("TOTAL: Rp ").append(formatter.format(subtotalAmount + taxAmount + serviceFeeAmount)).append("\n");
|
||||||
|
content.append("\nTerima kasih atas pembayaran Anda!");
|
||||||
|
|
||||||
|
return content.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation Methods
|
||||||
|
private void navigateBack() {
|
||||||
|
if (isNavigating) return;
|
||||||
|
|
||||||
|
Log.d(TAG, "Navigating back");
|
||||||
|
isNavigating = true;
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigateToNewTransaction() {
|
||||||
|
if (isNavigating) return;
|
||||||
|
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle("Transaksi Baru")
|
||||||
|
.setMessage("Apakah Anda ingin melakukan transaksi baru?")
|
||||||
|
.setPositiveButton("Ya", (dialog, which) -> {
|
||||||
|
performNavigateToNewTransaction();
|
||||||
|
})
|
||||||
|
.setNegativeButton("Tidak", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performNavigateToNewTransaction() {
|
||||||
|
Log.d(TAG, "=== NAVIGATING TO NEW TRANSACTION ===");
|
||||||
|
isNavigating = true;
|
||||||
|
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(this, CreateTransactionActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error navigating to new transaction: " + e.getMessage());
|
||||||
|
isNavigating = false;
|
||||||
|
showToast("Gagal membuka transaksi baru");
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
private String maskCardNumber(String cardNumber) {
|
||||||
|
if (cardNumber == null || cardNumber.length() < 8) {
|
||||||
|
return cardNumber;
|
||||||
|
}
|
||||||
|
String first4 = cardNumber.substring(0, 4);
|
||||||
|
String last4 = cardNumber.substring(cardNumber.length() - 4);
|
||||||
|
return first4 + "****" + last4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showToast(String message) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (isNavigating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigateBack();
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
Log.d(TAG, "ResultTransactionActivity destroyed");
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package com.example.bdkipoc.transaction.managers;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.MyApplication;
|
||||||
|
import com.sunmi.pay.hardware.aidl.AidlConstants.CardType;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardScannerManager - Handles card detection for both EMV and Simple modes
|
||||||
|
*/
|
||||||
|
public class CardScannerManager {
|
||||||
|
private static final String TAG = "CardScannerManager";
|
||||||
|
|
||||||
|
private CardScannerCallback callback;
|
||||||
|
private boolean isProcessing = false;
|
||||||
|
|
||||||
|
public interface CardScannerCallback {
|
||||||
|
void onCardDetected(String cardType, Bundle cardData);
|
||||||
|
void onEMVCardDetected(int cardType);
|
||||||
|
void onScanError(String errorMessage);
|
||||||
|
void onScanProgress(String message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CardScannerManager(CardScannerCallback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startScanning(boolean isEMVMode) {
|
||||||
|
if (isProcessing) {
|
||||||
|
Log.d(TAG, "Card check already in progress - ignoring call");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting card check - setting isProcessing = true");
|
||||||
|
isProcessing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Small delay to ensure everything is ready
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
if (isProcessing) {
|
||||||
|
if (isEMVMode) {
|
||||||
|
startEMVCardCheck();
|
||||||
|
} else {
|
||||||
|
startSimpleCardCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in startScanning: " + e.getMessage(), e);
|
||||||
|
handleScanError("Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopScanning() {
|
||||||
|
try {
|
||||||
|
if (MyApplication.app != null && MyApplication.app.readCardOptV2 != null) {
|
||||||
|
MyApplication.app.readCardOptV2.cancelCheckCard();
|
||||||
|
}
|
||||||
|
isProcessing = false;
|
||||||
|
Log.d(TAG, "Card scanning stopped");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error stopping card check: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isScanning() {
|
||||||
|
return isProcessing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startEMVCardCheck() {
|
||||||
|
try {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanProgress("EMV Mode: Starting card scan...");
|
||||||
|
}
|
||||||
|
|
||||||
|
int cardType = AidlConstantsV2.CardType.NFC.getValue() | AidlConstantsV2.CardType.IC.getValue();
|
||||||
|
Log.d(TAG, "Starting EMV checkCard with cardType: " + cardType);
|
||||||
|
|
||||||
|
MyApplication.app.readCardOptV2.checkCard(cardType, mEMVCheckCardCallback, 60);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Log.e(TAG, "Error in startEMVCardCheck: " + e.getMessage());
|
||||||
|
handleScanError("Error starting EMV card scan: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startSimpleCardCheck() {
|
||||||
|
try {
|
||||||
|
if (!MyApplication.app.isConnectPaySDK()) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanProgress("Connecting to PaySDK...");
|
||||||
|
}
|
||||||
|
MyApplication.app.bindPaySDKService();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanProgress("Simple Mode: Starting card scan...");
|
||||||
|
}
|
||||||
|
|
||||||
|
int cardType = CardType.MAGNETIC.getValue() | CardType.IC.getValue() | CardType.NFC.getValue();
|
||||||
|
|
||||||
|
MyApplication.app.readCardOptV2.checkCard(cardType, mSimpleCheckCardCallback, 60);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
handleScanError("Error starting card scan: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleScanError(String errorMessage) {
|
||||||
|
Log.e(TAG, "Scan error: " + errorMessage);
|
||||||
|
isProcessing = false;
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanError(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetScanning() {
|
||||||
|
Log.d(TAG, "Resetting scanning state");
|
||||||
|
isProcessing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Card Detection Callback
|
||||||
|
private final CheckCardCallbackV2 mSimpleCheckCardCallback = new CheckCardCallbackV2.Stub() {
|
||||||
|
@Override
|
||||||
|
public void findMagCard(Bundle info) throws RemoteException {
|
||||||
|
Log.d(TAG, "Simple Mode: findMagCard callback triggered");
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCardDetected("MAGNETIC", info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findICCard(String atr) throws RemoteException {
|
||||||
|
Bundle info = new Bundle();
|
||||||
|
info.putString("atr", atr);
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCardDetected("IC", info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findRFCard(String uuid) throws RemoteException {
|
||||||
|
Bundle info = new Bundle();
|
||||||
|
info.putString("uuid", uuid);
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCardDetected("NFC", info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(int code, String message) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanError("Card error: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findICCardEx(Bundle info) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCardDetected("IC", info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findRFCardEx(Bundle info) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCardDetected("NFC", info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onErrorEx(Bundle info) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
String msg = info.getString("message", "Unknown error");
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanError("Card error: " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// EMV Card Detection Callback
|
||||||
|
private final CheckCardCallbackV2 mEMVCheckCardCallback = new CheckCardCallbackV2.Stub() {
|
||||||
|
@Override
|
||||||
|
public void findMagCard(Bundle info) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCardDetected("MAGNETIC", info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findICCard(String atr) throws RemoteException {
|
||||||
|
MyApplication.app.basicOptV2.buzzerOnDevice(1, 2750, 200, 0);
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onEMVCardDetected(AidlConstantsV2.CardType.IC.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findRFCard(String uuid) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onEMVCardDetected(AidlConstantsV2.CardType.NFC.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(int code, String message) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanError("EMV Error: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findICCardEx(Bundle info) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onEMVCardDetected(AidlConstantsV2.CardType.IC.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findRFCardEx(Bundle info) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onEMVCardDetected(AidlConstantsV2.CardType.NFC.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onErrorEx(Bundle info) throws RemoteException {
|
||||||
|
isProcessing = false;
|
||||||
|
String msg = info.getString("message", "Unknown error");
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onScanError("EMV Error: " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
package com.example.bdkipoc.transaction.managers;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.MyApplication;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.bean.EMVCandidateV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.emv.EMVListenerV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EMVManager - Handles all EMV related operations
|
||||||
|
*/
|
||||||
|
public class EMVManager {
|
||||||
|
private static final String TAG = "EMVManager";
|
||||||
|
|
||||||
|
private EMVOptV2 mEMVOptV2;
|
||||||
|
private EMVManagerCallback callback;
|
||||||
|
|
||||||
|
// EMV Process Variables
|
||||||
|
private int mCardType;
|
||||||
|
private String mCardNo;
|
||||||
|
private int mPinType;
|
||||||
|
private String mCertInfo;
|
||||||
|
private int mProcessStep;
|
||||||
|
|
||||||
|
public interface EMVManagerCallback {
|
||||||
|
void onAppSelect(String[] candidateNames);
|
||||||
|
void onFinalAppSelect();
|
||||||
|
void onConfirmCardNo(String cardNo);
|
||||||
|
void onCertVerify(String certInfo);
|
||||||
|
void onShowPinPad(int pinType);
|
||||||
|
void onOnlineProcess();
|
||||||
|
void onSignature();
|
||||||
|
void onTransactionSuccess(int code, String desc);
|
||||||
|
void onTransactionFailed(int code, String desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EMVManager(EMVManagerCallback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
initEMVComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initEMVComponents() {
|
||||||
|
if (MyApplication.app != null) {
|
||||||
|
mEMVOptV2 = MyApplication.app.emvOptV2;
|
||||||
|
Log.d(TAG, "EMV components initialized");
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "MyApplication.app is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initEMVData() {
|
||||||
|
try {
|
||||||
|
if (mEMVOptV2 != null) {
|
||||||
|
mEMVOptV2.initEmvProcess();
|
||||||
|
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
try {
|
||||||
|
initEmvTlvData();
|
||||||
|
Log.d(TAG, "EMV data initialized successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in delayed EMV init: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error initializing EMV data: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initEmvTlvData() {
|
||||||
|
try {
|
||||||
|
// Set PayPass (MasterCard) TLV data
|
||||||
|
String[] tagsPayPass = {"DF8117", "DF8118", "DF8119", "DF811F", "DF811E", "DF812C",
|
||||||
|
"DF8123", "DF8124", "DF8125", "DF8126", "DF811B", "DF811D", "DF8122", "DF8120", "DF8121"};
|
||||||
|
String[] valuesPayPass = {"E0", "F8", "F8", "E8", "00", "00",
|
||||||
|
"000000000000", "000000100000", "999999999999", "000000100000",
|
||||||
|
"30", "02", "0000000000", "000000000000", "000000000000"};
|
||||||
|
mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_PAYPASS, tagsPayPass, valuesPayPass);
|
||||||
|
|
||||||
|
// Set AMEX TLV data
|
||||||
|
String[] tagsAE = {"9F6D", "9F6E", "9F33", "9F35", "DF8168", "DF8167", "DF8169", "DF8170"};
|
||||||
|
String[] valuesAE = {"C0", "D8E00000", "E0E888", "22", "00", "00", "00", "60"};
|
||||||
|
mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_AE, tagsAE, valuesAE);
|
||||||
|
|
||||||
|
// Set JCB TLV data
|
||||||
|
String[] tagsJCB = {"9F53", "DF8161"};
|
||||||
|
String[] valuesJCB = {"708000", "7F00"};
|
||||||
|
mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_JCB, tagsJCB, valuesJCB);
|
||||||
|
|
||||||
|
// Set DPAS TLV data
|
||||||
|
String[] tagsDPAS = {"9F66"};
|
||||||
|
String[] valuesDPAS = {"B600C000"};
|
||||||
|
mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_DPAS, tagsDPAS, valuesDPAS);
|
||||||
|
|
||||||
|
// Set Flash TLV data
|
||||||
|
String[] tagsFLASH = {"9F58", "9F59", "9F5A", "9F5D", "9F5E"};
|
||||||
|
String[] valuesFLASH = {"03", "D88700", "00", "000000000000", "E000"};
|
||||||
|
mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_FLASH, tagsFLASH, valuesFLASH);
|
||||||
|
|
||||||
|
// Set Pure TLV data
|
||||||
|
String[] tagsPURE = {"DF7F", "DF8134", "DF8133"};
|
||||||
|
String[] valuesPURE = {"A0000007271010", "DF", "36006043F9"};
|
||||||
|
mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_PURE, tagsPURE, valuesPURE);
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putBoolean("optOnlineRes", true);
|
||||||
|
mEMVOptV2.setTermParamEx(bundle);
|
||||||
|
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(TAG, "Error setting EMV TLV data: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startEMVTransaction(String transactionAmount, int cardType) {
|
||||||
|
if (mProcessStep != 0) {
|
||||||
|
Log.d(TAG, "EMV transaction already in progress (step: " + mProcessStep + ") - ignoring call");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting EMV transaction process");
|
||||||
|
mProcessStep = 1;
|
||||||
|
mCardType = cardType;
|
||||||
|
|
||||||
|
try {
|
||||||
|
mEMVOptV2.initEmvProcess();
|
||||||
|
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
try {
|
||||||
|
if (mProcessStep <= 0) {
|
||||||
|
Log.d(TAG, "EMV process was cancelled - not starting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("amount", transactionAmount);
|
||||||
|
bundle.putString("transType", "00");
|
||||||
|
bundle.putInt("flowType", AidlConstantsV2.EMV.FlowType.TYPE_EMV_STANDARD);
|
||||||
|
bundle.putInt("cardType", mCardType);
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting transactProcessEx with reset EMV");
|
||||||
|
mEMVOptV2.transactProcessEx(bundle, mEMVListener);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in delayed EMV start: " + e.getMessage(), e);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onTransactionFailed(-1, "Error starting EMV: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error starting EMV transaction: " + e.getMessage());
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onTransactionFailed(-1, "Error starting EMV transaction: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetEMVProcess() {
|
||||||
|
try {
|
||||||
|
if (mEMVOptV2 != null) {
|
||||||
|
mEMVOptV2.initEmvProcess();
|
||||||
|
}
|
||||||
|
mProcessStep = 0;
|
||||||
|
mCardNo = null;
|
||||||
|
Log.d(TAG, "EMV process reset");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error resetting EMV process: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EMV Import Methods
|
||||||
|
public void importAppSelect(int selectIndex) {
|
||||||
|
try {
|
||||||
|
mEMVOptV2.importAppSelect(selectIndex);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importFinalAppSelectStatus(int status) {
|
||||||
|
try {
|
||||||
|
mEMVOptV2.importAppFinalSelectStatus(status);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importCardNoStatus(int status) {
|
||||||
|
try {
|
||||||
|
mEMVOptV2.importCardNoStatus(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importCertStatus(int status) {
|
||||||
|
try {
|
||||||
|
mEMVOptV2.importCertStatus(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importPinInputStatus(int inputResult) {
|
||||||
|
try {
|
||||||
|
mEMVOptV2.importPinInputStatus(mPinType, inputResult);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importSignatureStatus(int status) {
|
||||||
|
try {
|
||||||
|
mEMVOptV2.importSignatureStatus(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mockOnlineProcess() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String[] tags = {"71", "72", "91", "8A", "89"};
|
||||||
|
String[] values = {"", "", "", "", ""};
|
||||||
|
byte[] out = new byte[1024];
|
||||||
|
int len = mEMVOptV2.importOnlineProcStatus(0, tags, values, out);
|
||||||
|
if (len < 0) {
|
||||||
|
Log.e(TAG, "importOnlineProcessStatus error,code:" + len);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public String getCardNo() {
|
||||||
|
return mCardNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCardType() {
|
||||||
|
return mCardType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPinType() {
|
||||||
|
return mPinType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
public String maskCardNumber(String cardNo) {
|
||||||
|
if (cardNo == null || cardNo.length() < 8) {
|
||||||
|
return cardNo;
|
||||||
|
}
|
||||||
|
String first4 = cardNo.substring(0, 4);
|
||||||
|
String last4 = cardNo.substring(cardNo.length() - 4);
|
||||||
|
StringBuilder middle = new StringBuilder();
|
||||||
|
for (int i = 0; i < cardNo.length() - 8; i++) {
|
||||||
|
middle.append("*");
|
||||||
|
}
|
||||||
|
return first4 + middle.toString() + last4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getCandidateNames(List<EMVCandidateV2> candiList) {
|
||||||
|
if (candiList == null || candiList.size() == 0) return new String[0];
|
||||||
|
String[] result = new String[candiList.size()];
|
||||||
|
for (int i = 0; i < candiList.size(); i++) {
|
||||||
|
EMVCandidateV2 candi = candiList.get(i);
|
||||||
|
String name = candi.appPreName;
|
||||||
|
name = TextUtils.isEmpty(name) ? candi.appLabel : name;
|
||||||
|
name = TextUtils.isEmpty(name) ? candi.appName : name;
|
||||||
|
name = TextUtils.isEmpty(name) ? "" : name;
|
||||||
|
result[i] = name;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EMV Listener
|
||||||
|
private final EMVListenerV2 mEMVListener = new EMVListenerV2.Stub() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWaitAppSelect(List<EMVCandidateV2> appNameList, boolean isFirstSelect) throws RemoteException {
|
||||||
|
Log.d(TAG, "onWaitAppSelect isFirstSelect:" + isFirstSelect);
|
||||||
|
mProcessStep = 1; // EMV_APP_SELECT
|
||||||
|
String[] candidateNames = getCandidateNames(appNameList);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onAppSelect(candidateNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAppFinalSelect(String tag9F06Value) throws RemoteException {
|
||||||
|
Log.d(TAG, "onAppFinalSelect tag9F06Value:" + tag9F06Value);
|
||||||
|
mProcessStep = 2; // EMV_FINAL_APP_SELECT
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onFinalAppSelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfirmCardNo(String cardNo) throws RemoteException {
|
||||||
|
Log.d(TAG, "onConfirmCardNo cardNo:" + maskCardNumber(cardNo));
|
||||||
|
mCardNo = cardNo;
|
||||||
|
mProcessStep = 3; // EMV_CONFIRM_CARD_NO
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onConfirmCardNo(cardNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestShowPinPad(int pinType, int remainTime) throws RemoteException {
|
||||||
|
Log.d(TAG, "onRequestShowPinPad pinType:" + pinType + " remainTime:" + remainTime);
|
||||||
|
mPinType = pinType;
|
||||||
|
mProcessStep = 5; // EMV_SHOW_PIN_PAD
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onShowPinPad(pinType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestSignature() throws RemoteException {
|
||||||
|
Log.d(TAG, "onRequestSignature");
|
||||||
|
mProcessStep = 7; // EMV_SIGNATURE
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onSignature();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCertVerify(int certType, String certInfo) throws RemoteException {
|
||||||
|
Log.d(TAG, "onCertVerify certType:" + certType + " certInfo:" + certInfo);
|
||||||
|
mCertInfo = certInfo;
|
||||||
|
mProcessStep = 4; // EMV_CERT_VERIFY
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCertVerify(certInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOnlineProc() throws RemoteException {
|
||||||
|
Log.d(TAG, "onOnlineProcess");
|
||||||
|
mProcessStep = 6; // EMV_ONLINE_PROCESS
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onOnlineProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCardDataExchangeComplete() throws RemoteException {
|
||||||
|
Log.d(TAG, "onCardDataExchangeComplete");
|
||||||
|
if (mCardType == AidlConstantsV2.CardType.NFC.getValue()) {
|
||||||
|
MyApplication.app.basicOptV2.buzzerOnDevice(1, 2750, 200, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransResult(int code, String desc) throws RemoteException {
|
||||||
|
Log.d(TAG, "onTransResult code:" + code + " desc:" + desc);
|
||||||
|
|
||||||
|
if (code == 1 || code == 2 || code == 5 || code == 6) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onTransactionSuccess(code, desc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onTransactionFailed(code, desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfirmationCodeVerified() throws RemoteException {
|
||||||
|
Log.d(TAG, "onConfirmationCodeVerified");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestDataExchange(String cardNo) throws RemoteException {
|
||||||
|
Log.d(TAG, "onRequestDataExchange,cardNo:" + cardNo);
|
||||||
|
mEMVOptV2.importDataExchangeStatus(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTermRiskManagement() throws RemoteException {
|
||||||
|
Log.d(TAG, "onTermRiskManagement");
|
||||||
|
mEMVOptV2.importTermRiskManagementStatus(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPreFirstGenAC() throws RemoteException {
|
||||||
|
Log.d(TAG, "onPreFirstGenAC");
|
||||||
|
mEMVOptV2.importPreFirstGenACStatus(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDataStorageProc(String[] containerID, String[] containerContent) throws RemoteException {
|
||||||
|
Log.d(TAG, "onDataStorageProc");
|
||||||
|
String[] tags = new String[0];
|
||||||
|
String[] values = new String[0];
|
||||||
|
mEMVOptV2.importDataStorage(tags, values);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.example.bdkipoc.transaction.managers;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.AnimationUtils;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModalManager - Handles modal UI operations
|
||||||
|
*/
|
||||||
|
public class ModalManager {
|
||||||
|
private static final String TAG = "ModalManager";
|
||||||
|
|
||||||
|
private FrameLayout modalOverlay;
|
||||||
|
private TextView modalText;
|
||||||
|
private ImageView modalIcon;
|
||||||
|
private Animation fadeIn;
|
||||||
|
private Animation fadeOut;
|
||||||
|
private boolean isModalShowing = false;
|
||||||
|
|
||||||
|
public ModalManager(FrameLayout modalOverlay, TextView modalText, ImageView modalIcon) {
|
||||||
|
this.modalOverlay = modalOverlay;
|
||||||
|
this.modalText = modalText;
|
||||||
|
this.modalIcon = modalIcon;
|
||||||
|
initAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initAnimations() {
|
||||||
|
fadeIn = AnimationUtils.loadAnimation(modalOverlay.getContext(), android.R.anim.fade_in);
|
||||||
|
fadeOut = AnimationUtils.loadAnimation(modalOverlay.getContext(), android.R.anim.fade_out);
|
||||||
|
|
||||||
|
fadeIn.setDuration(300);
|
||||||
|
fadeOut.setDuration(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showScanCardModal() {
|
||||||
|
if (isModalShowing) return;
|
||||||
|
|
||||||
|
modalOverlay.post(() -> {
|
||||||
|
modalText.setText("Silakan Tempelkan / Gesekkan / Masukkan Kartu ke Perangkat");
|
||||||
|
modalIcon.setImageResource(R.drawable.ic_card_insert);
|
||||||
|
|
||||||
|
modalOverlay.setVisibility(View.VISIBLE);
|
||||||
|
modalOverlay.startAnimation(fadeIn);
|
||||||
|
|
||||||
|
isModalShowing = true;
|
||||||
|
Log.d(TAG, "Modal scan card shown");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showProcessingModal(String message) {
|
||||||
|
if (!isModalShowing) {
|
||||||
|
modalOverlay.post(() -> {
|
||||||
|
modalText.setText(message);
|
||||||
|
modalIcon.setImageResource(R.drawable.ic_card_insert);
|
||||||
|
|
||||||
|
modalOverlay.setVisibility(View.VISIBLE);
|
||||||
|
modalOverlay.startAnimation(fadeIn);
|
||||||
|
|
||||||
|
isModalShowing = true;
|
||||||
|
Log.d(TAG, "Modal processing shown: " + message);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Just update text if modal already showing
|
||||||
|
modalOverlay.post(() -> {
|
||||||
|
modalText.setText(message);
|
||||||
|
Log.d(TAG, "Modal text updated: " + message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hideModal() {
|
||||||
|
if (!isModalShowing) return;
|
||||||
|
|
||||||
|
modalOverlay.post(() -> {
|
||||||
|
fadeOut.setAnimationListener(new Animation.AnimationListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animation animation) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animation animation) {
|
||||||
|
modalOverlay.setVisibility(View.GONE);
|
||||||
|
isModalShowing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(Animation animation) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
modalOverlay.startAnimation(fadeOut);
|
||||||
|
Log.d(TAG, "Modal hidden");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShowing() {
|
||||||
|
return isModalShowing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateText(String text) {
|
||||||
|
if (isModalShowing) {
|
||||||
|
modalOverlay.post(() -> {
|
||||||
|
modalText.setText(text);
|
||||||
|
Log.d(TAG, "Modal text updated: " + text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateIcon(int iconResource) {
|
||||||
|
if (isModalShowing) {
|
||||||
|
modalOverlay.post(() -> {
|
||||||
|
modalIcon.setImageResource(iconResource);
|
||||||
|
Log.d(TAG, "Modal icon updated");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.example.bdkipoc.transaction.managers;
|
||||||
|
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.MyApplication;
|
||||||
|
import com.example.bdkipoc.utils.ByteUtil;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.AidlErrorCodeV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.bean.PinPadConfigV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadListenerV2;
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PinPadManager - Handles PIN pad operations
|
||||||
|
*/
|
||||||
|
public class PinPadManager {
|
||||||
|
private static final String TAG = "PinPadManager";
|
||||||
|
|
||||||
|
private PinPadOptV2 mPinPadOptV2;
|
||||||
|
private PinPadManagerCallback callback;
|
||||||
|
|
||||||
|
public interface PinPadManagerCallback {
|
||||||
|
void onPinInputLength(int length);
|
||||||
|
void onPinInputConfirmed(byte[] pinBlock);
|
||||||
|
void onPinInputCancelled();
|
||||||
|
void onPinInputError(int code, String message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PinPadManager(PinPadManagerCallback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
initPinPadComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPinPadComponents() {
|
||||||
|
if (MyApplication.app != null) {
|
||||||
|
mPinPadOptV2 = MyApplication.app.pinPadOptV2;
|
||||||
|
Log.d(TAG, "PIN Pad components initialized");
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "MyApplication.app is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initPinPad(String cardNo, int pinType) {
|
||||||
|
Log.d(TAG, "========== PIN PAD INITIALIZATION ==========");
|
||||||
|
try {
|
||||||
|
if (mPinPadOptV2 == null) {
|
||||||
|
throw new IllegalStateException("PIN Pad service not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardNo == null || cardNo.length() < 13) {
|
||||||
|
throw new IllegalArgumentException("Invalid card number for PIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
PinPadConfigV2 pinPadConfig = new PinPadConfigV2();
|
||||||
|
pinPadConfig.setPinPadType(0);
|
||||||
|
pinPadConfig.setPinType(pinType);
|
||||||
|
pinPadConfig.setOrderNumKey(true); // Set to true for normal order, false for random
|
||||||
|
|
||||||
|
String panForPin = cardNo.substring(cardNo.length() - 13, cardNo.length() - 1);
|
||||||
|
byte[] panBytes = panForPin.getBytes("US-ASCII");
|
||||||
|
pinPadConfig.setPan(panBytes);
|
||||||
|
|
||||||
|
pinPadConfig.setTimeout(60 * 1000);
|
||||||
|
pinPadConfig.setPinKeyIndex(12);
|
||||||
|
pinPadConfig.setMaxInput(12);
|
||||||
|
pinPadConfig.setMinInput(0);
|
||||||
|
pinPadConfig.setKeySystem(0);
|
||||||
|
pinPadConfig.setAlgorithmType(0);
|
||||||
|
|
||||||
|
Log.d(TAG, "Initializing PIN pad with config");
|
||||||
|
mPinPadOptV2.initPinPad(pinPadConfig, mPinPadListener);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "PIN pad initialization failed: " + e.getMessage());
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPinInputError(-1, "PIN Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelPinInput() {
|
||||||
|
try {
|
||||||
|
if (mPinPadOptV2 != null) {
|
||||||
|
// Cancel PIN input if needed
|
||||||
|
Log.d(TAG, "PIN input cancelled");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error cancelling PIN input: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PIN Pad Listener
|
||||||
|
private final PinPadListenerV2 mPinPadListener = new PinPadListenerV2.Stub() {
|
||||||
|
@Override
|
||||||
|
public void onPinLength(int len) throws RemoteException {
|
||||||
|
Log.d(TAG, "PIN input length: " + len);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPinInputLength(len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfirm(int i, byte[] pinBlock) throws RemoteException {
|
||||||
|
Log.d(TAG, "PIN input confirmed");
|
||||||
|
|
||||||
|
if (pinBlock != null) {
|
||||||
|
String hexStr = ByteUtil.bytes2HexStr(pinBlock);
|
||||||
|
Log.d(TAG, "PIN block received: " + hexStr);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPinInputConfirmed(pinBlock);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "PIN bypass confirmed");
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPinInputConfirmed(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCancel() throws RemoteException {
|
||||||
|
Log.d(TAG, "PIN input cancelled by user");
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPinInputCancelled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(int code) throws RemoteException {
|
||||||
|
Log.e(TAG, "PIN pad error: " + code);
|
||||||
|
String msg = AidlErrorCodeV2.valueOf(code).getMsg();
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPinInputError(code, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHover(int event, byte[] data) throws RemoteException {
|
||||||
|
Log.d(TAG, "PIN pad hover event: " + event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
package com.example.bdkipoc.transaction.managers;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PostTransactionBackendManager - Handles backend transaction posting
|
||||||
|
*
|
||||||
|
* This manager handles the communication with the backend service for transaction posting
|
||||||
|
* and provides the transaction_uuid needed for Midtrans integration.
|
||||||
|
*/
|
||||||
|
public class PostTransactionBackendManager {
|
||||||
|
private static final String TAG = "PostTransactionBackend";
|
||||||
|
|
||||||
|
// Backend Configuration
|
||||||
|
private static final String BACKEND_BASE_URL = "https://be-edc.msvc.app";
|
||||||
|
private static final String TRANSACTIONS_ENDPOINT = BACKEND_BASE_URL + "/transactions";
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
private static final String DEFAULT_DEVICE_CODE = "PB4K252T00021";
|
||||||
|
private static final int DEFAULT_DEVICE_ID = 1;
|
||||||
|
private static final String DEFAULT_CASHFLOW = "MONEY_IN";
|
||||||
|
private static final String DEFAULT_CHANNEL_CATEGORY = "RETAIL_OUTLET";
|
||||||
|
|
||||||
|
// ✅ NEW: Static merchant data
|
||||||
|
private static final String DEFAULT_MERCHANT_NAME = "BUDIAJAIB123";
|
||||||
|
private static final String DEFAULT_MID = "542531513";
|
||||||
|
private static final String DEFAULT_TID = "535151521";
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private PostTransactionCallback callback;
|
||||||
|
|
||||||
|
public interface PostTransactionCallback {
|
||||||
|
void onPostTransactionSuccess(JSONObject response, String transactionUuid);
|
||||||
|
void onPostTransactionError(String errorMessage);
|
||||||
|
void onPostTransactionProgress(String message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostTransactionBackendManager(Context context, PostTransactionCallback callback) {
|
||||||
|
this.context = context;
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post transaction to backend service
|
||||||
|
*/
|
||||||
|
public void postTransaction(String paymentType, String referenceId, long amount, String status) {
|
||||||
|
String channelCode = mapPaymentTypeToChannelCode(paymentType);
|
||||||
|
String transactionUuid = generateUUID();
|
||||||
|
|
||||||
|
Log.d(TAG, "=== POSTING TRANSACTION TO BACKEND ===");
|
||||||
|
Log.d(TAG, "Payment Type: " + paymentType);
|
||||||
|
Log.d(TAG, "Channel Code: " + channelCode);
|
||||||
|
Log.d(TAG, "Reference ID: " + referenceId);
|
||||||
|
Log.d(TAG, "Amount: " + amount);
|
||||||
|
Log.d(TAG, "Status: " + status);
|
||||||
|
Log.d(TAG, "Transaction UUID: " + transactionUuid);
|
||||||
|
Log.d(TAG, "=====================================");
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPostTransactionProgress("Posting transaction to backend...");
|
||||||
|
}
|
||||||
|
|
||||||
|
new PostTransactionTask(paymentType, channelCode, referenceId, amount, status, transactionUuid).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post transaction with INIT status (for pre-authorization)
|
||||||
|
*/
|
||||||
|
public void postInitTransaction(String paymentType, String referenceId, long amount) {
|
||||||
|
postTransaction(paymentType, referenceId, amount, "INIT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post transaction with SUCCESS status (for completed transactions)
|
||||||
|
*/
|
||||||
|
public void postSuccessTransaction(String paymentType, String referenceId, long amount) {
|
||||||
|
postTransaction(paymentType, referenceId, amount, "SUCCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update existing transaction status
|
||||||
|
*/
|
||||||
|
public void updateTransactionStatus(String transactionUuid, String newStatus) {
|
||||||
|
Log.d(TAG, "Updating transaction " + transactionUuid + " to status: " + newStatus);
|
||||||
|
// TODO: Implement update endpoint if available
|
||||||
|
// For now, we'll create a new transaction with updated status
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map payment type to channel code for backend
|
||||||
|
*/
|
||||||
|
private String mapPaymentTypeToChannelCode(String paymentType) {
|
||||||
|
if (paymentType == null) {
|
||||||
|
return "CREDIT_CARD"; // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (paymentType.toLowerCase()) {
|
||||||
|
case "credit_card":
|
||||||
|
return "CREDIT_CARD";
|
||||||
|
case "debit_card":
|
||||||
|
return "DEBIT_CARD";
|
||||||
|
case "e_money":
|
||||||
|
return "E_MONEY";
|
||||||
|
case "qris":
|
||||||
|
return "QRIS";
|
||||||
|
default:
|
||||||
|
Log.w(TAG, "Unknown payment type: " + paymentType + ", using CREDIT_CARD");
|
||||||
|
return "CREDIT_CARD";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate UUID v4 for transaction
|
||||||
|
*/
|
||||||
|
private String generateUUID() {
|
||||||
|
return UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device serial number (Sunmi device code)
|
||||||
|
*/
|
||||||
|
private String getDeviceCode() {
|
||||||
|
try {
|
||||||
|
// Try to get actual device serial number
|
||||||
|
// For Sunmi devices, this might be available through system properties
|
||||||
|
String serialNumber = android.os.Build.SERIAL;
|
||||||
|
if (serialNumber != null && !serialNumber.equals("unknown") && !serialNumber.equals(android.os.Build.UNKNOWN)) {
|
||||||
|
return serialNumber;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Could not get device serial number: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default device code
|
||||||
|
return DEFAULT_DEVICE_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncTask for posting transaction to backend
|
||||||
|
*/
|
||||||
|
private class PostTransactionTask extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
private String paymentType;
|
||||||
|
private String channelCode;
|
||||||
|
private String referenceId;
|
||||||
|
private long amount;
|
||||||
|
private String status;
|
||||||
|
private String transactionUuid;
|
||||||
|
private String errorMessage;
|
||||||
|
private JSONObject responseData;
|
||||||
|
|
||||||
|
public PostTransactionTask(String paymentType, String channelCode, String referenceId,
|
||||||
|
long amount, String status, String transactionUuid) {
|
||||||
|
this.paymentType = paymentType;
|
||||||
|
this.channelCode = channelCode;
|
||||||
|
this.referenceId = referenceId;
|
||||||
|
this.amount = amount;
|
||||||
|
this.status = status;
|
||||||
|
this.transactionUuid = transactionUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... voids) {
|
||||||
|
try {
|
||||||
|
// Build transaction payload
|
||||||
|
JSONObject payload = buildTransactionPayload();
|
||||||
|
|
||||||
|
Log.d(TAG, "Backend payload: " + payload.toString());
|
||||||
|
|
||||||
|
// Make HTTP request
|
||||||
|
return makeBackendRequest(payload);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Backend transaction exception: " + e.getMessage(), e);
|
||||||
|
errorMessage = "Backend transaction error: " + e.getMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean success) {
|
||||||
|
if (success && responseData != null && callback != null) {
|
||||||
|
try {
|
||||||
|
// Extract transaction_uuid from response
|
||||||
|
JSONObject data = responseData.optJSONObject("data");
|
||||||
|
String returnedUuid = null;
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
returnedUuid = data.optString("transaction_uuid", transactionUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "✅ Backend transaction successful!");
|
||||||
|
Log.d(TAG, "Original UUID: " + transactionUuid);
|
||||||
|
Log.d(TAG, "Returned UUID: " + returnedUuid);
|
||||||
|
|
||||||
|
callback.onPostTransactionSuccess(responseData, returnedUuid != null ? returnedUuid : transactionUuid);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error processing backend response: " + e.getMessage());
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onPostTransactionError("Error processing backend response: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (callback != null) {
|
||||||
|
callback.onPostTransactionError(errorMessage != null ? errorMessage : "Unknown backend error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject buildTransactionPayload() throws JSONException {
|
||||||
|
JSONObject payload = new JSONObject();
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
payload.put("type", "PAYMENT");
|
||||||
|
payload.put("channel_category", DEFAULT_CHANNEL_CATEGORY);
|
||||||
|
payload.put("channel_code", channelCode);
|
||||||
|
payload.put("reference_id", referenceId);
|
||||||
|
payload.put("amount", amount);
|
||||||
|
payload.put("cashflow", DEFAULT_CASHFLOW);
|
||||||
|
payload.put("status", status);
|
||||||
|
payload.put("device_id", DEFAULT_DEVICE_ID);
|
||||||
|
payload.put("transaction_uuid", transactionUuid);
|
||||||
|
payload.put("transaction_time_seconds", 2.2); // Default value as mentioned
|
||||||
|
payload.put("device_code", getDeviceCode());
|
||||||
|
|
||||||
|
// ✅ NEW: Static merchant data (no longer null)
|
||||||
|
payload.put("merchant_name", DEFAULT_MERCHANT_NAME);
|
||||||
|
payload.put("mid", DEFAULT_MID);
|
||||||
|
payload.put("tid", DEFAULT_TID);
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean makeBackendRequest(JSONObject payload) {
|
||||||
|
try {
|
||||||
|
URL url = new URI(TRANSACTIONS_ENDPOINT).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
|
||||||
|
// Set request properties
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Accept", "*/*");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(30000);
|
||||||
|
conn.setReadTimeout(30000);
|
||||||
|
|
||||||
|
// Send payload
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
byte[] input = payload.toString().getBytes("utf-8");
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get response
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
Log.d(TAG, "Backend response code: " + responseCode);
|
||||||
|
|
||||||
|
BufferedReader br;
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String responseLine;
|
||||||
|
|
||||||
|
if (responseCode >= 200 && responseCode < 300) {
|
||||||
|
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
||||||
|
} else {
|
||||||
|
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((responseLine = br.readLine()) != null) {
|
||||||
|
response.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseString = response.toString();
|
||||||
|
Log.d(TAG, "Backend response: " + responseString);
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
try {
|
||||||
|
responseData = new JSONObject(responseString);
|
||||||
|
|
||||||
|
// Check if response indicates success
|
||||||
|
int status = responseData.optInt("status", 0);
|
||||||
|
String message = responseData.optString("message", "");
|
||||||
|
|
||||||
|
if (status == 200 && "Successfully".equals(message)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
errorMessage = "Backend error: " + message + " (Status: " + status + ")";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "Error parsing backend response: " + e.getMessage());
|
||||||
|
errorMessage = "Invalid backend response format";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Backend request exception: " + e.getMessage(), e);
|
||||||
|
errorMessage = "Network error: " + e.getMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to generate reference ID
|
||||||
|
*/
|
||||||
|
public static String generateReferenceId() {
|
||||||
|
return "ref" + System.currentTimeMillis() + (int)(Math.random() * 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to map card menu ID to payment type
|
||||||
|
*/
|
||||||
|
public static String mapCardMenuToPaymentType(int cardMenuId) {
|
||||||
|
// Based on MainActivity.java card IDs
|
||||||
|
switch (cardMenuId) {
|
||||||
|
case 2131296346: // R.id.card_kartu_kredit
|
||||||
|
return "credit_card";
|
||||||
|
case 2131296344: // R.id.card_kartu_debit
|
||||||
|
return "debit_card";
|
||||||
|
case 2131296360: // R.id.card_uang_elektronik
|
||||||
|
return "e_money";
|
||||||
|
case 2131296352: // R.id.card_qris
|
||||||
|
return "qris";
|
||||||
|
default:
|
||||||
|
Log.w(TAG, "Unknown card menu ID: " + cardMenuId + ", defaulting to credit_card");
|
||||||
|
return "credit_card";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to log transaction details
|
||||||
|
*/
|
||||||
|
public void debugTransactionData(String paymentType, String referenceId, long amount, String status) {
|
||||||
|
Log.d(TAG, "=== TRANSACTION DEBUG INFO ===");
|
||||||
|
Log.d(TAG, "Payment Type: " + paymentType);
|
||||||
|
Log.d(TAG, "Channel Code: " + mapPaymentTypeToChannelCode(paymentType));
|
||||||
|
Log.d(TAG, "Reference ID: " + referenceId);
|
||||||
|
Log.d(TAG, "Amount: " + amount);
|
||||||
|
Log.d(TAG, "Status: " + status);
|
||||||
|
Log.d(TAG, "Device Code: " + getDeviceCode());
|
||||||
|
Log.d(TAG, "Device ID: " + DEFAULT_DEVICE_ID);
|
||||||
|
Log.d(TAG, "Merchant Name: " + DEFAULT_MERCHANT_NAME);
|
||||||
|
Log.d(TAG, "MID: " + DEFAULT_MID);
|
||||||
|
Log.d(TAG, "TID: " + DEFAULT_TID);
|
||||||
|
Log.d(TAG, "==============================");
|
||||||
|
}
|
||||||
|
}
|
||||||
265
app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package com.example.bdkipoc.utils;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class ByteUtil {
|
||||||
|
|
||||||
|
/** 打印内容 */
|
||||||
|
public static String byte2PrintHex(byte[] raw, int offset, int count) {
|
||||||
|
if (raw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (offset < 0 || offset > raw.length) {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
int end = offset + count;
|
||||||
|
if (end > raw.length) {
|
||||||
|
end = raw.length;
|
||||||
|
}
|
||||||
|
StringBuilder hex = new StringBuilder();
|
||||||
|
for (int i = offset; i < end; i++) {
|
||||||
|
int v = raw[i] & 0xFF;
|
||||||
|
String hv = Integer.toHexString(v);
|
||||||
|
if (hv.length() < 2) {
|
||||||
|
hex.append(0);
|
||||||
|
}
|
||||||
|
hex.append(hv);
|
||||||
|
hex.append(" ");
|
||||||
|
}
|
||||||
|
if (hex.length() > 0) {
|
||||||
|
hex.deleteCharAt(hex.length() - 1);
|
||||||
|
}
|
||||||
|
return hex.toString().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字节数组转换成16进制字符串
|
||||||
|
*
|
||||||
|
* @param bytes 源字节数组
|
||||||
|
* @return 转换后的16进制字符串
|
||||||
|
*/
|
||||||
|
public static String bytes2HexStr(byte... bytes) {
|
||||||
|
if (bytes == null || bytes.length == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return bytes2HexStr(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字节数组转换成16进制字符串
|
||||||
|
*
|
||||||
|
* @param src 源字节数组
|
||||||
|
* @param offset 偏移量
|
||||||
|
* @param len 数据长度
|
||||||
|
* @return 转换后的16进制字符串
|
||||||
|
*/
|
||||||
|
public static String bytes2HexStr(byte[] src, int offset, int len) {
|
||||||
|
int end = offset + len;
|
||||||
|
if (src == null || src.length == 0 || offset < 0 || len < 0 || end > src.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
byte[] buffer = new byte[len * 2];
|
||||||
|
int h = 0, l = 0;
|
||||||
|
for (int i = offset, j = 0; i < end; i++) {
|
||||||
|
h = src[i] >> 4 & 0x0f;
|
||||||
|
l = src[i] & 0x0f;
|
||||||
|
buffer[j++] = (byte) (h > 9 ? h - 10 + 'A' : h + '0');
|
||||||
|
buffer[j++] = (byte) (l > 9 ? l - 10 + 'A' : l + '0');
|
||||||
|
}
|
||||||
|
return new String(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hexStr2Bytes(String hexStr) {
|
||||||
|
if (TextUtils.isEmpty(hexStr)) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
int length = hexStr.length() / 2;
|
||||||
|
char[] chars = hexStr.toCharArray();
|
||||||
|
byte[] b = new byte[length];
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
b[i] = (byte) (char2Byte(chars[i * 2]) << 4 | char2Byte(chars[i * 2 + 1]));
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte hexStr2Byte(String hexStr) {
|
||||||
|
return (byte) Integer.parseInt(hexStr, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String hexStr2Str(String hexStr) {
|
||||||
|
String vi = "0123456789ABC DEF".trim();
|
||||||
|
char[] array = hexStr.toCharArray();
|
||||||
|
byte[] bytes = new byte[hexStr.length() / 2];
|
||||||
|
int temp;
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
char c = array[2 * i];
|
||||||
|
temp = vi.indexOf(c) * 16;
|
||||||
|
c = array[2 * i + 1];
|
||||||
|
temp += vi.indexOf(c);
|
||||||
|
bytes[i] = (byte) (temp & 0xFF);
|
||||||
|
}
|
||||||
|
return new String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String hexStr2AsciiStr(String hexStr) {
|
||||||
|
String vi = "0123456789ABC DEF".trim();
|
||||||
|
hexStr = hexStr.trim().replace(" ", "").toUpperCase(Locale.US);
|
||||||
|
char[] array = hexStr.toCharArray();
|
||||||
|
byte[] bytes = new byte[hexStr.length() / 2];
|
||||||
|
int temp = 0x00;
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
char c = array[2 * i];
|
||||||
|
temp = vi.indexOf(c) << 4;
|
||||||
|
c = array[2 * i + 1];
|
||||||
|
temp |= vi.indexOf(c);
|
||||||
|
bytes[i] = (byte) (temp & 0xFF);
|
||||||
|
}
|
||||||
|
return new String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将无符号short转换成int,大端模式(高位在前)
|
||||||
|
*/
|
||||||
|
public static int unsignedShort2IntBE(byte[] src, int offset) {
|
||||||
|
return (src[offset] & 0xff) << 8 | (src[offset + 1] & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将无符号short转换成int,小端模式(低位在前)
|
||||||
|
*/
|
||||||
|
public static int unsignedShort2IntLE(byte[] src, int offset) {
|
||||||
|
return (src[offset] & 0xff) | (src[offset + 1] & 0xff) << 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将无符号byte转换成int
|
||||||
|
*/
|
||||||
|
public static int unsignedByte2Int(byte[] src, int offset) {
|
||||||
|
return src[offset] & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字节数组转换成int,大端模式(高位在前)
|
||||||
|
*/
|
||||||
|
public static int unsignedInt2IntBE(byte[] src, int offset) {
|
||||||
|
int result = 0;
|
||||||
|
for (int i = offset; i < offset + 4; i++) {
|
||||||
|
result |= (src[i] & 0xff) << (offset + 3 - i) * 8;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字节数组转换成int,小端模式(低位在前)
|
||||||
|
*/
|
||||||
|
public static int unsignedInt2IntLE(byte[] src, int offset) {
|
||||||
|
int value = 0;
|
||||||
|
for (int i = offset; i < offset + 4; i++) {
|
||||||
|
value |= (src[i] & 0xff) << (i - offset) * 8;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将int转换成byte数组,大端模式(高位在前)
|
||||||
|
*/
|
||||||
|
public static byte[] int2BytesBE(int src) {
|
||||||
|
byte[] result = new byte[4];
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
result[i] = (byte) (src >> (3 - i) * 8);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将int转换成byte数组,小端模式(低位在前)
|
||||||
|
*/
|
||||||
|
public static byte[] int2BytesLE(int src) {
|
||||||
|
byte[] result = new byte[4];
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
result[i] = (byte) (src >> i * 8);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将short转换成byte数组,大端模式(高位在前)
|
||||||
|
*/
|
||||||
|
public static byte[] short2BytesBE(short src) {
|
||||||
|
byte[] result = new byte[2];
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
result[i] = (byte) (src >> (1 - i) * 8);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将short转换成byte数组,小端模式(低位在前)
|
||||||
|
*/
|
||||||
|
public static byte[] short2BytesLE(short src) {
|
||||||
|
byte[] result = new byte[2];
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
result[i] = (byte) (src >> i * 8);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字节数组列表合并成单个字节数组
|
||||||
|
*/
|
||||||
|
public static byte[] concatByteArrays(byte[]... list) {
|
||||||
|
if (list == null || list.length == 0) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
return concatByteArrays(Arrays.asList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字节数组列表合并成单个字节数组
|
||||||
|
*/
|
||||||
|
public static byte[] concatByteArrays(List<byte[]> list) {
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
int totalLen = 0;
|
||||||
|
for (byte[] b : list) {
|
||||||
|
if (b == null || b.length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
totalLen += b.length;
|
||||||
|
}
|
||||||
|
byte[] result = new byte[totalLen];
|
||||||
|
int index = 0;
|
||||||
|
for (byte[] b : list) {
|
||||||
|
if (b == null || b.length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
System.arraycopy(b, 0, result, index, b.length);
|
||||||
|
index += b.length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert char to byte
|
||||||
|
*
|
||||||
|
* @param c char
|
||||||
|
* @return byte
|
||||||
|
*/
|
||||||
|
private static int char2Byte(char c) {
|
||||||
|
if (c >= 'a') {
|
||||||
|
return (c - 'a' + 10) & 0x0f;
|
||||||
|
}
|
||||||
|
if (c >= 'A') {
|
||||||
|
return (c - 'A' + 10) & 0x0f;
|
||||||
|
}
|
||||||
|
return (c - '0') & 0x0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
89
app/src/main/java/com/example/bdkipoc/utils/LogUtil.java
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package com.example.bdkipoc.utils;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class LogUtil {
|
||||||
|
|
||||||
|
public static final int VERBOSE = 1;
|
||||||
|
public static final int DEBUG = 2;
|
||||||
|
public static final int INFO = 3;
|
||||||
|
public static final int WARN = 4;
|
||||||
|
public static final int ERROR = 5;
|
||||||
|
public static final int NOTHING = 6;
|
||||||
|
public static int LEVEL = VERBOSE;
|
||||||
|
|
||||||
|
public static void setLevel(int Level) {
|
||||||
|
LEVEL = Level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void v(String TAG, String msg) {
|
||||||
|
if (LEVEL <= VERBOSE && !TextUtils.isEmpty(msg)) {
|
||||||
|
MyLog(VERBOSE, TAG, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void d(String TAG, String msg) {
|
||||||
|
if (LEVEL <= DEBUG && !TextUtils.isEmpty(msg)) {
|
||||||
|
MyLog(DEBUG, TAG, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void i(String TAG, String msg) {
|
||||||
|
if (LEVEL <= INFO && !TextUtils.isEmpty(msg)) {
|
||||||
|
MyLog(INFO, TAG, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void w(String TAG, String msg) {
|
||||||
|
if (LEVEL <= WARN && !TextUtils.isEmpty(msg)) {
|
||||||
|
MyLog(WARN, TAG, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(String TAG, String msg) {
|
||||||
|
if (LEVEL <= ERROR && !TextUtils.isEmpty(msg)) {
|
||||||
|
MyLog(ERROR, TAG, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MyLog(int type, String TAG, String msg) {
|
||||||
|
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||||
|
int index = 4;
|
||||||
|
String className = stackTrace[index].getFileName();
|
||||||
|
String methodName = stackTrace[index].getMethodName();
|
||||||
|
int lineNumber = stackTrace[index].getLineNumber();
|
||||||
|
methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
stringBuilder.append("[ (")
|
||||||
|
.append(className)
|
||||||
|
.append(":")
|
||||||
|
.append(lineNumber)
|
||||||
|
.append(")#")
|
||||||
|
.append(methodName)
|
||||||
|
.append(" ] ");
|
||||||
|
stringBuilder.append(msg);
|
||||||
|
String logStr = stringBuilder.toString();
|
||||||
|
switch (type) {
|
||||||
|
case VERBOSE:
|
||||||
|
Log.v(TAG, logStr);
|
||||||
|
break;
|
||||||
|
case DEBUG:
|
||||||
|
Log.d(TAG, logStr);
|
||||||
|
break;
|
||||||
|
case INFO:
|
||||||
|
Log.i(TAG, logStr);
|
||||||
|
break;
|
||||||
|
case WARN:
|
||||||
|
Log.w(TAG, logStr);
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
|
Log.e(TAG, logStr);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
107
app/src/main/java/com/example/bdkipoc/utils/Utility.java
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package com.example.bdkipoc.utils;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.MyApplication;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class Utility {
|
||||||
|
private Utility() {
|
||||||
|
throw new AssertionError("Create instance of Utility is forbidden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bundle对象转换成字符串 */
|
||||||
|
public static String bundle2String(Bundle bundle) {
|
||||||
|
return bundle2String(bundle, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据key排序后将Bundle内容拼接成字符串
|
||||||
|
*
|
||||||
|
* @param bundle 要处理的bundle
|
||||||
|
* @param order 排序规则,0-不排序,1-升序,2-降序
|
||||||
|
* @return 拼接后的字符串
|
||||||
|
*/
|
||||||
|
public static String bundle2String(Bundle bundle, int order) {
|
||||||
|
if (bundle == null || bundle.keySet().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
List<String> list = new ArrayList<>(bundle.keySet());
|
||||||
|
if (order == 1) { //升序
|
||||||
|
Collections.sort(list, String::compareTo);
|
||||||
|
} else if (order == 2) {//降序
|
||||||
|
Collections.sort(list, Collections.reverseOrder());
|
||||||
|
}
|
||||||
|
for (String key : list) {
|
||||||
|
sb.append(key);
|
||||||
|
sb.append(":");
|
||||||
|
Object value = bundle.get(key);
|
||||||
|
if (value instanceof byte[]) {
|
||||||
|
sb.append(ByteUtil.bytes2HexStr((byte[]) value));
|
||||||
|
} else {
|
||||||
|
sb.append(value);
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.deleteCharAt(sb.length() - 1);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将null转换成空串 */
|
||||||
|
public static String null2String(String str) {
|
||||||
|
return str == null ? "" : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatStr(String format, Object... params) {
|
||||||
|
return String.format(Locale.ENGLISH, format, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** check whether src is hex format */
|
||||||
|
public static boolean checkHexValue(String src) {
|
||||||
|
return Pattern.matches("[0-9a-fA-F]+", src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示Toast */
|
||||||
|
public static void showToast(final String msg) {
|
||||||
|
Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
handler.post(() -> Toast.makeText(MyApplication.app, msg, Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示Toast */
|
||||||
|
public static void showToast(int resId) {
|
||||||
|
showToast(MyApplication.app.getString(resId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据结果码获取成功失败信息 */
|
||||||
|
public static String getStateString(int code) {
|
||||||
|
return code == 0 ? "success" : "failed, code:" + code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据结果状态获取成功失败信息 */
|
||||||
|
public static String getStateString(boolean state) {
|
||||||
|
return state ? "success" : "failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将dp转成px */
|
||||||
|
public static int dp2px(int dp) {
|
||||||
|
float density = MyApplication.app.getResources().getDisplayMetrics().density;
|
||||||
|
return Math.round(dp * density);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将px转成dp */
|
||||||
|
public static int px2dp(int px) {
|
||||||
|
float density = MyApplication.app.getResources().getDisplayMetrics().density;
|
||||||
|
return Math.round(px / density);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.example.bdkipoc.wrapper;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
|
||||||
|
import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2;
|
||||||
|
|
||||||
|
|
||||||
|
public class CheckCardCallbackV2Wrapper extends CheckCardCallbackV2.Stub {
|
||||||
|
@Override
|
||||||
|
public void findMagCard(Bundle info) throws RemoteException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findICCard(String atr) throws RemoteException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findRFCard(String uuid) throws RemoteException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(int code, String message) throws RemoteException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findICCardEx(Bundle info) throws RemoteException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void findRFCardEx(Bundle info) throws RemoteException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onErrorEx(Bundle info) throws RemoteException {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
4
app/src/main/res/anim/fade_in.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0" />
|
||||||
4
app/src/main/res/anim/fade_out.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0" />
|
||||||
14
app/src/main/res/anim/scale_in.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<scale
|
||||||
|
android:duration="200"
|
||||||
|
android:fromXScale="0.8"
|
||||||
|
android:fromYScale="0.8"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:toXScale="1.0"
|
||||||
|
android:toYScale="1.0" />
|
||||||
|
<alpha
|
||||||
|
android:duration="200"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0" />
|
||||||
|
</set>
|
||||||
14
app/src/main/res/anim/scale_out.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<scale
|
||||||
|
android:duration="200"
|
||||||
|
android:fromXScale="1.0"
|
||||||
|
android:fromYScale="1.0"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:toXScale="0.8"
|
||||||
|
android:toYScale="0.8" />
|
||||||
|
<alpha
|
||||||
|
android:duration="200"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0" />
|
||||||
|
</set>
|
||||||
10
app/src/main/res/anim/slide_down.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:duration="300"
|
||||||
|
android:fromYDelta="0%p"
|
||||||
|
android:toYDelta="50%p" />
|
||||||
|
<alpha
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0" />
|
||||||
|
</set>
|
||||||
10
app/src/main/res/anim/slide_in_left.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:duration="300"
|
||||||
|
android:fromXDelta="-100%p"
|
||||||
|
android:toXDelta="0" />
|
||||||
|
<alpha
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0" />
|
||||||
|
</set>
|
||||||
10
app/src/main/res/anim/slide_in_right.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:duration="300"
|
||||||
|
android:fromXDelta="100%p"
|
||||||
|
android:toXDelta="0" />
|
||||||
|
<alpha
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0" />
|
||||||
|
</set>
|
||||||
10
app/src/main/res/anim/slide_out_left.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:duration="300"
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="-100%p" />
|
||||||
|
<alpha
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0" />
|
||||||
|
</set>
|
||||||
10
app/src/main/res/anim/slide_out_right.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:duration="300"
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="100%p" />
|
||||||
|
<alpha
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0" />
|
||||||
|
</set>
|
||||||
10
app/src/main/res/anim/slide_up.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:duration="300"
|
||||||
|
android:fromYDelta="50%p"
|
||||||
|
android:toYDelta="0%p" />
|
||||||
|
<alpha
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0" />
|
||||||
|
</set>
|
||||||
BIN
app/src/main/res/drawable/banner.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
6
app/src/main/res/drawable/bg_status.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#F0F0F0"/>
|
||||||
|
<corners android:radius="8dp"/>
|
||||||
|
<stroke android:width="1dp" android:color="#E0E0E0"/>
|
||||||
|
</shape>
|
||||||
6
app/src/main/res/drawable/button_active_background.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#DE0701" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
</shape>
|
||||||
5
app/src/main/res/drawable/button_cancel_background.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<stroke android:width="2dp" android:color="#E31937"/>
|
||||||
|
<corners android:radius="8dp"/>
|
||||||
|
<solid android:color="@android:color/transparent"/>
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/button_active_background" android:state_enabled="true"/>
|
||||||
|
<item android:drawable="@drawable/button_inactive_background" android:state_enabled="false"/>
|
||||||
|
</selector>
|
||||||
9
app/src/main/res/drawable/button_finish_background.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="#3498DB" />
|
||||||
|
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
6
app/src/main/res/drawable/button_inactive_background.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#ECEFF0" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
</shape>
|
||||||
13
app/src/main/res/drawable/button_secondary_background.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="#F5F5F5" />
|
||||||
|
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#E0E0E0" />
|
||||||
|
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
6
app/src/main/res/drawable/card_background.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#3498DB" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
<stroke android:width="0dp" />
|
||||||
|
</shape>
|
||||||
BIN
app/src/main/res/drawable/ic_arrow_back.png
Normal file
|
After Width: | Height: | Size: 220 B |
10
app/src/main/res/drawable/ic_backspace.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- res/drawable/icons/ic_backspace.xml -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#333333"
|
||||||
|
android:pathData="M22,3H7c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12 19,15.59z"/>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/drawable/ic_card_insert.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
10
app/src/main/res/drawable/ic_check_circle.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/drawable/ic_credit_card.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
app/src/main/res/drawable/ic_debit_card.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable/ic_e_money.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable/ic_email.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable/ic_help.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable/ic_history.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable/ic_logo_icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
10
app/src/main/res/drawable/ic_notifications.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/drawable/ic_print.png
Normal file
|
After Width: | Height: | Size: 586 B |
10
app/src/main/res/drawable/ic_qr_code.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M3,11h8L11,3L3,3v8zM5,5h4v4L5,9L5,5zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM3,21h8v-8L3,13v8zM5,15h4v4L5,19v-4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"/>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/drawable/ic_qris.png
Normal file
|
After Width: | Height: | Size: 607 B |
BIN
app/src/main/res/drawable/ic_refund.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable/ic_reprint.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/drawable/ic_settings.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/drawable/ic_settlement.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable/ic_store_info.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable/ic_success_payment.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
app/src/main/res/drawable/ic_transfer.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
5
app/src/main/res/drawable/search_background.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#f5f5f5" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#e0e0e0" />
|
||||||
|
</shape>
|
||||||
4
app/src/main/res/drawable/search_button_background.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#666666" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
5
app/src/main/res/drawable/timer_circle_background.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#F0F0F0"/>
|
||||||
|
<corners android:radius="24dp"/>
|
||||||
|
<stroke android:width="1dp" android:color="#E0E0E0"/>
|
||||||
|
</shape>
|
||||||