Compare commits

...

56 Commits

Author SHA1 Message Date
6f78b6df3f Merge branch 'temp-feature' into development 2025-06-27 17:15:14 +07:00
da312ec3ae Implement PaymentActivity dan TransactionActivity 2025-06-27 17:03:14 +07:00
53964211c2 Result Transaction dan QRIS 2025-06-27 17:01:05 +07:00
b66ef4bb00 proeses transaction post backend and database 2025-06-26 15:48:12 +07:00
7a2ddc3f15 display result transation 2025-06-26 13:57:07 +07:00
8a73206a76 safepoint charge 2025-06-25 20:56:28 +07:00
f6650f99d0 ganti akun midtrans 2025-06-25 12:54:01 +07:00
8ac97437a2 add Success Screen 2025-06-25 10:17:56 +07:00
2b57d35553 custom appbar 2025-06-25 09:28:05 +07:00
f2c3de9f5f refactor 2025-06-25 00:35:03 +07:00
f5d9e53118 Safepoint Transaction-1 2025-06-23 20:42:33 +07:00
ece79942c1 refactor transaction 2025-06-23 11:38:31 +07:00
0af0e836b1 menambahkan mdal pada screen emv 2025-06-23 10:52:04 +07:00
f403358554 Safepoint Modal Scan Card 2025-06-23 09:20:26 +07:00
d43c4bad0c card Scanning tanpa button 2025-06-22 22:03:22 +07:00
174a1461fd refactor credit card 2025-06-22 20:10:56 +07:00
f4e5e03077 adding input amount transaction 2025-06-21 01:15:40 +07:00
f48e3e64a4 Safepoint Check EMV 2025-06-18 23:42:48 +07:00
2ea0792d28 Implement EMV 2025-06-18 14:41:18 +07:00
9834d4b841 init config EM 2025-06-18 11:49:34 +07:00
8add903edb Display UI card read 2025-06-18 11:44:11 +07:00
124da43a1e Safepoint Card Reading 2025-06-17 22:49:41 +07:00
d7617186a6 Refactor structur java 2025-06-17 15:15:40 +07:00
93fc410e37 Setting Config 2025-06-17 15:06:22 +07:00
448dfd9835 QRISFLOW DESIGN IMPROVED 2025-06-17 14:39:12 +07:00
eac3179d8a QRISFLOW Safepoint 2025-06-16 14:57:01 +07:00
729bdddad4 safepoint qris result activity 2025-06-13 15:40:56 +07:00
c56cae64b9 safepoint Detail transaksi 2025-06-13 14:41:10 +07:00
d4245c5906 Sorting List 2025-06-13 09:29:35 +07:00
eddade3200 safepoint QRIS 2025-06-12 16:56:26 +07:00
13ab6b717e implement SDK di MainActivity 2025-06-11 23:07:35 +07:00
991f77dabe transaction update 2025-06-10 16:58:42 +07:00
da8bcf17cc fix list 2025-06-10 13:24:32 +07:00
b0ee2e8ee6 memperbaiki list cetak ulang 2025-06-10 12:10:35 +07:00
4aaa9957e7 solved duplicate data 2025-06-09 18:36:52 +07:00
99fab68e71 QRIS FLOW 2025-06-09 12:04:58 +07:00
074a4b1f53 Fix payment dan struk 2025-06-09 01:27:59 +07:00
a1f536b03e midtrans solve 2025-06-08 17:30:07 +07:00
edca7f92ec implement history 2025-06-06 05:29:37 +07:00
3f189f5975 implement menu settlement 2025-06-05 16:19:46 +07:00
5a03fc3aec UI Cetak ulang struk 2025-06-05 13:03:44 +07:00
a30e767adc update UI QRIS Result 2025-06-03 17:17:46 +07:00
74f95e0374 update UI QRIS 2025-06-02 13:16:46 +07:00
1799e7eb0e adjustment 2025-05-30 20:17:43 +07:00
2a24016637 implement menu debit dan qris 2025-05-30 19:27:43 +07:00
459d9ab0f1 improve page struct 2025-05-30 16:13:51 +07:00
191966a2e4 improve payment success UI 2025-05-30 14:59:46 +07:00
46fb81b6a7 initialize success sreen dan receipt screen 2025-05-30 11:47:53 +07:00
290f3015d9 Improve UI Pin Page 2025-05-28 21:31:57 +07:00
f1228db89a UI PIN 2025-05-28 14:33:26 +07:00
810964b4be menambahkan modal 2025-05-28 12:06:20 +07:00
a7fa40d60a UI Kartu Kredit 2025-05-27 15:46:20 +07:00
a07e7a99ac improve button lainnya 2025-05-23 01:04:54 +07:00
c55af6141f improve menu home page 2025-05-22 23:33:20 +07:00
6d681f5e41 Implement PaymentActivity dan TransactionActivity 2025-05-22 17:14:08 +07:00
1ca26371a1 Menu Home dan Icon 2025-05-22 17:03:58 +07:00
129 changed files with 16026 additions and 1595 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

