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"
|
||||
}
|
||||
@@ -6,10 +6,17 @@ android {
|
||||
namespace 'com.example.bdkipoc'
|
||||
compileSdk 35
|
||||
|
||||
// Tambahkan lint options
|
||||
lint {
|
||||
abortOnError false
|
||||
disable 'GoogleAppIndexingWarning'
|
||||
disable 'NonConstantResourceId'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.bdkipoc"
|
||||
minSdk 21
|
||||
targetSdk 30
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
@@ -22,19 +29,32 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
// Keep Java 11 - lebih modern dari referensi
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
// Tambahkan sourceSets untuk native libs jika diperlukan
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['libs']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
|
||||
implementation libs.appcompat
|
||||
implementation libs.material
|
||||
implementation libs.activity
|
||||
implementation libs.constraintlayout
|
||||
implementation libs.cardview
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'com.sunmi:printerlibrary:1.0.15'
|
||||
|
||||
// Test dependencies
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation libs.ext.junit
|
||||
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_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
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@@ -17,6 +33,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.BDKIPOC"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -28,12 +45,45 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".TransactionActivity"
|
||||
android:name=".cetakulang.ReprintActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".PaymentActivity"
|
||||
android:name=".cetakulang.ReprintAdapterActivity"
|
||||
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>
|
||||
|
||||
</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;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
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 {
|
||||
|
||||
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
|
||||
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);
|
||||
EdgeToEdge.enable(this);
|
||||
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());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
|
||||
// Set up click listeners for the cards
|
||||
CardView paymentCard = findViewById(R.id.card_payment);
|
||||
CardView transactionsCard = findViewById(R.id.card_transactions);
|
||||
// Initialize views
|
||||
btnLainnya = findViewById(R.id.btn_lainnya);
|
||||
|
||||
paymentCard.setOnClickListener(v -> {
|
||||
// Launch payment activity
|
||||
startActivity(new android.content.Intent(MainActivity.this, PaymentActivity.class));
|
||||
});
|
||||
// Check if we're returning from a completed transaction
|
||||
checkTransactionCompletion();
|
||||
|
||||
transactionsCard.setOnClickListener(v -> {
|
||||
// Launch transactions activity
|
||||
startActivity(new android.content.Intent(MainActivity.this, TransactionActivity.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");
|
||||
}
|
||||
});
|
||||
|
||||
// Set up scan dan bayar card click listener
|
||||
LinearLayout scanBayarContent = findViewById(R.id.scan_bayar_content);
|
||||
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>
|
||||