View File

@@ -6,10 +6,17 @@ android {
namespace 'com.example.bdkipoc' namespace 'com.example.bdkipoc'
compileSdk 35 compileSdk 35
// Tambahkan lint options
lint {
abortOnError false
disable 'GoogleAppIndexingWarning'
disable 'NonConstantResourceId'
}
defaultConfig { defaultConfig {
applicationId "com.example.bdkipoc" applicationId "com.example.bdkipoc"
minSdk 21 minSdk 21
targetSdk 30 targetSdk 33
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@@ -22,19 +29,32 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
// Keep Java 11 - lebih modern dari referensi
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11
} }
// Tambahkan sourceSets untuk native libs jika diperlukan
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
} }
dependencies { dependencies {
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
implementation libs.appcompat implementation libs.appcompat
implementation libs.material implementation libs.material
implementation libs.activity implementation libs.activity
implementation libs.constraintlayout implementation libs.constraintlayout
implementation libs.cardview implementation libs.cardview
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'com.sunmi:printerlibrary:1.0.15'
// Test dependencies
testImplementation libs.junit testImplementation libs.junit
androidTestImplementation libs.ext.junit androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core androidTestImplementation libs.espresso.core

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -8,7 +8,23 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="com.sunmi.perm.LED" />
<uses-permission android:name="com.sunmi.perm.MSR" />
<uses-permission android:name="com.sunmi.perm.ICC" />
<uses-permission android:name="com.sunmi.perm.PINPAD" />
<uses-permission android:name="com.sunmi.perm.SECURITY" />
<uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name=".MyApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -17,6 +33,7 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.BDKIPOC" android:theme="@style/Theme.BDKIPOC"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@@ -28,12 +45,45 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".TransactionActivity" android:name=".cetakulang.ReprintActivity"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".PaymentActivity" android:name=".cetakulang.ReprintAdapterActivity"
android:exported="false" /> android:exported="false" />
<activity android:name=".QrisResultActivity" />
<activity
android:name=".ReceiptActivity"
android:exported="false" />
<activity
android:name=".QrisActivity"
android:exported="false" />
<activity
android:name=".QrisResultActivity"
android:exported="false" />
<activity
android:name=".SettlementActivity"
android:exported="false" />
<activity
android:name=".HistoryActivity"
android:exported="false" />
<activity
android:name=".HistoryDetailActivity"
android:exported="false" />
<activity
android:name=".transaction.CreateTransactionActivity"
android:exported="false" />
<activity
android:name=".transaction.ResultTransactionActivity"
android:exported="false" />
<activity android:name="com.sunmi.emv.l2.view.AppSelectActivity"/>
</application> </application>
</manifest> </manifest>

View 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);
}
}

View 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";
}

View File

@@ -1,41 +1,450 @@
package com.example.bdkipoc; package com.example.bdkipoc;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.WindowManager;
import androidx.activity.EdgeToEdge; import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView; import androidx.cardview.widget.CardView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.graphics.Insets; import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.button.MaterialButton;
import com.example.bdkipoc.cetakulang.ReprintActivity;
import com.example.bdkipoc.cetakulang.ReprintAdapterActivity;
import com.example.bdkipoc.R;
import com.example.bdkipoc.transaction.CreateTransactionActivity;
import com.example.bdkipoc.transaction.ResultTransactionActivity;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private boolean isExpanded = false; // False = showing only 9 main menus, True = showing all 15 menus
private MaterialButton btnLainnya;
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
// Enable hardware acceleration for smoother scrolling
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
EdgeToEdge.enable(this); EdgeToEdge.enable(this);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets; return insets;
}); });
// Set up click listeners for the cards // Initialize views
CardView paymentCard = findViewById(R.id.card_payment); btnLainnya = findViewById(R.id.btn_lainnya);
CardView transactionsCard = findViewById(R.id.card_transactions);
paymentCard.setOnClickListener(v -> { // Check if we're returning from a completed transaction
// Launch payment activity checkTransactionCompletion();
startActivity(new android.content.Intent(MainActivity.this, PaymentActivity.class));
});
transactionsCard.setOnClickListener(v -> { // Setup initial state - 9 main menus visible, 6 dummy menus hidden
// Launch transactions activity setupInitialMenuState();
startActivity(new android.content.Intent(MainActivity.this, TransactionActivity.class));
// 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", "==================================");
}
}

View 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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

File diff suppressed because it is too large Load Diff

View 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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}
}

View 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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}
}

View 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();
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}
}

View File

@@ -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);
}
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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");
});
}
}
}

View File

@@ -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);
}
};
}

View File

@@ -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, "==============================");
}
}

View 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;
}
}

View 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;
}
}
}

View 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);
}
}

View File

@@ -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 {
}
}

View 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" />

View 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" />

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View 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>

View File

@@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#666666" />
<corners android:radius="4dp" />
</shape>

View 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>

Some files were not shown because too many files have changed in this diff Show More