Compare commits

...

79 Commits

Author SHA1 Message Date
8cef8fdb22 refactor code + generate qr (bug pending) 2025-08-02 11:43:46 +07:00
e0aec6e840 menambah interval waktu ke 15 menit 2025-08-01 09:48:52 +07:00
538249fc57 penambahan button di QRIS 2025-07-30 17:20:41 +07:00
a38cea065f Create Transaction bug reset State 2025-07-07 11:56:16 +07:00
671b585fe5 Implement UI Info Toko 2025-07-04 18:47:44 +07:00
c18fd2d831 Implement API Info Toko 2025-07-02 17:45:41 +07:00
c033a26516 Implement API ke Form Bantuan 2025-07-02 12:47:09 +07:00
f64779755a Improve BatuanActivitiy 2025-07-01 17:37:55 +07:00
22d0409c0a Implement API ke Form Bantuan 2025-07-01 17:06:02 +07:00
2803182a02 Implement Banner UI 2025-07-01 16:11:40 +07:00
960f64ee81 Implement Integrasi Prduction key midtrans 2025-07-01 13:07:14 +07:00
9dac55d07a Implement BantuanFormActivity API 2025-07-01 11:44:30 +07:00
ddf76d2540 update ResultTransaction integrasi dengan printer 2025-06-30 22:36:06 +07:00
b2442ada48 Implement ReceiptActivity dengan printer 2025-06-30 21:43:06 +07:00
a52f56e154 Implement BantuanFormActivity UI 2025-06-30 15:22:59 +07:00
4209b193d7 implement login API dan BantuanActivity Riwayaat API 2025-06-30 14:50:03 +07:00
44225f1d67 backup bantuan activity with api 2025-06-30 10:18:57 +07:00
88069c0b56 Implement BantuanActivity UI 2025-06-29 20:33:23 +07:00
6f98a91372 Implement BantuanActivity 2025-06-29 15:25:34 +07:00
597921e32b Implement HistoryActivity 2025-06-29 02:54:08 +07:00
3ac3598359 implement History 2025-06-28 21:42:32 +07:00
6660fca373 Implement PaymentActivity dan ReprintActivity 2025-06-28 00:03:27 +07:00
edb1c6d09b layouting cetak ulang 2025-06-27 21:04:29 +07:00
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
158 changed files with 21931 additions and 1604 deletions

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

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

View File

@ -5,11 +5,18 @@ plugins {
android {
namespace 'com.example.bdkipoc'
compileSdk 35
// Tambahkan lint options
lint {
abortOnError false
disable 'GoogleAppIndexingWarning'
disable 'NonConstantResourceId'
}
defaultConfig {
applicationId "com.example.bdkipoc"
minSdk 21
targetSdk 30
targetSdk 33
versionCode 1
versionName "1.0"
@ -22,19 +29,35 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// Keep Java 11 - lebih modern dari referensi
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
// Tambahkan sourceSets untuk native libs jika diperlukan
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
implementation libs.cardview
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.sunmi:printerlibrary:1.0.15'
// Test dependencies
testImplementation libs.junit
androidTestImplementation libs.ext.junit
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_COARSE_LOCATION" />
<uses-permission android:name="com.sunmi.perm.LED" />
<uses-permission android:name="com.sunmi.perm.MSR" />
<uses-permission android:name="com.sunmi.perm.ICC" />
<uses-permission android:name="com.sunmi.perm.PINPAD" />
<uses-permission android:name="com.sunmi.perm.SECURITY" />
<uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -17,6 +33,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BDKIPOC"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@ -28,12 +45,67 @@
</intent-filter>
</activity>
<activity
android:name=".TransactionActivity"
android:name=".cetakulang.ReprintActivity"
android:exported="false" />
<activity
android:name=".PaymentActivity"
android:name=".cetakulang.ReprintAdapterActivity"
android:exported="false" />
<activity android:name=".QrisResultActivity" />
<activity
android:name=".ReceiptActivity"
android:exported="false" />
<activity
android:name=".QrisActivity"
android:exported="false" />
<!-- FIXED: Updated to correct package path -->
<activity
android:name=".qris.view.QrisResultActivity"
android:exported="false" />
<activity
android:name=".SettlementActivity"
android:exported="false" />
<activity
android:name=".LoginActivity"
android:exported="false" />
<activity
android:name=".histori.HistoryActivity"
android:exported="false" />
<activity
android:name=".histori.HistoryListActivity"
android:exported="false" />
<activity
android:name=".transaction.CreateTransactionActivity"
android:exported="false" />
<activity
android:name=".transaction.ResultTransactionActivity"
android:exported="false" />
<activity
android:name=".settlement.SettlementActivity"
android:exported="false" />
<activity
android:name=".bantuan.BantuanActivity"
android:exported="false" />
<activity
android:name=".bantuan.BantuanFormActivity"
android:exported="false" />
<activity
android:name=".infotoko.InfoTokoActivity"
android:exported="false" />
<activity android:name="com.sunmi.emv.l2.view.AppSelectActivity"/>
</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

@ -0,0 +1,311 @@
package com.example.bdkipoc;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.button.MaterialButton;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LoginActivity extends AppCompatActivity {
private static final String TAG = "LoginActivity";
private static final String API_URL = "https://be-edc.msvc.app/users/auth";
private static final String PREFS_NAME = "LoginPrefs";
private static final String KEY_TOKEN = "token";
private static final String KEY_USER_DATA = "user_data";
private static final String KEY_IS_LOGGED_IN = "is_logged_in";
private EditText etIdentifier, etPassword;
private MaterialButton btnLogin;
private ProgressBar progressBar;
private ExecutorService executor;
private String currentPassword;
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// Enable hardware acceleration
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// Check if user is already logged in
if (isUserLoggedIn()) {
navigateToMainActivity();
return;
}
initializeViews();
setupListeners();
executor = Executors.newSingleThreadExecutor();
}
private void initializeViews() {
etIdentifier = findViewById(R.id.et_identifier);
etPassword = findViewById(R.id.et_password);
btnLogin = findViewById(R.id.btn_login);
progressBar = findViewById(R.id.progress_bar);
}
private void setupListeners() {
btnLogin.setOnClickListener(v -> {
String identifier = etIdentifier.getText().toString().trim();
String password = etPassword.getText().toString().trim();
if (validateInput(identifier, password)) {
performLogin(identifier, password);
}
});
}
private boolean validateInput(String identifier, String password) {
if (TextUtils.isEmpty(identifier)) {
etIdentifier.setError("Email/Username tidak boleh kosong");
etIdentifier.requestFocus();
return false;
}
if (TextUtils.isEmpty(password)) {
etPassword.setError("Password tidak boleh kosong");
etPassword.requestFocus();
return false;
}
if (password.length() < 6) {
etPassword.setError("Password minimal 6 karakter");
etPassword.requestFocus();
return false;
}
return true;
}
private void performLogin(String identifier, String password) {
setLoadingState(true);
currentPassword = password;
executor.execute(() -> {
try {
// Create JSON payload
JSONObject jsonPayload = new JSONObject();
jsonPayload.put("identifier", identifier);
jsonPayload.put("password", password);
// Setup HTTP connection
URL url = new URL(API_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("accept", "*/*");
connection.setDoOutput(true);
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
// Send request
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
// Get response
int responseCode = connection.getResponseCode();
Log.d(TAG, "Response Code: " + responseCode);
BufferedReader reader;
if (responseCode >= 200 && responseCode < 300) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
connection.disconnect();
Log.d(TAG, "Response: " + response.toString());
// Parse response on main thread
runOnUiThread(() -> handleLoginResponse(responseCode, response.toString()));
} catch (Exception e) {
Log.e(TAG, "Login error: " + e.getMessage(), e);
runOnUiThread(() -> {
setLoadingState(false);
Toast.makeText(this, "Koneksi gagal: " + e.getMessage(), Toast.LENGTH_LONG).show();
});
}
});
}
private void handleLoginResponse(int responseCode, String responseBody) {
setLoadingState(false);
try {
JSONObject jsonResponse = new JSONObject(responseBody);
if (responseCode >= 200 && responseCode < 300) {
// Login successful
String message = jsonResponse.optString("message", "");
int status = jsonResponse.optInt("status", 0);
if (status == 200) {
JSONObject result = jsonResponse.getJSONObject("result");
String token = result.getString("token");
JSONObject userData = result.getJSONObject("user");
// Save login data
// saveLoginData(token, userData.toString());
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_TOKEN, token);
editor.putString(KEY_USER_DATA, userData.toString());
editor.putBoolean(KEY_IS_LOGGED_IN, true);
editor.putString("current_password", currentPassword);
editor.apply();
Toast.makeText(this, "Login berhasil! " + message, Toast.LENGTH_SHORT).show();
// Navigate to MainActivity
navigateToMainActivity();
} else {
Toast.makeText(this, "Login gagal: " + message, Toast.LENGTH_LONG).show();
}
} else {
// Login failed
String errorMessage = jsonResponse.optString("message", "Login gagal");
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
Log.e(TAG, "Login failed: " + errorMessage);
}
} catch (Exception e) {
Log.e(TAG, "Error parsing response: " + e.getMessage(), e);
Toast.makeText(this, "Error parsing response", Toast.LENGTH_LONG).show();
}
}
private void setLoadingState(boolean isLoading) {
btnLogin.setEnabled(!isLoading);
etIdentifier.setEnabled(!isLoading);
etPassword.setEnabled(!isLoading);
progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
if (isLoading) {
btnLogin.setText("Memproses...");
} else {
btnLogin.setText("MASUK");
}
}
private void saveLoginData(String token, String userData) {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_TOKEN, token);
editor.putString(KEY_USER_DATA, userData);
editor.putBoolean(KEY_IS_LOGGED_IN, true);
editor.apply();
Log.d(TAG, "Login data saved successfully");
}
private boolean isUserLoggedIn() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
return prefs.getBoolean(KEY_IS_LOGGED_IN, false);
}
private void navigateToMainActivity() {
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
}
// Public static methods untuk mengakses data login dari activity lain
public static String getToken(android.content.Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
return prefs.getString(KEY_TOKEN, "");
}
public static String getUserData(android.content.Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
return prefs.getString(KEY_USER_DATA, "");
}
public static JSONObject getUserDataAsJson(android.content.Context context) {
try {
String userData = getUserData(context);
if (!TextUtils.isEmpty(userData)) {
return new JSONObject(userData);
}
} catch (Exception e) {
Log.e("LoginActivity", "Error parsing user data: " + e.getMessage());
}
return null;
}
public static void logout(android.content.Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.clear();
editor.apply();
// Navigate back to login
Intent intent = new Intent(context, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
}
public static boolean isLoggedIn(android.content.Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
return prefs.getBoolean(KEY_IS_LOGGED_IN, false);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
}
}
}

View File

@ -1,41 +1,643 @@
package com.example.bdkipoc;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ImageView;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.WindowManager;
import android.view.Menu;
import android.view.MenuItem;
import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.button.MaterialButton;
import com.example.bdkipoc.R;
import com.example.bdkipoc.cetakulang.ReprintActivity;
import com.example.bdkipoc.cetakulang.ReprintAdapterActivity;
import com.example.bdkipoc.histori.HistoryActivity;
import com.example.bdkipoc.transaction.CreateTransactionActivity;
import com.example.bdkipoc.transaction.ResultTransactionActivity;
import com.example.bdkipoc.settlement.SettlementActivity;
import com.example.bdkipoc.bantuan.BantuanActivity;
import com.example.bdkipoc.infotoko.InfoTokoActivity;
import org.json.JSONObject;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private boolean isExpanded = false; // False = showing only 9 main menus, True = showing all 15 menus
private MaterialButton btnLainnya;
private MaterialButton logoutButton;
private TextView tvUserName, tvUserRole;
private LinearLayout userInfoSection;
private String authToken;
private JSONObject userData;
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// Enable hardware acceleration for smoother scrolling
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
);
super.onCreate(savedInstanceState);
// Check if user is logged in
if (!LoginActivity.isLoggedIn(this)) {
// User is not logged in, redirect to login
Intent intent = new Intent(this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return;
}
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// Set up click listeners for the cards
CardView paymentCard = findViewById(R.id.card_payment);
CardView transactionsCard = findViewById(R.id.card_transactions);
// Load user data
loadUserData();
paymentCard.setOnClickListener(v -> {
// Launch payment activity
startActivity(new android.content.Intent(MainActivity.this, PaymentActivity.class));
// Initialize views
btnLainnya = findViewById(R.id.btn_lainnya);
logoutButton = findViewById(R.id.logout_button);
tvUserName = findViewById(R.id.tv_user_name);
tvUserRole = findViewById(R.id.tv_user_role);
userInfoSection = findViewById(R.id.user_info_section);
// Setup logout button
if (logoutButton != null) {
logoutButton.setOnClickListener(v -> performLogout());
}
// Check if we're returning from a completed transaction
checkTransactionCompletion();
// Setup initial state - 9 main menus visible, 6 dummy menus hidden
setupInitialMenuState();
// Setup menu listeners
setupMenuListeners();
// Display user info
displayUserInfo();
}
private void loadUserData() {
// Get authentication token
authToken = LoginActivity.getToken(this);
// Get user data
userData = LoginActivity.getUserDataAsJson(this);
Log.d(TAG, "Loaded auth token: " + (authToken != null ? "" : ""));
Log.d(TAG, "Loaded user data: " + (userData != null ? "" : ""));
if (userData != null) {
Log.d(TAG, "User: " + userData.optString("name", "Unknown"));
Log.d(TAG, "Email: " + userData.optString("email", "Unknown"));
Log.d(TAG, "Role: " + userData.optString("role", "Unknown"));
}
}
private void displayUserInfo() {
if (userData != null) {
String userName = userData.optString("name", "User");
String userRole = userData.optString("role", "");
// Display welcome message
String welcomeMessage = "Selamat datang, " + userName;
if (!userRole.isEmpty()) {
welcomeMessage += " (" + userRole + ")";
}
// Show welcome toast
Toast.makeText(this, welcomeMessage, Toast.LENGTH_LONG).show();
// Update merchant card with user info
if (tvUserName != null) {
tvUserName.setText(userName);
}
if (tvUserRole != null && !userRole.isEmpty()) {
tvUserRole.setText("(" + userRole + ")");
tvUserRole.setVisibility(View.VISIBLE);
} else if (tvUserRole != null) {
tvUserRole.setVisibility(View.GONE);
}
// Show user info section in merchant card
if (userInfoSection != null) {
userInfoSection.setVisibility(View.VISIBLE);
}
}
}
// Method to get auth token for use in other activities
public static String getAuthToken(android.content.Context context) {
return LoginActivity.getToken(context);
}
// Method to get user data for use in other activities
public static JSONObject getUserData(android.content.Context context) {
return LoginActivity.getUserDataAsJson(context);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Menu logout sudah ada di custom toolbar, tidak perlu action bar menu
return false; // Return false to not show action bar menu
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// No longer needed since logout is handled by custom toolbar button
return super.onOptionsItemSelected(item);
}
private void performLogout() {
// Show confirmation dialog with red theme
androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this);
builder.setTitle("Logout");
builder.setMessage("Apakah Anda yakin ingin keluar dari aplikasi?");
builder.setIcon(android.R.drawable.ic_dialog_alert);
// Set positive button (Ya)
builder.setPositiveButton("Ya", (dialog, which) -> {
// Show logout progress
Toast.makeText(this, "Logging out...", Toast.LENGTH_SHORT).show();
// Perform logout
LoginActivity.logout(this);
});
// Set negative button (Batal)
builder.setNegativeButton("Batal", (dialog, which) -> {
dialog.dismiss();
});
// Create and show dialog
androidx.appcompat.app.AlertDialog dialog = builder.create();
dialog.show();
// Customize button colors
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE)
.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE)
.setTextColor(getResources().getColor(android.R.color.black));
}
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 and auth token
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) {
startActivityWithAuth(new Intent(MainActivity.this, QrisActivity.class));
// Col-2
} else if (cardId == R.id.card_transfer) {
Toast.makeText(this, "Transfer - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_uang_elektronik) {
navigateToCreateTransaction("e_money", cardId, "Uang Elektronik");
} else if (cardId == R.id.card_cetak_ulang) {
startActivityWithAuth(new Intent(MainActivity.this, ReprintActivity.class));
// Col-3
} else if (cardId == R.id.card_refund) {
Toast.makeText(this, "Refund - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_settlement) {
startActivityWithAuth(new Intent(MainActivity.this, SettlementActivity.class));
} else if (cardId == R.id.card_histori) {
startActivityWithAuth(new Intent(MainActivity.this, HistoryActivity.class));
// Col-4
} else if (cardId == R.id.card_bantuan) {
startActivityWithAuth(new Intent(MainActivity.this, BantuanActivity.class));
} else if (cardId == R.id.card_info_toko) {
startActivityWithAuth(new Intent(MainActivity.this, InfoTokoActivity.class));
} else if (cardId == R.id.card_pengaturan) {
Toast.makeText(this, "Pengaturan - Coming Soon", Toast.LENGTH_SHORT).show();
} else {
// Fallback for any other cards
navigateToCreateTransaction("credit_card", cardId, "Unknown");
}
});
}
}
// Get references to ONLY the dummy cards that need to be toggled
CardView[] toggleableCards = {
findViewById(R.id.card_bantuan),
findViewById(R.id.card_info_toko),
findViewById(R.id.card_pengaturan),
};
// Set up "Lainnya" button click listener
btnLainnya.setOnClickListener(v -> {
isExpanded = !isExpanded;
if (isExpanded) {
// Show the 6 dummy menus with animation
for (CardView card : toggleableCards) {
if (card != null) {
card.setVisibility(View.VISIBLE);
card.setAlpha(0f);
card.animate()
.alpha(1f)
.setDuration(300)
.setInterpolator(new AccelerateDecelerateInterpolator())
.start();
}
}
btnLainnya.setText("Tampilkan Lebih Sedikit");
} else {
// Hide the 6 dummy menus with animation
for (CardView card : toggleableCards) {
if (card != null) {
card.animate()
.alpha(0f)
.setDuration(300)
.setInterpolator(new AccelerateDecelerateInterpolator())
.withEndAction(() -> card.setVisibility(View.GONE))
.start();
}
}
btnLainnya.setText("Lainnya");
}
});
transactionsCard.setOnClickListener(v -> {
// Launch transactions activity
startActivity(new android.content.Intent(MainActivity.this, TransactionActivity.class));
// Setup Banner Image
ImageView bannerQris = findViewById(R.id.banner_qris);
if (bannerQris != null) {
bannerQris.setOnClickListener(v -> {
// Pindah ke halaman QRIS
startActivityWithAuth(new Intent(MainActivity.this, QrisActivity.class));
});
}
}
// NEW: Enhanced navigation method with payment type information and auth token
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");
// NEW: Pass authentication data
intent.putExtra("AUTH_TOKEN", authToken);
if (userData != null) {
intent.putExtra("USER_DATA", userData.toString());
}
// DEBUG: Log navigation details
Log.d(TAG, "=== NAVIGATING TO CREATE TRANSACTION ===");
Log.d(TAG, "Payment Type: " + paymentType);
Log.d(TAG, "Card Menu ID: " + cardMenuId);
Log.d(TAG, "Card Name: " + cardName);
Log.d(TAG, "Auth Token: " + (authToken != null ? "" : ""));
Log.d(TAG, "========================================");
startActivity(intent);
} catch (Exception e) {
Log.e(TAG, "Error navigating to CreateTransaction: " + e.getMessage(), e);
Toast.makeText(this, "Error opening transaction: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
// NEW: Helper method to start activity with authentication data
private void startActivityWithAuth(Intent intent) {
try {
// Add authentication data to intent
intent.putExtra("AUTH_TOKEN", authToken);
if (userData != null) {
intent.putExtra("USER_DATA", userData.toString());
}
startActivity(intent);
} catch (Exception e) {
Log.e(TAG, "Error starting activity: " + e.getMessage(), e);
Toast.makeText(this, "Error opening activity: " + 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 {
Log.w(TAG, "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();
// Check if user is still logged in
if (!LoginActivity.isLoggedIn(this)) {
// User is not logged in, redirect to login
Intent intent = new Intent(this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return;
}
// Clear any transaction completion flags to avoid repeated messages
getIntent().removeExtra("transaction_completed");
getIntent().removeExtra("transaction_amount");
// NEW: Log resume for debugging
Log.d(TAG, "MainActivity resumed");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "MainActivity paused");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "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");
// Add authentication data
intent.putExtra("AUTH_TOKEN", LoginActivity.getToken(context));
String userData = LoginActivity.getUserData(context);
if (!userData.isEmpty()) {
intent.putExtra("USER_DATA", userData);
}
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 {
Log.w(TAG, "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() {
Log.d(TAG, "=== 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);
Log.d(TAG,
"Card ID: " + cardId + " -> Payment Type: " + paymentType + " -> Name: " + cardName);
}
Log.d(TAG, "==================================");
}
}

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

View File

@ -0,0 +1,574 @@
package com.example.bdkipoc.bantuan;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.example.bdkipoc.R;
import com.example.bdkipoc.LoginActivity;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BantuanActivity extends AppCompatActivity {
// UI Components
private LinearLayout tabUmum, tabRiwayat;
private TextView textUmum, textRiwayat;
private ScrollView contentUmum, contentRiwayat;
private LinearLayout riwayatContainer;
private LinearLayout btnForm, btnWhatsApp, backNavigation;
// State
private boolean isUmumTabActive = true;
private List<TicketData> ticketList = new ArrayList<>();
private ExecutorService executor = Executors.newSingleThreadExecutor();
private Handler mainHandler = new Handler(Looper.getMainLooper());
// Enhanced TicketData class with date parsing
public static class TicketData {
public String createdAt, ticketCode, issueName, status;
public Date parsedDate; // Added for sorting
public TicketData(String createdAt, String ticketCode, String issueName, String status) {
this.createdAt = createdAt;
this.ticketCode = ticketCode;
this.issueName = issueName;
this.status = status;
this.parsedDate = parseDate(createdAt);
}
// Parse date from ISO format to Date object
private Date parseDate(String dateString) {
try {
// Try different date formats
SimpleDateFormat[] formats = {
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()),
new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
};
for (SimpleDateFormat format : formats) {
try {
return format.parse(dateString);
} catch (Exception e) {
// Continue to next format
}
}
// If all parsing fails, return current date
return new Date();
} catch (Exception e) {
return new Date(); // Fallback to current date
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check authentication
if (!LoginActivity.isLoggedIn(this)) {
LoginActivity.logout(this);
return;
}
setContentView(R.layout.activity_bantuan);
initViews();
setupListeners();
showUmumTab();
loadTicketData();
}
private void initViews() {
// Tabs
tabUmum = findViewById(R.id.tab_umum);
tabRiwayat = findViewById(R.id.tab_riwayat);
textUmum = findViewById(R.id.text_umum);
textRiwayat = findViewById(R.id.text_riwayat);
// Content
contentUmum = findViewById(R.id.content_umum);
contentRiwayat = findViewById(R.id.content_riwayat);
// Riwayat container
if (contentRiwayat != null && contentRiwayat.getChildCount() > 0) {
View child = contentRiwayat.getChildAt(0);
if (child instanceof LinearLayout) {
riwayatContainer = (LinearLayout) child;
}
}
// Buttons
btnForm = findViewById(R.id.btn_form);
btnWhatsApp = findViewById(R.id.btn_whatsapp);
backNavigation = findViewById(R.id.back_navigation);
}
private void setupListeners() {
if (backNavigation != null) {
backNavigation.setOnClickListener(v -> onBackPressed());
}
if (tabUmum != null) {
tabUmum.setOnClickListener(v -> showUmumTab());
}
if (tabRiwayat != null) {
tabRiwayat.setOnClickListener(v -> showRiwayatTab());
}
if (btnForm != null) {
btnForm.setOnClickListener(v -> {
Intent intent = new Intent(this, BantuanFormActivity.class);
startActivity(intent);
});
}
if (btnWhatsApp != null) {
btnWhatsApp.setOnClickListener(v -> openWhatsAppCS());
}
}
private void showUmumTab() {
if (isUmumTabActive) return;
isUmumTabActive = true;
if (contentUmum != null) contentUmum.setVisibility(View.VISIBLE);
if (contentRiwayat != null) contentRiwayat.setVisibility(View.GONE);
updateTabAppearance();
}
private void showRiwayatTab() {
if (!isUmumTabActive) return;
isUmumTabActive = false;
if (contentUmum != null) contentUmum.setVisibility(View.GONE);
if (contentRiwayat != null) contentRiwayat.setVisibility(View.VISIBLE);
updateTabAppearance();
// Load data if available
if (!ticketList.isEmpty()) {
populateRiwayatContent();
} else {
showLoadingMessage();
}
}
private void updateTabAppearance() {
if (isUmumTabActive) {
// Umum active
if (tabUmum != null) tabUmum.setBackgroundResource(R.drawable.tab_active_bg);
if (textUmum != null) textUmum.setTextColor(ContextCompat.getColor(this, android.R.color.white));
// Riwayat inactive
if (tabRiwayat != null) tabRiwayat.setBackgroundResource(R.drawable.tab_inactive_bg);
if (textRiwayat != null) textRiwayat.setTextColor(Color.parseColor("#DE0701"));
} else {
// Riwayat active
if (tabRiwayat != null) tabRiwayat.setBackgroundResource(R.drawable.tab_active_bg);
if (textRiwayat != null) textRiwayat.setTextColor(ContextCompat.getColor(this, android.R.color.white));
// Umum inactive
if (tabUmum != null) tabUmum.setBackgroundResource(R.drawable.tab_inactive_bg);
if (textUmum != null) textUmum.setTextColor(Color.parseColor("#DE0701"));
}
}
// Simplified API call with new endpoint
private void loadTicketData() {
String authToken = LoginActivity.getToken(this);
if (authToken == null || authToken.isEmpty()) {
LoginActivity.logout(this);
return;
}
executor.execute(() -> {
HttpURLConnection connection = null;
try {
// Updated API endpoint
URL url = new URL("https://be-edc.msvc.app/tickets/list");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("accept", "application/json");
connection.setRequestProperty("Authorization", "Bearer " + authToken);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
parseTicketData(response.toString());
} else if (responseCode == 401) {
mainHandler.post(() -> {
android.widget.Toast.makeText(this, "Session expired. Please login again.",
android.widget.Toast.LENGTH_LONG).show();
LoginActivity.logout(this);
});
} else {
mainHandler.post(() -> {
android.widget.Toast.makeText(this, "Failed to load data. Error: " + responseCode,
android.widget.Toast.LENGTH_LONG).show();
});
}
} catch (Exception e) {
mainHandler.post(() -> {
android.widget.Toast.makeText(this, "Network error: " + e.getMessage(),
android.widget.Toast.LENGTH_LONG).show();
});
} finally {
if (connection != null) {
connection.disconnect();
}
}
});
}
// Enhanced JSON parsing with sorting
private void parseTicketData(String jsonResponse) {
try {
JSONObject jsonObject = new JSONObject(jsonResponse);
// Check for different possible response structures
JSONArray dataArray = null;
if (jsonObject.has("results") && jsonObject.getJSONObject("results").has("data")) {
// Structure: { "results": { "data": [...] } }
dataArray = jsonObject.getJSONObject("results").getJSONArray("data");
} else if (jsonObject.has("data")) {
// Structure: { "data": [...] }
dataArray = jsonObject.getJSONArray("data");
} else if (jsonObject.has("tickets")) {
// Structure: { "tickets": [...] }
dataArray = jsonObject.getJSONArray("tickets");
} else {
// Assume the response is directly an array
dataArray = new JSONArray(jsonResponse);
}
List<TicketData> newTicketList = new ArrayList<>();
for (int i = 0; i < dataArray.length(); i++) {
JSONObject ticket = dataArray.getJSONObject(i);
String createdAt = ticket.optString("createdAt", ticket.optString("created_at", ""));
String ticketCode = ticket.optString("ticket_code", ticket.optString("ticketCode", ""));
String status = ticket.optString("status", "");
String issueName = "Tidak ada keterangan";
if (ticket.has("issue") && !ticket.isNull("issue")) {
JSONObject issue = ticket.getJSONObject("issue");
issueName = issue.optString("name", issueName);
} else if (ticket.has("issue_name")) {
issueName = ticket.optString("issue_name", issueName);
} else if (ticket.has("title")) {
issueName = ticket.optString("title", issueName);
}
if (!createdAt.isEmpty() && !ticketCode.isEmpty()) {
newTicketList.add(new TicketData(createdAt, ticketCode, issueName, status));
}
}
// Sort by date - newest first
sortTicketsByDate(newTicketList);
mainHandler.post(() -> {
ticketList.clear();
ticketList.addAll(newTicketList);
if (!isUmumTabActive) {
populateRiwayatContent();
}
android.widget.Toast.makeText(this, "Data riwayat berhasil dimuat (" +
ticketList.size() + " item)", android.widget.Toast.LENGTH_SHORT).show();
});
} catch (Exception e) {
mainHandler.post(() -> {
android.widget.Toast.makeText(this, "Error parsing data: " + e.getMessage(),
android.widget.Toast.LENGTH_LONG).show();
});
}
}
// New method to sort tickets by date (newest first)
private void sortTicketsByDate(List<TicketData> tickets) {
Collections.sort(tickets, new Comparator<TicketData>() {
@Override
public int compare(TicketData ticket1, TicketData ticket2) {
// Sort by parsedDate in descending order (newest first)
if (ticket1.parsedDate == null && ticket2.parsedDate == null) {
return 0;
} else if (ticket1.parsedDate == null) {
return 1; // null dates go to the end
} else if (ticket2.parsedDate == null) {
return -1; // null dates go to the end
} else {
// Compare dates in descending order (newest first)
return ticket2.parsedDate.compareTo(ticket1.parsedDate);
}
}
});
}
private void populateRiwayatContent() {
if (riwayatContainer == null) return;
riwayatContainer.removeAllViews();
if (ticketList.isEmpty()) {
showEmptyMessage();
return;
}
// Data is already sorted, just populate the UI
for (int i = 0; i < ticketList.size(); i++) {
TicketData ticket = ticketList.get(i);
LinearLayout historyItem = createHistoryItem(ticket, i);
if (historyItem != null) {
riwayatContainer.addView(historyItem);
// Add separator except for last item
if (i < ticketList.size() - 1) {
View separator = new View(this);
separator.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, 1));
separator.setBackgroundColor(Color.parseColor("#e0e0e0"));
riwayatContainer.addView(separator);
}
}
}
}
// Enhanced createHistoryItem with position indicator
private LinearLayout createHistoryItem(TicketData ticket, int position) {
LinearLayout mainLayout = new LinearLayout(this);
mainLayout.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
mainLayout.setOrientation(LinearLayout.VERTICAL);
mainLayout.setPadding(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(16));
mainLayout.setBackgroundColor(Color.WHITE);
// Header (date and status)
LinearLayout headerLayout = new LinearLayout(this);
headerLayout.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
headerLayout.setOrientation(LinearLayout.HORIZONTAL);
// Date with "Terbaru" indicator for first item
TextView dateText = new TextView(this);
LinearLayout.LayoutParams dateParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f);
dateText.setLayoutParams(dateParams);
String dateDisplay = formatDate(ticket.createdAt);
if (position == 0) {
dateDisplay += " (Terbaru)";
dateText.setTextColor(Color.parseColor("#DE0701"));
}
dateText.setText(dateDisplay);
dateText.setTextSize(16);
dateText.setTypeface(null, android.graphics.Typeface.BOLD);
// Status
TextView statusText = new TextView(this);
statusText.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
statusText.setText(formatStatus(ticket.status));
statusText.setTextSize(14);
statusText.setTextColor(getStatusColor(ticket.status));
headerLayout.addView(dateText);
headerLayout.addView(statusText);
// Ticket code
TextView ticketCodeText = new TextView(this);
LinearLayout.LayoutParams ticketParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
ticketParams.setMargins(0, dpToPx(4), 0, 0);
ticketCodeText.setLayoutParams(ticketParams);
ticketCodeText.setText("Nomor tiket: " + ticket.ticketCode);
ticketCodeText.setTextSize(14);
ticketCodeText.setTextColor(ContextCompat.getColor(this, android.R.color.darker_gray));
// Issue name
TextView issueText = new TextView(this);
LinearLayout.LayoutParams issueParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
issueParams.setMargins(0, dpToPx(8), 0, 0);
issueText.setLayoutParams(issueParams);
issueText.setText(ticket.issueName);
issueText.setTextSize(16);
mainLayout.addView(headerLayout);
mainLayout.addView(ticketCodeText);
mainLayout.addView(issueText);
return mainLayout;
}
private void showLoadingMessage() {
if (riwayatContainer == null) return;
riwayatContainer.removeAllViews();
TextView loadingText = new TextView(this);
loadingText.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
loadingText.setText("Memuat data...");
loadingText.setTextSize(16);
loadingText.setGravity(android.view.Gravity.CENTER);
loadingText.setPadding(dpToPx(16), dpToPx(32), dpToPx(16), dpToPx(32));
loadingText.setTextColor(ContextCompat.getColor(this, android.R.color.darker_gray));
riwayatContainer.addView(loadingText);
}
private void showEmptyMessage() {
TextView emptyText = new TextView(this);
emptyText.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
emptyText.setText("Belum ada data riwayat");
emptyText.setTextSize(16);
emptyText.setGravity(android.view.Gravity.CENTER);
emptyText.setPadding(dpToPx(16), dpToPx(32), dpToPx(16), dpToPx(32));
emptyText.setTextColor(ContextCompat.getColor(this, android.R.color.darker_gray));
riwayatContainer.addView(emptyText);
}
private void openWhatsAppCS() {
try {
JSONObject userData = LoginActivity.getUserDataAsJson(this);
String userName = "User";
String userEmail = "";
if (userData != null) {
userName = userData.optString("name", "User");
userEmail = userData.optString("email", "");
}
String phoneNumber = "+6281234567890"; // Update with actual CS number
String message = "Halo, saya " + userName;
if (!userEmail.isEmpty()) {
message += " (" + userEmail + ")";
}
message += " butuh bantuan terkait Payvora PRO";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(android.net.Uri.parse("https://wa.me/" + phoneNumber + "?text=" +
android.net.Uri.encode(message)));
startActivity(intent);
} catch (Exception e) {
android.widget.Toast.makeText(this, "Error opening WhatsApp: " + e.getMessage(),
android.widget.Toast.LENGTH_SHORT).show();
}
}
// Utility methods
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 (Exception e) {
return isoDate.length() > 10 ? isoDate.substring(0, 10) : isoDate;
}
}
private String formatStatus(String status) {
switch (status.toLowerCase()) {
case "new": return "Pengajuan";
case "on_progres": return "Proses";
case "done": return "Selesai";
case "cancel": return "Cancel";
default: return status;
}
}
private int getStatusColor(String status) {
switch (status.toLowerCase()) {
case "new": return ContextCompat.getColor(this, android.R.color.holo_blue_light);
case "on_progres": return ContextCompat.getColor(this, android.R.color.holo_orange_light);
case "done": return ContextCompat.getColor(this, android.R.color.holo_green_dark);
case "cancel": return ContextCompat.getColor(this, android.R.color.holo_red_dark);
default: return ContextCompat.getColor(this, android.R.color.black);
}
}
private int dpToPx(int dp) {
float density = getResources().getDisplayMetrics().density;
return Math.round(dp * density);
}
@Override
protected void onResume() {
super.onResume();
if (!LoginActivity.isLoggedIn(this)) {
finish();
}
// Refresh data when returning from form
loadTicketData();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
}
}
}

View File

@ -0,0 +1,950 @@
package com.example.bdkipoc.bantuan;
import android.app.DatePickerDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import com.example.bdkipoc.R;
import com.example.bdkipoc.LoginActivity;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BantuanFormActivity extends AppCompatActivity {
private static final String TAG = "BantuanFormActivity";
// UI Components
private EditText etTicketCode;
private Spinner spinnerSource, spinnerIssue, spinnerMerchant, spinnerAssign;
private TextView tvStatus, tvResolvedDate;
private LinearLayout llResolvedDate;
private Button btnKirim;
private LinearLayout backNavigation;
private LinearLayout successScreen;
private LinearLayout mainContent;
// Data
private String selectedResolvedDate = "";
private ExecutorService executor = Executors.newSingleThreadExecutor();
private Handler mainHandler = new Handler(Looper.getMainLooper());
// Dynamic data lists
private List<ParameterDetail> sourceList = new ArrayList<>();
private List<ParameterDetail> issueList = new ArrayList<>();
private List<ParameterDetail> assignList = new ArrayList<>();
private List<ParameterDetail> merchantList = new ArrayList<>();
// Inner class for parameter details
public static class ParameterDetail {
public int id;
public String name;
public ParameterDetail(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return name;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check authentication
if (!LoginActivity.isLoggedIn(this)) {
LoginActivity.logout(this);
return;
}
setContentView(R.layout.activity_bantuan_form);
initViews();
// Load dynamic data first, then setup spinners
loadHeaderParams();
loadUsers();
loadMerchants();
setupListeners();
// Initialize button state
updateButtonState();
}
private void initViews() {
// Main content containers
mainContent = findViewById(R.id.main_content);
successScreen = findViewById(R.id.success_screen);
// Form fields
etTicketCode = findViewById(R.id.et_ticket_code);
// Spinners
spinnerSource = findViewById(R.id.spinner_source);
spinnerIssue = findViewById(R.id.spinner_issue);
spinnerMerchant = findViewById(R.id.spinner_merchant);
spinnerAssign = findViewById(R.id.spinner_assign);
// Resolved Date components
llResolvedDate = findViewById(R.id.ll_resolved_date);
tvResolvedDate = findViewById(R.id.tv_resolved_date);
// Status (read-only)
tvStatus = findViewById(R.id.tv_status);
// Buttons
btnKirim = findViewById(R.id.btn_kirim);
backNavigation = findViewById(R.id.back_navigation);
}
private void loadHeaderParams() {
String authToken = LoginActivity.getToken(this);
if (authToken == null || authToken.isEmpty()) {
LoginActivity.logout(this);
return;
}
executor.execute(() -> {
HttpURLConnection connection = null;
try {
URL url = new URL("https://be-edc.msvc.app/header-params/list");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Authorization", "Bearer " + authToken);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
// Parse response
JSONObject jsonResponse = new JSONObject(response.toString());
JSONArray dataArray = jsonResponse.getJSONArray("data");
// Clear existing lists
sourceList.clear();
issueList.clear();
// Process each header parameter
for (int i = 0; i < dataArray.length(); i++) {
JSONObject headerParam = dataArray.getJSONObject(i);
String code = headerParam.getString("code");
String name = headerParam.getString("name");
// Check if this is ticket_sources (code "01")
if ("01".equals(code) && "ticket_sources".equals(name)) {
JSONArray details = headerParam.getJSONArray("details");
for (int j = 0; j < details.length(); j++) {
JSONObject detail = details.getJSONObject(j);
int id = detail.getInt("id");
String detailName = detail.getString("name");
String status = detail.getString("status");
// Only add active items
if ("1".equals(status)) {
sourceList.add(new ParameterDetail(id, detailName));
}
}
}
// Check if this is issue_categories (code "02")
else if ("02".equals(code) && "issue_categories".equals(name)) {
JSONArray details = headerParam.getJSONArray("details");
for (int j = 0; j < details.length(); j++) {
JSONObject detail = details.getJSONObject(j);
int id = detail.getInt("id");
String detailName = detail.getString("name");
String status = detail.getString("status");
// Only add active items
if ("1".equals(status)) {
issueList.add(new ParameterDetail(id, detailName));
}
}
}
}
// Update UI on main thread
mainHandler.post(() -> {
setupSpinners();
updateButtonState();
});
} else if (responseCode == 401) {
mainHandler.post(() -> {
Toast.makeText(this, "Session expired. Please login again.", Toast.LENGTH_LONG).show();
LoginActivity.logout(this);
});
} else {
mainHandler.post(() -> {
Toast.makeText(this, "Failed to load data. Error: " + responseCode, Toast.LENGTH_LONG).show();
// Setup with empty data
setupSpinners();
});
}
} catch (Exception e) {
mainHandler.post(() -> {
Toast.makeText(this, "Network error: " + e.getMessage(), Toast.LENGTH_LONG).show();
// Setup with empty data
setupSpinners();
});
} finally {
if (connection != null) {
connection.disconnect();
}
}
});
}
private void loadMerchants() {
String authToken = LoginActivity.getToken(this);
if (authToken == null || authToken.isEmpty()) {
LoginActivity.logout(this);
return;
}
executor.execute(() -> {
HttpURLConnection connection = null;
try {
URL url = new URL("https://be-edc.msvc.app/merchants/list?location_id=0");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Authorization", "Bearer " + authToken);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
// Parse response
JSONObject jsonResponse = new JSONObject(response.toString());
JSONArray dataArray = jsonResponse.getJSONArray("data");
// Clear existing list
merchantList.clear();
// Process each merchant
for (int i = 0; i < dataArray.length(); i++) {
JSONObject merchant = dataArray.getJSONObject(i);
String status = merchant.getString("status");
// Only add active merchants
if ("1".equals(status)) {
int id = merchant.getInt("id");
String name = merchant.getString("name");
String merchantCode = merchant.optString("merchant_code", "");
// Only add merchants that have merchant_code
if (!merchantCode.isEmpty()) {
merchantList.add(new ParameterDetail(id, name));
}
}
}
// Update UI on main thread
mainHandler.post(() -> {
setupSpinners();
updateButtonState();
});
} else if (responseCode == 401) {
mainHandler.post(() -> {
Toast.makeText(this, "Session expired. Please login again.", Toast.LENGTH_LONG).show();
LoginActivity.logout(this);
});
} else {
mainHandler.post(() -> {
Toast.makeText(this, "Failed to load merchants. Error: " + responseCode, Toast.LENGTH_LONG).show();
// Setup with empty data
setupSpinners();
});
}
} catch (Exception e) {
mainHandler.post(() -> {
Toast.makeText(this, "Network error loading merchants: " + e.getMessage(), Toast.LENGTH_LONG).show();
// Setup with empty data
setupSpinners();
});
} finally {
if (connection != null) {
connection.disconnect();
}
}
});
}
private void showSuccessScreen() {
Log.d(TAG, "Showing success screen");
// Hide main content and show success screen
if (mainContent != null) {
mainContent.setVisibility(View.GONE);
}
if (successScreen != null) {
successScreen.setVisibility(View.VISIBLE);
}
// Navigate back to BantuanActivity after 2 seconds
new Handler(Looper.getMainLooper()).postDelayed(() -> {
navigateToBantuanActivity();
}, 2000);
}
private void navigateToBantuanActivity() {
Log.d(TAG, "Navigating back to BantuanActivity");
try {
// Create intent to BantuanActivity
Intent intent = new Intent(this, Class.forName("com.example.bdkipoc.bantuan.BantuanActivity"));
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
} catch (ClassNotFoundException e) {
Log.e(TAG, "BantuanActivity class not found", e);
// Fallback: just finish current activity
finish();
}
}
private void loadUsers() {
String authToken = LoginActivity.getToken(this);
if (authToken == null || authToken.isEmpty()) {
LoginActivity.logout(this);
return;
}
executor.execute(() -> {
HttpURLConnection connection = null;
try {
URL url = new URL("https://be-edc.msvc.app/users");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Authorization", "Bearer " + authToken);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
// Parse response
JSONArray usersArray = new JSONArray(response.toString());
// Clear existing list
assignList.clear();
// Process each user
for (int i = 0; i < usersArray.length(); i++) {
JSONObject user = usersArray.getJSONObject(i);
String role = user.getString("role");
boolean isActive = user.getBoolean("is_active");
// Only add superadmin users who are active
if ("superadmin".equals(role) && isActive) {
int id = user.getInt("id");
String name = user.getString("name");
assignList.add(new ParameterDetail(id, name));
}
}
// Update UI on main thread
mainHandler.post(() -> {
setupSpinners();
updateButtonState();
});
} else if (responseCode == 401) {
mainHandler.post(() -> {
Toast.makeText(this, "Session expired. Please login again.", Toast.LENGTH_LONG).show();
LoginActivity.logout(this);
});
} else {
mainHandler.post(() -> {
Toast.makeText(this, "Failed to load users. Error: " + responseCode, Toast.LENGTH_LONG).show();
// Setup with empty data
setupSpinners();
});
}
} catch (Exception e) {
mainHandler.post(() -> {
Toast.makeText(this, "Network error loading users: " + e.getMessage(), Toast.LENGTH_LONG).show();
// Setup with empty data
setupSpinners();
});
} finally {
if (connection != null) {
connection.disconnect();
}
}
});
}
private void setupSpinners() {
// Setup Source Spinner (Dynamic)
List<String> sourceOptions = new ArrayList<>();
sourceOptions.add("Pilih Source");
for (ParameterDetail source : sourceList) {
sourceOptions.add(source.name);
}
ArrayAdapter<String> sourceAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, sourceOptions);
sourceAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerSource.setAdapter(sourceAdapter);
// Setup Issue Spinner (Dynamic)
List<String> issueOptions = new ArrayList<>();
issueOptions.add("Pilih Issue");
for (ParameterDetail issue : issueList) {
issueOptions.add(issue.name);
}
ArrayAdapter<String> issueAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, issueOptions);
issueAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerIssue.setAdapter(issueAdapter);
// Setup Merchant Spinner (Dynamic)
List<String> merchantOptions = new ArrayList<>();
merchantOptions.add("Pilih Merchant");
for (ParameterDetail merchant : merchantList) {
merchantOptions.add(merchant.name);
}
ArrayAdapter<String> merchantAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, merchantOptions);
merchantAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerMerchant.setAdapter(merchantAdapter);
// Setup Assign Spinner (Dynamic)
List<String> assignOptions = new ArrayList<>();
assignOptions.add("Pilih Assign");
for (ParameterDetail assign : assignList) {
assignOptions.add(assign.name);
}
ArrayAdapter<String> assignAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, assignOptions);
assignAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerAssign.setAdapter(assignAdapter);
// Setup Resolved Date components (no spinner setup needed)
// Set status to fixed value
if (tvStatus != null) {
tvStatus.setText("Pengajuan");
}
}
private void setupListeners() {
// Back navigation
if (backNavigation != null) {
backNavigation.setOnClickListener(v -> onBackPressed());
}
// Source spinner listener
spinnerSource.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
updateButtonState();
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {}
});
// Issue spinner listener
spinnerIssue.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
updateButtonState();
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {}
});
// Merchant spinner listener
spinnerMerchant.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
updateButtonState();
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {}
});
// Assign spinner listener
spinnerAssign.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
updateButtonState();
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {}
});
// Resolved Date click listener
if (llResolvedDate != null) {
llResolvedDate.setOnClickListener(v -> showDatePicker());
}
// Text watcher for EditText field
setupTextWatcher();
// Submit button listener
if (btnKirim != null) {
btnKirim.setOnClickListener(v -> {
if (btnKirim.isEnabled()) {
submitForm();
}
});
}
}
private void setupTextWatcher() {
android.text.TextWatcher textWatcher = new android.text.TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(android.text.Editable s) {
updateButtonState();
}
};
if (etTicketCode != null) {
etTicketCode.addTextChangedListener(textWatcher);
}
}
private void updateButtonState() {
if (btnKirim == null) return;
boolean isFormValid = checkFormValidity();
if (isFormValid) {
// Active state - Red background
btnKirim.setBackgroundColor(0xFFDE0701); // Red color matching theme
btnKirim.setTextColor(getResources().getColor(android.R.color.white));
btnKirim.setEnabled(true);
btnKirim.setAlpha(1.0f);
} else {
// Inactive state - Gray background
btnKirim.setBackgroundColor(getResources().getColor(android.R.color.darker_gray));
btnKirim.setTextColor(getResources().getColor(android.R.color.white));
btnKirim.setEnabled(false);
btnKirim.setAlpha(0.6f);
}
}
private boolean checkFormValidity() {
// Check if all required fields have values
boolean hasTicketCode = etTicketCode != null && !etTicketCode.getText().toString().trim().isEmpty();
boolean hasSource = spinnerSource != null && spinnerSource.getSelectedItemPosition() > 0;
boolean hasIssue = spinnerIssue != null && spinnerIssue.getSelectedItemPosition() > 0;
boolean hasMerchant = spinnerMerchant != null && spinnerMerchant.getSelectedItemPosition() > 0;
boolean hasAssign = spinnerAssign != null && spinnerAssign.getSelectedItemPosition() > 0;
boolean hasResolvedDate = !selectedResolvedDate.isEmpty();
return hasTicketCode && hasSource && hasIssue && hasMerchant && hasAssign && hasResolvedDate;
}
private void showDatePicker() {
Calendar calendar = Calendar.getInstance();
DatePickerDialog datePickerDialog = new DatePickerDialog(
this,
(view, year, month, dayOfMonth) -> {
Calendar selectedCalendar = Calendar.getInstance();
selectedCalendar.set(year, month, dayOfMonth);
// Format for API (ISO 8601)
SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault());
selectedResolvedDate = apiFormat.format(selectedCalendar.getTime());
// Format for display
SimpleDateFormat displayFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
String displayDate = displayFormat.format(selectedCalendar.getTime());
// Update TextView to show selected date
if (tvResolvedDate != null) {
tvResolvedDate.setText(displayDate);
tvResolvedDate.setTextColor(0xFF000000); // Black color for selected date
}
Log.d(TAG, "Date selected: " + displayDate + " (API format: " + selectedResolvedDate + ")");
// Update button state after date selection
updateButtonState();
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
);
// Set minimum date to today to prevent past dates
datePickerDialog.getDatePicker().setMinDate(System.currentTimeMillis());
datePickerDialog.show();
}
private int getSourceId(int position) {
if (position > 0 && position <= sourceList.size()) {
return sourceList.get(position - 1).id;
}
return 0;
}
private int getIssueId(int position) {
if (position > 0 && position <= issueList.size()) {
return issueList.get(position - 1).id;
}
return 0;
}
private int getMerchantId(int position) {
if (position > 0 && position <= merchantList.size()) {
return merchantList.get(position - 1).id;
}
return 0;
}
private int getAssignId(int position) {
if (position > 0 && position <= assignList.size()) {
return assignList.get(position - 1).id;
}
return 0;
}
private void submitForm() {
if (!validateForm()) {
Log.w(TAG, "Form validation failed");
return;
}
Log.d(TAG, "Form validation passed, preparing data...");
// Disable button and show loading
btnKirim.setEnabled(false);
btnKirim.setText("Mengirim...");
// Prepare form data
String ticketCode = etTicketCode.getText().toString().trim();
int sourceId = getSourceId(spinnerSource.getSelectedItemPosition());
int issueId = getIssueId(spinnerIssue.getSelectedItemPosition());
int merchantId = getMerchantId(spinnerMerchant.getSelectedItemPosition());
int assignId = getAssignId(spinnerAssign.getSelectedItemPosition());
// Validate IDs
if (sourceId == 0) {
Log.e(TAG, "Invalid source ID: " + sourceId + ", position: " + spinnerSource.getSelectedItemPosition());
Toast.makeText(this, "Error: Source tidak valid", Toast.LENGTH_SHORT).show();
btnKirim.setEnabled(true);
btnKirim.setText("Kirim Sekarang");
return;
}
if (issueId == 0) {
Log.e(TAG, "Invalid issue ID: " + issueId + ", position: " + spinnerIssue.getSelectedItemPosition());
Toast.makeText(this, "Error: Issue tidak valid", Toast.LENGTH_SHORT).show();
btnKirim.setEnabled(true);
btnKirim.setText("Kirim Sekarang");
return;
}
if (merchantId == 0) {
Log.e(TAG, "Invalid merchant ID: " + merchantId + ", position: " + spinnerMerchant.getSelectedItemPosition());
Toast.makeText(this, "Error: Merchant tidak valid", Toast.LENGTH_SHORT).show();
btnKirim.setEnabled(true);
btnKirim.setText("Kirim Sekarang");
return;
}
if (assignId == 0) {
Log.e(TAG, "Invalid assign ID: " + assignId + ", position: " + spinnerAssign.getSelectedItemPosition());
Toast.makeText(this, "Error: Assign To tidak valid", Toast.LENGTH_SHORT).show();
btnKirim.setEnabled(true);
btnKirim.setText("Kirim Sekarang");
return;
}
Log.d(TAG, "All IDs validated successfully");
submitToAPI(ticketCode, sourceId, issueId, merchantId, assignId, selectedResolvedDate);
}
private boolean validateForm() {
boolean isValid = true;
// Validate Ticket Code
if (etTicketCode.getText().toString().trim().isEmpty()) {
etTicketCode.setError("Ticket Code wajib diisi");
isValid = false;
}
// Validate Source
if (spinnerSource.getSelectedItemPosition() == 0) {
Toast.makeText(this, "Pilih Source", Toast.LENGTH_SHORT).show();
isValid = false;
}
// Validate Issue
if (spinnerIssue.getSelectedItemPosition() == 0) {
Toast.makeText(this, "Pilih Issue", Toast.LENGTH_SHORT).show();
isValid = false;
}
// Validate Merchant
if (spinnerMerchant.getSelectedItemPosition() == 0) {
Toast.makeText(this, "Pilih Merchant", Toast.LENGTH_SHORT).show();
isValid = false;
}
// Validate Assign
if (spinnerAssign.getSelectedItemPosition() == 0) {
Toast.makeText(this, "Pilih Assign", Toast.LENGTH_SHORT).show();
isValid = false;
}
// Validate Resolved Date
if (selectedResolvedDate.isEmpty()) {
Toast.makeText(this, "Pilih tanggal resolved", Toast.LENGTH_SHORT).show();
isValid = false;
}
return isValid;
}
private void submitToAPI(String ticketCode, int sourceId, int issueId, int merchantId,
int assignId, String resolvedAt) {
Log.d(TAG, "Starting API submission...");
Log.d(TAG, "Ticket Code: " + ticketCode);
Log.d(TAG, "Source ID: " + sourceId);
Log.d(TAG, "Issue ID: " + issueId);
Log.d(TAG, "Merchant ID: " + merchantId);
Log.d(TAG, "Assign ID: " + assignId);
Log.d(TAG, "Resolved At: " + resolvedAt);
String authToken = LoginActivity.getToken(this);
if (authToken == null || authToken.isEmpty()) {
Log.e(TAG, "Auth token is null or empty");
mainHandler.post(() -> {
btnKirim.setEnabled(true);
btnKirim.setText("Kirim Sekarang");
LoginActivity.logout(this);
});
return;
}
Log.d(TAG, "Auth token obtained: " + authToken.substring(0, Math.min(authToken.length(), 10)) + "...");
executor.execute(() -> {
HttpURLConnection connection = null;
try {
URL url = new URL("https://be-edc.msvc.app/tickets");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Authorization", "Bearer " + authToken);
connection.setDoOutput(true);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
// Create JSON payload
JSONObject payload = new JSONObject();
payload.put("ticket_code", ticketCode);
payload.put("source_id", sourceId);
payload.put("issue_id", issueId);
payload.put("merchant_id", merchantId);
payload.put("status", "new");
payload.put("is_sla_violated", true);
payload.put("assigned_to", assignId);
payload.put("resolved_at", resolvedAt);
String jsonPayload = payload.toString();
Log.d(TAG, "JSON Payload: " + jsonPayload);
// Send request
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes("utf-8");
os.write(input, 0, input.length);
Log.d(TAG, "Request sent successfully");
}
int responseCode = connection.getResponseCode();
Log.d(TAG, "Response Code: " + responseCode);
// Read response
BufferedReader reader;
if (responseCode >= 200 && responseCode < 300) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
String responseBody = response.toString();
Log.d(TAG, "Response Body: " + responseBody);
// Handle response
mainHandler.post(() -> {
btnKirim.setEnabled(true);
btnKirim.setText("Kirim Sekarang");
if (responseCode >= 200 && responseCode < 300) {
// Success
Log.d(TAG, "Request successful");
try {
JSONObject responseJson = new JSONObject(responseBody);
String message = responseJson.optString("message", "Tiket berhasil dibuat");
// Show success screen instead of toast
showSuccessScreen();
} catch (Exception e) {
Log.e(TAG, "Error parsing success response", e);
// Show success screen even if parsing fails
showSuccessScreen();
}
} else if (responseCode == 401) {
Log.e(TAG, "Unauthorized - token expired");
Toast.makeText(this, "Session expired. Please login again.",
Toast.LENGTH_LONG).show();
LoginActivity.logout(this);
} else {
// Error
Log.e(TAG, "Request failed with code: " + responseCode);
Log.e(TAG, "Error response: " + responseBody);
try {
JSONObject errorJson = new JSONObject(responseBody);
String errorMessage = errorJson.optString("message", "Gagal mengirim tiket");
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e(TAG, "Error parsing error response", e);
Toast.makeText(this, "Gagal mengirim tiket. Error: " + responseCode,
Toast.LENGTH_LONG).show();
}
}
});
} catch (Exception e) {
Log.e(TAG, "Network error during submission", e);
mainHandler.post(() -> {
btnKirim.setEnabled(true);
btnKirim.setText("Kirim Sekarang");
Toast.makeText(this, "Network error: " + e.getMessage(),
Toast.LENGTH_LONG).show();
});
} finally {
if (connection != null) {
connection.disconnect();
}
}
});
}
private void clearForm() {
if (etTicketCode != null) etTicketCode.setText("");
if (spinnerSource != null) spinnerSource.setSelection(0);
if (spinnerIssue != null) spinnerIssue.setSelection(0);
if (spinnerMerchant != null) spinnerMerchant.setSelection(0);
if (spinnerAssign != null) spinnerAssign.setSelection(0);
// Reset resolved date
if (tvResolvedDate != null) {
tvResolvedDate.setText("Pilih Tanggal Resolved");
tvResolvedDate.setTextColor(0xFFAAAAAA); // Light gray color
}
selectedResolvedDate = "";
// Update button state after clearing form
updateButtonState();
}
@Override
protected void onResume() {
super.onResume();
if (!LoginActivity.isLoggedIn(this)) {
finish();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,672 @@
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) {
Log.d("ReprintAdapterActivity", "🔍 Mapping payment method - channelCode: " + channelCode + ", channelCategory: " + channelCategory);
// Priority 1: Use channelCode for specific mapping
if (channelCode != null && !channelCode.isEmpty()) {
String code = channelCode.toUpperCase().trim();
switch (code) {
case "QRIS":
return "QRIS";
case "DEBIT":
case "DEBIT_CARD":
return "Kartu Debit";
case "CREDIT":
case "CREDIT_CARD":
return "Kartu Kredit";
case "BCA":
return "BCA";
case "MANDIRI":
return "Mandiri";
case "BNI":
return "BNI";
case "BRI":
return "BRI";
case "PERMATA":
return "Permata";
case "CIMB":
return "CIMB Niaga";
case "DANAMON":
return "Danamon";
case "BSI":
return "BSI";
case "CASH":
return "Tunai";
case "EDC":
return "EDC";
case "RETAIL_OUTLET":
// SPECIAL HANDLING: For RETAIL_OUTLET, determine by context
return determinePaymentMethodFromCategory(channelCategory);
default:
Log.d("ReprintAdapterActivity", "🔍 Unknown channelCode: " + code + ", trying channelCategory");
break;
}
}
// Priority 2: Use channelCategory as fallback
if (channelCategory != null && !channelCategory.isEmpty()) {
return mapChannelCategoryToPaymentMethod(channelCategory);
}
// Final fallback
Log.w("ReprintAdapterActivity", "⚠️ No valid payment method found, using default");
return "Unknown";
}
private String determinePaymentMethodFromCategory(String channelCategory) {
if (channelCategory == null || channelCategory.isEmpty()) {
return "QRIS"; // Default assumption for RETAIL_OUTLET
}
String category = channelCategory.toUpperCase().trim();
Log.d("ReprintAdapterActivity", "🔍 Mapping channelCategory: " + category);
switch (category) {
case "RETAIL_OUTLET":
return "QRIS"; // Most RETAIL_OUTLET transactions are QRIS
case "DEBIT":
case "DEBIT_CARD":
return "Kartu Debit";
case "CREDIT":
case "CREDIT_CARD":
return "Kartu Kredit";
case "E_MONEY":
case "EMONEY":
return "E-Money";
case "BANK_TRANSFER":
return "Transfer Bank";
case "VIRTUAL_ACCOUNT":
return "Virtual Account";
default:
Log.d("ReprintAdapterActivity", "🔍 Unknown channelCategory: " + category + ", defaulting to QRIS");
return "QRIS";
}
}
private String mapChannelCategoryToPaymentMethod(String channelCategory) {
if (channelCategory == null || channelCategory.isEmpty()) {
return "Unknown";
}
String category = channelCategory.toUpperCase().trim();
switch (category) {
case "RETAIL_OUTLET":
return "QRIS";
case "DEBIT":
case "DEBIT_CARD":
return "Kartu Debit";
case "CREDIT":
case "CREDIT_CARD":
return "Kartu Kredit";
case "E_MONEY":
case "EMONEY":
return "E-Money";
case "BANK_TRANSFER":
return "Transfer Bank";
case "VIRTUAL_ACCOUNT":
return "Virtual Account";
case "QRIS":
return "QRIS";
default:
// Capitalize first letter for unknown categories
return capitalizeFirstLetter(channelCategory);
}
}
private String capitalizeFirstLetter(String text) {
if (text == null || text.isEmpty()) {
return text;
}
return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase();
}
@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,556 @@
package com.example.bdkipoc.histori;
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.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.graphics.Color;
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;
import com.example.bdkipoc.R;
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 LinearLayout backNavigation;
// Store full data for detail view
private static List<HistoryItem> fullHistoryData = new ArrayList<>();
private String API_URL;
private String SUMMARY_API_URL;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_history);
initViews();
setupRecyclerView();
buildApiUrl();
fetchApiData();
fetchSummaryData(); // Add this line to fetch summary data
setupClickListeners();
}
private void buildApiUrl() {
// Option 1: Get today's date (current implementation)
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
String todayDate = dateFormat.format(new Date());
// Option 2: Set specific date (uncomment and modify if needed)
// String specificDate = "2025-06-27"; // Format: yyyy-MM-dd
// String todayDate = specificDate;
// Option 3: Set date using Calendar for specific date
// Calendar calendar = Calendar.getInstance();
// calendar.set(2025, Calendar.JUNE, 27); // Year, Month (0-based), Day
// String todayDate = dateFormat.format(calendar.getTime());
// Build API URL with date as both from_date and to_date, and limit=10
API_URL = "https://be-edc.msvc.app/transactions?page=0&limit=10&sortOrder=DESC&from_date="
+ todayDate + "&to_date=" + todayDate + "&location_id=0&merchant_id=0&tid=&mid=&sortColumn=id";
// Build Summary API URL for getting total amount and transaction count
SUMMARY_API_URL = "https://be-edc.msvc.app/transactions/list?from_date="
+ todayDate + "&to_date=" + todayDate + "&location_id=0&merchant_id=0";
}
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);
backNavigation = findViewById(R.id.back_navigation);
historyList = new ArrayList<>();
}
private void setupRecyclerView() {
adapter = new HistoryAdapter(historyList);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
}
private void setupClickListeners() {
backNavigation.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, HistoryListActivity.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 fetchSummaryData() {
new SummaryApiTask().execute(SUMMARY_API_URL);
}
private void processApiData(JSONArray dataArray) {
try {
historyList.clear();
fullHistoryData.clear(); // Clear static data
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 both lists (since we're limiting to 10 in API call)
historyList.add(historyItem);
fullHistoryData.add(historyItem);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
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 processSummaryData(JSONArray dataArray) {
try {
long totalAmount = 0;
int totalTransactions = 0;
for (int i = 0; i < dataArray.length(); i++) {
JSONObject item = dataArray.getJSONObject(i);
String amount = item.getString("amount");
// Parse amount safely
double amountValue = 0;
try {
amountValue = Double.parseDouble(amount);
} catch (NumberFormatException e) {
amountValue = 0;
}
totalAmount += (long) amountValue;
totalTransactions++;
}
final long finalTotalAmount = totalAmount;
final int finalTotalTransactions = totalTransactions;
runOnUiThread(new Runnable() {
@Override
public void run() {
updateSummary(finalTotalAmount, finalTotalTransactions);
}
});
} catch (JSONException e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(HistoryActivity.this, "Error parsing summary data", Toast.LENGTH_SHORT).show();
// Set default values if summary fails
updateSummary(0, 0);
}
});
}
}
private void updateSummary(long totalAmount, int totalTransactions) {
tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
tvTotalTransactions.setText(String.valueOf(totalTransactions));
}
private void loadSampleData() {
historyList.clear();
fullHistoryData.clear();
// Get today's date for sample data
SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
Date now = new Date();
String todayDate = dateFormat.format(now);
String currentTime = timeFormat.format(now);
// Create sample data with today's date - limit to 10 items
HistoryItem[] sampleData = {
new HistoryItem("08:30", todayDate, 1500000, "QRIS", "SUCCESS", "TXN001"),
new HistoryItem("09:15", todayDate, 750000, "Debit", "SUCCESS", "TXN002"),
new HistoryItem("10:20", todayDate, 2250000, "QRIS", "FAILED", "TXN003"),
new HistoryItem("11:45", todayDate, 980000, "Kredit", "SUCCESS", "TXN004"),
new HistoryItem("12:30", todayDate, 1800000, "QRIS", "SUCCESS", "TXN005"),
new HistoryItem("13:15", todayDate, 650000, "Debit", "FAILED", "TXN006"),
new HistoryItem("14:00", todayDate, 3200000, "QRIS", "SUCCESS", "TXN007"),
new HistoryItem("15:30", todayDate, 1100000, "Kredit", "SUCCESS", "TXN008"),
new HistoryItem("16:45", todayDate, 890000, "Debit", "FAILED", "TXN009"),
new HistoryItem("17:20", todayDate, 2100000, "QRIS", "SUCCESS", "TXN010")
};
long totalAmount = 0;
for (HistoryItem item : sampleData) {
historyList.add(item);
fullHistoryData.add(item);
totalAmount += item.getAmount();
}
tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
tvTotalTransactions.setText("10");
adapter.notifyDataSetChanged();
}
private String formatChannelName(String channelCode) {
switch (channelCode) {
case "DEBIT_CARD":
return "Debit";
case "CREDIT_CARD":
return "Kredit";
case "QRIS":
return "QRIS";
case "E_MONEY":
return "E-Money";
case "OTHER":
return "Lainnya";
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 today's date as fallback
SimpleDateFormat fallbackFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
return fallbackFormat.format(new Date());
}
}
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 main API call (transaction list with limit)
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();
}
}
}
// AsyncTask for summary API call (all transactions for totals)
private class SummaryApiTask 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) {
JSONArray dataArray = jsonResponse.getJSONArray("data");
processSummaryData(dataArray);
} else {
Toast.makeText(HistoryActivity.this, "Summary API Error", Toast.LENGTH_SHORT).show();
updateSummary(0, 0);
}
} catch (JSONException e) {
e.printStackTrace();
Toast.makeText(HistoryActivity.this, "Summary JSON Parse Error", Toast.LENGTH_SHORT).show();
updateSummary(0, 0);
}
} else {
Toast.makeText(HistoryActivity.this, "Summary Network Error", Toast.LENGTH_SHORT).show();
updateSummary(0, 0);
}
}
}
}
// 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);
// Format time with dots instead of colons (09.00 instead of 09:00)
String formattedTime = item.getTime().replace(":", ".");
holder.tvTime.setText(formattedTime + ", " + item.getDate());
holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
holder.tvChannel.setText(item.getChannelName());
// Set status color and text
String status = item.getStatus();
if ("SUCCESS".equals(status)) {
holder.tvStatus.setText("Berhasil");
holder.tvStatus.setTextColor(Color.parseColor("#4CAF50")); // Green
} else if ("FAILED".equals(status)) {
holder.tvStatus.setText("Gagal");
holder.tvStatus.setTextColor(Color.parseColor("#F44336")); // Red
} else {
holder.tvStatus.setText("Tertunda");
holder.tvStatus.setTextColor(Color.parseColor("#FF9800")); // 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,337 @@
package com.example.bdkipoc.histori;
import android.content.Intent;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.bdkipoc.R;
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.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class HistoryListActivity extends AppCompatActivity {
private RecyclerView rvHistory;
private TextView tvEmpty;
private HistoryListAdapter adapter;
private List<Transaction> transactionList = new ArrayList<>();
private String API_URL;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_history_list);
// Initialize views
rvHistory = findViewById(R.id.rv_history);
tvEmpty = findViewById(R.id.tv_empty);
// Set up RecyclerView
adapter = new HistoryListAdapter(transactionList);
rvHistory.setLayoutManager(new LinearLayoutManager(this));
rvHistory.setAdapter(adapter);
// Set up app bar
setupAppBar();
// Build API URL and load data
buildApiUrl();
fetchTransactionData();
}
private void buildApiUrl() {
// Get current date in yyyy-MM-dd format
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
String currentDate = dateFormat.format(new Date());
// Option 2: Set specific date (uncomment and modify if needed)
// String specificDate = "2025-06-27"; // Format: yyyy-MM-dd
// String currentDate = specificDate;
// Build API URL with current date as both from_date and to_date
API_URL = "https://be-edc.msvc.app/transactions/list?from_date=" + currentDate +
"&to_date=" + currentDate + "&location_id=0&merchant_id=0";
}
private void setupAppBar() {
LinearLayout backNavigation = findViewById(R.id.back_navigation);
TextView appbarTitle = findViewById(R.id.appbarTitle);
appbarTitle.setText("Kembali");
backNavigation.setOnClickListener(v -> onBackPressed());
}
private void fetchTransactionData() {
new FetchTransactionsTask().execute(API_URL);
}
private void updateTransactionList(List<Transaction> transactions) {
transactionList.clear();
if (transactions != null && !transactions.isEmpty()) {
transactionList.addAll(transactions);
tvEmpty.setVisibility(View.GONE);
rvHistory.setVisibility(View.VISIBLE);
} else {
tvEmpty.setVisibility(View.VISIBLE);
rvHistory.setVisibility(View.GONE);
}
adapter.notifyDataSetChanged();
}
// AsyncTask to fetch transactions from API
private class FetchTransactionsTask 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) {
JSONArray dataArray = jsonResponse.getJSONArray("data");
List<Transaction> transactions = parseTransactions(dataArray);
updateTransactionList(transactions);
} else {
showError("API Error: " + jsonResponse.getString("message"));
}
} catch (JSONException e) {
e.printStackTrace();
showError("Error parsing data");
}
} else {
showError("Network error");
}
}
}
private List<Transaction> parseTransactions(JSONArray dataArray) throws JSONException {
List<Transaction> transactions = new ArrayList<>();
for (int i = 0; i < dataArray.length(); i++) {
JSONObject item = dataArray.getJSONObject(i);
try {
Transaction transaction = new Transaction(
item.getString("transaction_date"),
item.getString("amount"),
item.getString("channel_code"),
item.getString("status")
);
transactions.add(transaction);
} catch (ParseException e) {
e.printStackTrace();
}
}
// Urutkan dari terbaru ke terlama
transactions.sort((t1, t2) -> {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
Date d1 = sdf.parse(t1.getDateTime());
Date d2 = sdf.parse(t2.getDateTime());
return d2.compareTo(d1); // terbaru di atas
} catch (ParseException e) {
e.printStackTrace();
return 0;
}
});
return transactions;
}
private void showError(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
tvEmpty.setVisibility(View.VISIBLE);
rvHistory.setVisibility(View.GONE);
}
// Transaction model class
public static class Transaction {
private final String dateTime;
private final String amount;
private final String channel;
private final String status;
public Transaction(String dateTime, String amount, String channel, String status) throws ParseException {
this.dateTime = dateTime;
this.amount = amount;
this.channel = channel;
this.status = status;
}
public String getDateTime() {
return dateTime;
}
public String getAmount() {
return amount;
}
public String getChannel() {
return channel;
}
public String getStatus() {
return status;
}
}
// Adapter class
public class HistoryListAdapter extends RecyclerView.Adapter<HistoryListAdapter.ViewHolder> {
private final List<Transaction> transactions;
public HistoryListAdapter(List<Transaction> transactions) {
this.transactions = transactions;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_history_list, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Transaction transaction = transactions.get(position);
// Format date
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
SimpleDateFormat outputFormat = new SimpleDateFormat("HH:mm, dd MMM yyyy", Locale.getDefault());
try {
Date date = inputFormat.parse(transaction.getDateTime());
holder.tvTime.setText(outputFormat.format(date));
} catch (ParseException e) {
holder.tvTime.setText(transaction.getDateTime());
e.printStackTrace();
}
// Format amount
holder.tvAmount.setText(String.format(Locale.getDefault(), "Rp. %s", transaction.getAmount()));
// Set channel and status
holder.tvChannel.setText(formatChannelName(transaction.getChannel()));
holder.tvStatus.setText(formatStatusText(transaction.getStatus()));
// Set status color
int statusColor = Color.parseColor("#000000"); // default black
if ("SUCCESS".equalsIgnoreCase(transaction.getStatus())) {
statusColor = Color.parseColor("#4CAF50"); // green
} else if ("FAILED".equalsIgnoreCase(transaction.getStatus())) {
statusColor = Color.parseColor("#F44336"); // red
} else if ("INIT".equalsIgnoreCase(transaction.getStatus())) {
statusColor = Color.parseColor("#FF9800"); // orange
}
holder.tvStatus.setTextColor(statusColor);
}
@Override
public int getItemCount() {
return transactions.size();
}
private String formatChannelName(String channelCode) {
switch (channelCode) {
case "E_MONEY":
return "E-Money";
case "QRIS":
return "QRIS";
case "CREDIT_CARD":
return "Kredit";
case "DEBIT_CARD":
return "Debit";
default:
return channelCode;
}
}
private String formatStatusText(String status) {
switch (status) {
case "SUCCESS":
return "Berhasil";
case "FAILED":
return "Gagal";
case "INIT":
return "Tertunda";
default:
return status;
}
}
public class ViewHolder extends RecyclerView.ViewHolder {
public final TextView tvTime;
public final TextView tvAmount;
public final TextView tvChannel;
public final TextView tvStatus;
public ViewHolder(View view) {
super(view);
tvTime = view.findViewById(R.id.tv_time);
tvAmount = view.findViewById(R.id.tv_amount);
tvChannel = view.findViewById(R.id.tv_channel);
tvStatus = view.findViewById(R.id.tv_status);
// Set click listener if needed
view.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
Transaction transaction = transactions.get(position);
// TODO: Handle item click, maybe open detail activity
// Intent intent = new Intent(HistoryListActivity.this, HistoryDetailActivity.class);
// Pass transaction data to detail activity
// startActivity(intent);
}
});
}
}
}
}

View File

@ -0,0 +1,424 @@
package com.example.bdkipoc.infotoko;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.InputType;
import android.view.View;
import android.view.ViewParent;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import com.example.bdkipoc.LoginActivity;
import com.example.bdkipoc.R;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import android.text.method.PasswordTransformationMethod;
import org.json.JSONException;
import org.json.JSONObject;
public class InfoTokoActivity extends AppCompatActivity {
private static final String TAG = "InfoTokoActivity";
// Views
private TextView tvStoreName;
private TextView tvMerchantId;
private TextView tvTerminalId;
private TextInputEditText etEmail;
private TextInputEditText etPassword;
private TextInputEditText etOwnerName;
private TextInputEditText etNik;
private TextInputEditText etPhone;
private TextInputEditText etBusinessType;
private TextInputEditText etBusinessName;
private TextInputEditText etAddress;
private MaterialButton btnUpdate;
private LinearLayout backNavigation; // Changed from ImageView to LinearLayout
// Data
private String authToken;
private JSONObject userData;
private String userPassword; // Add password storage
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_info_toko);
// Check if user is logged in
if (!LoginActivity.isLoggedIn(this)) {
finish();
return;
}
// Initialize views
initializeViews();
// Load user data
loadUserData();
// Setup listeners
setupListeners();
// Display store information
displayStoreInfo();
}
private void initializeViews() {
// Header
tvStoreName = findViewById(R.id.tv_store_name);
tvMerchantId = findViewById(R.id.tv_merchant_id);
tvTerminalId = findViewById(R.id.tv_terminal_id);
// Find the back navigation from the included layout
backNavigation = findViewById(R.id.back_navigation);
// Optionally, you can also update the title in the appbar
TextView appbarTitle = findViewById(R.id.appbarTitle);
if (appbarTitle != null) {
appbarTitle.setText("Kembali");
}
// Account Information
etEmail = findViewById(R.id.et_email);
etPassword = findViewById(R.id.et_password);
// Store Information
etOwnerName = findViewById(R.id.et_owner_name);
etNik = findViewById(R.id.et_nik);
etPhone = findViewById(R.id.et_phone);
etBusinessType = findViewById(R.id.et_business_type);
etBusinessName = findViewById(R.id.et_business_name);
etAddress = findViewById(R.id.et_address);
// Button
btnUpdate = findViewById(R.id.btn_update);
}
private void loadUserData() {
// Get authentication token
authToken = getIntent().getStringExtra("AUTH_TOKEN");
if (authToken == null) {
authToken = LoginActivity.getToken(this);
}
// Get user data
String userDataString = getIntent().getStringExtra("USER_DATA");
if (userDataString != null) {
try {
userData = new JSONObject(userDataString);
} catch (JSONException e) {
Log.e(TAG, "Error parsing user data: " + e.getMessage());
}
}
if (userData == null) {
userData = LoginActivity.getUserDataAsJson(this);
}
// Get saved password from SharedPreferences
SharedPreferences prefs = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
userPassword = prefs.getString("current_password", ""); // Fix: use correct key
Log.d(TAG, "Loaded auth token: " + (authToken != null ? "" : ""));
Log.d(TAG, "Loaded user data: " + (userData != null ? "" : ""));
Log.d(TAG, "Loaded password: " + (!userPassword.isEmpty() ? "" : ""));
}
private void setupListeners() {
// Back button - now using the LinearLayout
if (backNavigation != null) {
backNavigation.setOnClickListener(v -> {
Log.d(TAG, "Back button clicked");
finish();
});
} else {
Log.e(TAG, "Back navigation not found!");
}
// Update button
if (btnUpdate != null) {
btnUpdate.setOnClickListener(v -> updateStoreInfo());
}
// Password toggle listener
setupPasswordToggle();
}
private void setupPasswordToggle() {
ViewParent passwordParentView = etPassword.getParent().getParent();
if (passwordParentView instanceof TextInputLayout) {
TextInputLayout passwordLayout = (TextInputLayout) passwordParentView;
// Set initial state to visible
etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
passwordLayout.setEndIconOnClickListener(v -> {
// Toggle password visibility
if (etPassword.getTransformationMethod() == null) {
// Hide password
etPassword.setTransformationMethod(PasswordTransformationMethod.getInstance());
passwordLayout.setEndIconDrawable(R.drawable.ic_visibility_off); // Set your eye-off icon
} else {
// Show password
etPassword.setTransformationMethod(null);
passwordLayout.setEndIconDrawable(R.drawable.ic_visibility); // Set your eye icon
}
// Move cursor to end
etPassword.setSelection(etPassword.getText().length());
});
}
}
private void displayStoreInfo() {
// Display store name and IDs (static for header)
tvStoreName.setText("TOKO KLONTONG PAK EKO");
tvMerchantId.setText("MID: 12345678901");
tvTerminalId.setText("TID: 12345678901");
// Hide fields that are not needed based on requirements
hideUnnecessaryFields();
// Display data from login response
if (userData != null) {
try {
// Email - from API response
String email = userData.optString("email", "");
if (!email.isEmpty()) {
etEmail.setText(email);
} else {
etEmail.setText("Email tidak tersedia");
}
// Password - show actual password from SharedPreferences (VISIBLE by default)
if (!userPassword.isEmpty()) {
etPassword.setText(userPassword);
// Start with password visible
etPassword.setTransformationMethod(null);
// Refresh the eye icon state
ViewParent passwordParentView = etPassword.getParent().getParent();
if (passwordParentView instanceof TextInputLayout) {
((TextInputLayout) passwordParentView).setEndIconDrawable(R.drawable.ic_visibility);
}
} else {
etPassword.setText("");
}
etPassword.setEnabled(true); // Enable for display with toggle
// Update the eye icon to show "hide" state initially
ViewParent passwordParentView = etPassword.getParent().getParent();
if (passwordParentView instanceof TextInputLayout) {
TextInputLayout passwordLayout = (TextInputLayout) passwordParentView;
passwordLayout.setPasswordVisibilityToggleEnabled(true);
// Force refresh the toggle icon
passwordLayout.refreshDrawableState();
}
// Debug log
Log.d(TAG, "Password field text: " + etPassword.getText().toString());
Log.d(TAG, "Password field length: " + etPassword.getText().length());
// Nama Pemilik - from API response
String ownerName = userData.optString("name", "");
if (!ownerName.isEmpty()) {
etOwnerName.setText(ownerName);
} else {
etOwnerName.setText("Nama tidak tersedia");
}
// Nomor Telepon - from API response
String phone = userData.optString("phone", "");
if (!phone.isEmpty()) {
etPhone.setText(phone);
} else {
etPhone.setText("Nomor telepon tidak tersedia");
}
// Log the user data for debugging
Log.d(TAG, "User Data: " + userData.toString());
} catch (Exception e) {
Log.e(TAG, "Error displaying user info: " + e.getMessage());
Toast.makeText(this, "Error loading user data", Toast.LENGTH_SHORT).show();
}
} else {
// Show default/empty values if no user data
etEmail.setText("Email tidak tersedia");
etPassword.setText("••••••••");
etPassword.setEnabled(false);
etOwnerName.setText("Nama tidak tersedia");
etPhone.setText("Nomor telepon tidak tersedia");
}
}
private void hideUnnecessaryFields() {
// Hide NIK field and its container
ViewParent nikContainer = etNik.getParent();
if (nikContainer != null && nikContainer instanceof View) {
((View) nikContainer).setVisibility(View.GONE);
}
// Hide Business Type field and its container
ViewParent businessTypeContainer = etBusinessType.getParent();
if (businessTypeContainer != null && businessTypeContainer instanceof View) {
((View) businessTypeContainer).setVisibility(View.GONE);
}
// Hide Business Name field and its container
ViewParent businessNameContainer = etBusinessName.getParent();
if (businessNameContainer != null && businessNameContainer instanceof View) {
((View) businessNameContainer).setVisibility(View.GONE);
}
// Hide Address field and its container
ViewParent addressContainer = etAddress.getParent();
if (addressContainer != null && addressContainer instanceof View) {
((View) addressContainer).setVisibility(View.GONE);
}
// Update the section title to be more accurate
// Note: You'll need to add an ID to the section title TextView in the XML
}
private void updateStoreInfo() {
// Validate inputs
if (!validateInputs()) {
return;
}
// Show loading
btnUpdate.setEnabled(false);
btnUpdate.setText("Memperbarui...");
// Simulate update process (in real app, this would call API)
btnUpdate.postDelayed(() -> {
// In a real implementation, you would:
// 1. Call API to update user info
// 2. Update SharedPreferences with new data
// 3. Show success/error message
// Show success message
Toast.makeText(this, "Informasi akun berhasil diperbarui", Toast.LENGTH_SHORT).show();
// If password was changed, inform user
String currentPasswordText = etPassword.getText().toString();
if (!currentPasswordText.isEmpty() && !currentPasswordText.equals(userPassword)) {
Toast.makeText(this, "Password berhasil diperbarui", Toast.LENGTH_SHORT).show();
userPassword = currentPasswordText; // Update local variable
}
// Reset button state
btnUpdate.setEnabled(true);
btnUpdate.setText("Perbarui Informasi Toko");
// Optional: Save updated data locally
saveUpdatedData();
}, 2000); // Simulate 2 second delay
}
private boolean validateInputs() {
// Check email
String email = etEmail.getText().toString().trim();
if (email.isEmpty()) {
etEmail.setError("Email tidak boleh kosong");
etEmail.requestFocus();
return false;
}
if (!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
etEmail.setError("Format email tidak valid");
etEmail.requestFocus();
return false;
}
// Check owner name
String ownerName = etOwnerName.getText().toString().trim();
if (ownerName.isEmpty()) {
etOwnerName.setError("Nama pemilik tidak boleh kosong");
etOwnerName.requestFocus();
return false;
}
// Check phone
String phone = etPhone.getText().toString().trim();
if (phone.isEmpty()) {
etPhone.setError("Nomor telepon tidak boleh kosong");
etPhone.requestFocus();
return false;
}
if (phone.length() < 10) {
etPhone.setError("Nomor telepon tidak valid");
etPhone.requestFocus();
return false;
}
// Check password if changed
String password = etPassword.getText().toString();
if (!password.isEmpty() && password.length() < 6) {
etPassword.setError("Password minimal 6 karakter");
etPassword.requestFocus();
return false;
}
return true;
}
private void saveUpdatedData() {
// In a real app, this would update the user data in SharedPreferences
// and call an API to update the server
try {
JSONObject updatedData = new JSONObject();
updatedData.put("email", etEmail.getText().toString().trim());
updatedData.put("name", etOwnerName.getText().toString().trim());
updatedData.put("phone", etPhone.getText().toString().trim());
// Merge with existing userData
if (userData != null) {
// Keep other fields from original userData
updatedData.put("id", userData.optString("id", ""));
updatedData.put("role", userData.optString("role", ""));
// Add other fields as needed
}
// Save updated password to SharedPreferences
String newPassword = etPassword.getText().toString();
if (!newPassword.isEmpty() && !newPassword.equals(userPassword)) {
SharedPreferences prefs = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
prefs.edit().putString("current_password", newPassword).apply(); // Fix: use correct key
Log.d(TAG, "Password updated in SharedPreferences");
}
Log.d(TAG, "Updated data: " + updatedData.toString());
// In real app, you would:
// 1. Call API to update user data
// 2. On success, update SharedPreferences:
// SharedPreferences prefs = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
// prefs.edit().putString("user_data", updatedData.toString()).apply();
} catch (JSONException e) {
Log.e(TAG, "Error creating updated data: " + e.getMessage());
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
finish();
}
}

View File

@ -0,0 +1,903 @@
package com.example.bdkipoc;
import com.example.bdkipoc.qris.view.QrisResultActivity;
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_SANDBOX_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=";
private static final String MIDTRANS_PRODUCTION_AUTH = "TWlkLXNlcnZlci1sMlZPalotdVlVanpvNnU4VzAtYmF1a2o="; // Base64 of "Mid-server-l2VOjZ-uYUjzo6u8W0-baukj:"
// Currently active server key (switch by commenting/uncommenting)
// private static final String MIDTRANS_AUTH = MIDTRANS_PRODUCTION_AUTH;
private static final String MIDTRANS_AUTH = MIDTRANS_SANDBOX_AUTH; // Default to sandbox
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
// Midtrans charge URL
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
// private static final String MIDTRANS_CHARGE_URL = "https://api.midtrans.com/v2/charge";
@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-") || // Sandbox format
serverKey.startsWith("Mid-server-")) && // Production format
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();
}
}

View File

@ -0,0 +1,187 @@
package com.example.bdkipoc.qris.model;
import android.util.Log;
import com.example.bdkipoc.qris.network.QrisApiService;
import org.json.JSONObject;
/**
* Repository class untuk menghandle semua data access terkait QRIS
* Mengabstraksi sumber data (API, local storage, etc.)
*/
public class QrisRepository {
private static final String TAG = "QrisRepository";
private QrisApiService apiService;
// Singleton pattern
private static QrisRepository instance;
private QrisRepository() {
this.apiService = new QrisApiService();
}
public static QrisRepository getInstance() {
if (instance == null) {
instance = new QrisRepository();
}
return instance;
}
/**
* Interface untuk callback hasil operasi
*/
public interface RepositoryCallback<T> {
void onSuccess(T result);
void onError(String errorMessage);
}
/**
* Refresh QR Code
*/
public void refreshQrCode(QrisTransaction transaction, RepositoryCallback<QrRefreshResult> callback) {
Log.d(TAG, "🔄 Refreshing QR code for transaction: " + transaction.getOrderId());
new Thread(() -> {
try {
QrRefreshResult result = apiService.generateNewQrCode(transaction);
if (result != null && result.qrUrl != null && !result.qrUrl.isEmpty()) {
Log.d(TAG, "✅ QR refresh successful");
callback.onSuccess(result);
} else {
Log.e(TAG, "❌ QR refresh failed - empty result");
callback.onError("Failed to generate new QR code");
}
} catch (Exception e) {
Log.e(TAG, "❌ QR refresh exception: " + e.getMessage(), e);
callback.onError("QR refresh error: " + e.getMessage());
}
}).start();
}
/**
* Check payment status
*/
public void checkPaymentStatus(QrisTransaction transaction, RepositoryCallback<PaymentStatusResult> callback) {
Log.d(TAG, "🔍 Checking payment status for: " + transaction.getCurrentQrTransactionId());
new Thread(() -> {
try {
// Gunakan current transaction ID, bukan original
PaymentStatusResult result = apiService.checkTransactionStatus(transaction);
if (result != null) {
Log.d(TAG, "✅ Payment status check successful: " + result.status);
// Update transaction ID jika berbeda
if (result.transactionId != null &&
!result.transactionId.equals(transaction.getCurrentQrTransactionId())) {
transaction.setCurrentQrTransactionId(result.transactionId);
}
callback.onSuccess(result);
} else {
Log.w(TAG, "⚠️ Payment status check returned null");
callback.onError("Failed to check payment status");
}
} catch (Exception e) {
Log.e(TAG, "❌ Payment status check exception: " + e.getMessage(), e);
callback.onError("Payment status error: " + e.getMessage());
}
}).start();
}
/**
* Send webhook simulation
*/
public void simulatePayment(QrisTransaction transaction, RepositoryCallback<Boolean> callback) {
Log.d(TAG, "🚀 Simulating payment for: " + transaction.getOrderId());
new Thread(() -> {
try {
boolean success = apiService.simulateWebhook(transaction);
if (success) {
Log.d(TAG, "✅ Payment simulation successful");
callback.onSuccess(true);
} else {
Log.e(TAG, "❌ Payment simulation failed");
callback.onError("Payment simulation failed");
}
} catch (Exception e) {
Log.e(TAG, "❌ Payment simulation exception: " + e.getMessage(), e);
callback.onError("Payment simulation error: " + e.getMessage());
}
}).start();
}
/**
* Poll for payment logs
*/
public void pollPaymentLogs(String orderId, RepositoryCallback<PaymentLogResult> callback) {
Log.d(TAG, "📊 Polling payment logs for: " + orderId);
new Thread(() -> {
try {
PaymentLogResult result = apiService.pollPendingPaymentLog(orderId);
if (result != null) {
Log.d(TAG, "✅ Payment log polling successful");
callback.onSuccess(result);
} else {
Log.w(TAG, "⚠️ No payment logs found");
callback.onError("No payment logs found");
}
} catch (Exception e) {
Log.e(TAG, "❌ Payment log polling exception: " + e.getMessage(), e);
callback.onError("Payment log polling error: " + e.getMessage());
}
}).start();
}
/**
* Result classes
*/
public static class QrRefreshResult {
public String qrUrl;
public String qrString;
public String transactionId;
public QrRefreshResult(String qrUrl, String qrString, String transactionId) {
this.qrUrl = qrUrl;
this.qrString = qrString;
this.transactionId = transactionId;
}
}
public static class PaymentStatusResult {
public String status;
public String paymentType;
public String issuer;
public String acquirer;
public String qrString;
public boolean statusChanged;
public String transactionId;
public PaymentStatusResult(String status) {
this.status = status;
this.statusChanged = false;
}
}
public static class PaymentLogResult {
public boolean found;
public String status;
public String orderId;
public PaymentLogResult(boolean found, String status, String orderId) {
this.found = found;
this.status = status;
this.orderId = orderId;
}
}
}

View File

@ -0,0 +1,3 @@
public class QrisResponse {
}

View File

@ -0,0 +1,288 @@
package com.example.bdkipoc.qris.model;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
/**
* Model class untuk data transaksi QRIS
* Menampung semua data yang dibutuhkan untuk transaksi
*/
public class QrisTransaction {
private static final String TAG = "QrisTransaction";
// Transaction identifiers
private String orderId;
private String transactionId;
private String referenceId;
private String merchantId;
// Amount information
private int originalAmount;
private String grossAmount;
private String formattedAmount;
// QR Code information
private String qrImageUrl;
private String qrString;
private long qrCreationTime;
private int qrExpirationMinutes;
// Provider information
private String acquirer;
private String detectedProvider;
private String actualIssuer;
private String actualAcquirer;
// Transaction timing
private String transactionTime;
private long creationTimestamp;
// Status tracking
private String currentStatus;
private boolean paymentProcessed;
private boolean isQrRefreshTransaction;
private String currentQrTransactionId;
// Provider expiration mapping
private static final Map<String, Integer> PROVIDER_EXPIRATION_MAP = new HashMap<String, Integer>() {{
put("shopeepay", 5);
put("shopee", 5);
put("airpay shopee", 5);
put("gopay", 15);
put("dana", 15);
put("ovo", 15);
put("linkaja", 15);
put("link aja", 15);
put("jenius", 15);
put("qris", 15);
put("others", 15);
}};
// Provider display name mapping
private static final Map<String, String> ISSUER_DISPLAY_MAP = new HashMap<String, String>() {{
put("airpay shopee", "ShopeePay");
put("shopeepay", "ShopeePay");
put("shopee", "ShopeePay");
put("linkaja", "LinkAja");
put("link aja", "LinkAja");
put("dana", "DANA");
put("ovo", "OVO");
put("gopay", "GoPay");
put("jenius", "Jenius");
put("sakuku", "Sakuku");
put("bni", "BNI");
put("bca", "BCA");
put("mandiri", "Mandiri");
put("bri", "BRI");
put("cimb", "CIMB Niaga");
put("permata", "Permata");
put("maybank", "Maybank");
put("qris", "QRIS");
}};
// Constructor
public QrisTransaction() {
this.creationTimestamp = System.currentTimeMillis();
this.currentStatus = "pending";
this.paymentProcessed = false;
this.isQrRefreshTransaction = false;
}
// Initialization method
public void initialize(String orderId, String transactionId, int amount,
String qrImageUrl, String qrString, String acquirer) {
this.orderId = orderId;
this.transactionId = transactionId;
this.currentQrTransactionId = transactionId;
this.originalAmount = amount;
this.qrImageUrl = qrImageUrl;
this.qrString = qrString;
this.acquirer = acquirer;
// Detect provider and set expiration
this.detectedProvider = detectProviderFromData();
this.qrExpirationMinutes = PROVIDER_EXPIRATION_MAP.get(detectedProvider.toLowerCase());
this.qrCreationTime = System.currentTimeMillis();
// Format amount
this.formattedAmount = formatRupiahAmount(String.valueOf(amount));
}
/**
* Detect provider dari acquirer atau QR string
*/
private String detectProviderFromData() {
// Try to detect from acquirer first
if (acquirer != null && !acquirer.isEmpty()) {
String lowerAcquirer = acquirer.toLowerCase().trim();
if (PROVIDER_EXPIRATION_MAP.containsKey(lowerAcquirer)) {
return lowerAcquirer;
}
}
// Try to detect from QR string content
if (qrString != null && !qrString.isEmpty()) {
String lowerQrString = qrString.toLowerCase();
for (String provider : PROVIDER_EXPIRATION_MAP.keySet()) {
if (lowerQrString.contains(provider.toLowerCase())) {
return provider;
}
}
}
return "others";
}
/**
* Format amount ke format Rupiah
*/
private String formatRupiahAmount(String amount) {
try {
String cleanAmount = amount.replaceAll("[^0-9]", "");
long amountLong = Long.parseLong(cleanAmount);
return "RP." + String.format("%,d", amountLong).replace(',', '.');
} catch (NumberFormatException e) {
return "RP." + amount;
}
}
/**
* Check apakah QR sudah expired
*/
public boolean isQrExpired() {
long currentTime = System.currentTimeMillis();
long elapsedMinutes = (currentTime - qrCreationTime) / (1000 * 60);
boolean expired = elapsedMinutes >= qrExpirationMinutes;
Log.d(TAG, "QR expired check: " + elapsedMinutes + "/" + qrExpirationMinutes + " = " + expired);
return expired;
}
/**
* Get remaining time dalam detik
*/
public int getRemainingTimeInSeconds() {
long currentTime = System.currentTimeMillis();
long elapsedMs = currentTime - qrCreationTime;
long totalExpirationMs = qrExpirationMinutes * 60 * 1000;
long remainingMs = totalExpirationMs - elapsedMs;
return (int) Math.max(0, remainingMs / 1000);
}
/**
* Update QR code dengan data baru
*/
public void updateQrCode(String newQrUrl, String newQrString, String newTransactionId) {
this.qrImageUrl = newQrUrl;
this.qrString = newQrString;
this.qrCreationTime = System.currentTimeMillis();
if (newTransactionId != null && !newTransactionId.isEmpty()) {
this.currentQrTransactionId = newTransactionId;
this.isQrRefreshTransaction = true;
}
}
/**
* Get display name untuk provider
*/
public String getDisplayProviderName() {
String issuerToCheck = actualIssuer != null && !actualIssuer.isEmpty()
? actualIssuer : acquirer;
if (issuerToCheck == null || issuerToCheck.isEmpty()) {
return "QRIS";
}
String lowerName = issuerToCheck.toLowerCase().trim();
String displayName = ISSUER_DISPLAY_MAP.get(lowerName);
if (displayName != null) {
return displayName;
}
// Fallback: capitalize first letter
String[] words = issuerToCheck.split("\\s+");
StringBuilder result = new StringBuilder();
for (String word : words) {
if (word.length() > 0) {
result.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1).toLowerCase())
.append(" ");
}
}
return result.toString().trim();
}
// Getters and Setters
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getCurrentQrTransactionId() { return currentQrTransactionId; }
public void setCurrentQrTransactionId(String currentQrTransactionId) {
this.currentQrTransactionId = currentQrTransactionId;
}
public String getReferenceId() { return referenceId; }
public void setReferenceId(String referenceId) { this.referenceId = referenceId; }
public String getMerchantId() { return merchantId; }
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
public int getOriginalAmount() { return originalAmount; }
public void setOriginalAmount(int originalAmount) { this.originalAmount = originalAmount; }
public String getGrossAmount() { return grossAmount; }
public void setGrossAmount(String grossAmount) { this.grossAmount = grossAmount; }
public String getFormattedAmount() { return formattedAmount; }
public void setFormattedAmount(String formattedAmount) { this.formattedAmount = formattedAmount; }
public String getQrImageUrl() { return qrImageUrl; }
public void setQrImageUrl(String qrImageUrl) { this.qrImageUrl = qrImageUrl; }
public String getQrString() { return qrString; }
public void setQrString(String qrString) { this.qrString = qrString; }
public long getQrCreationTime() { return qrCreationTime; }
public void setQrCreationTime(long qrCreationTime) { this.qrCreationTime = qrCreationTime; }
public int getQrExpirationMinutes() { return qrExpirationMinutes; }
public void setQrExpirationMinutes(int qrExpirationMinutes) {
this.qrExpirationMinutes = qrExpirationMinutes;
}
public String getAcquirer() { return acquirer; }
public void setAcquirer(String acquirer) { this.acquirer = acquirer; }
public String getDetectedProvider() { return detectedProvider; }
public void setDetectedProvider(String detectedProvider) { this.detectedProvider = detectedProvider; }
public String getActualIssuer() { return actualIssuer; }
public void setActualIssuer(String actualIssuer) { this.actualIssuer = actualIssuer; }
public String getActualAcquirer() { return actualAcquirer; }
public void setActualAcquirer(String actualAcquirer) { this.actualAcquirer = actualAcquirer; }
public String getTransactionTime() { return transactionTime; }
public void setTransactionTime(String transactionTime) { this.transactionTime = transactionTime; }
public long getCreationTimestamp() { return creationTimestamp; }
public void setCreationTimestamp(long creationTimestamp) { this.creationTimestamp = creationTimestamp; }
public String getCurrentStatus() { return currentStatus; }
public void setCurrentStatus(String currentStatus) { this.currentStatus = currentStatus; }
public boolean isPaymentProcessed() { return paymentProcessed; }
public void setPaymentProcessed(boolean paymentProcessed) { this.paymentProcessed = paymentProcessed; }
public boolean isQrRefreshTransaction() { return isQrRefreshTransaction; }
public void setQrRefreshTransaction(boolean qrRefreshTransaction) {
this.isQrRefreshTransaction = qrRefreshTransaction;
}
}

View File

@ -0,0 +1,3 @@
public class MidtransApiClient {
}

View File

@ -0,0 +1,410 @@
package com.example.bdkipoc.qris.network;
import android.util.Log;
import com.example.bdkipoc.qris.model.QrisRepository;
import com.example.bdkipoc.qris.model.QrisTransaction;
import com.example.bdkipoc.qris.model.QrisRepository.PaymentStatusResult;
import org.json.JSONArray;
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;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* API Service untuk handling semua network calls terkait QRIS
* Mengabstraksi implementasi detail dari repository
*/
public class QrisApiService {
private static final String TAG = "QrisApiService";
// API Endpoints
private static final String MIDTRANS_SANDBOX_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=";
private static final String MIDTRANS_PRODUCTION_AUTH = "TWlkLXNlcnZlci1sMlZPalotdVlVanpvNnU4VzAtYmF1a2o=";
private static final String MIDTRANS_AUTH = MIDTRANS_SANDBOX_AUTH; // Default to sandbox
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
private static final String MIDTRANS_STATUS_BASE_URL = "https://api.sandbox.midtrans.com/v2/";
private String backendBase = "https://be-edc.msvc.app";
private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans";
/**
* Generate new QR code via Midtrans API
*/
public QrisRepository.QrRefreshResult generateNewQrCode(QrisTransaction transaction) throws Exception {
Log.d(TAG, "🔧 Generating new QR code for: " + transaction.getOrderId());
// Generate unique order ID untuk QR refresh
String shortTimestamp = String.valueOf(System.currentTimeMillis()).substring(7);
String newOrderId = transaction.getOrderId().substring(0, Math.min(transaction.getOrderId().length(), 43)) + "-q" + shortTimestamp;
// Validate order ID length
if (newOrderId.length() > 50) {
newOrderId = transaction.getOrderId().substring(0, 36) + "-q" + shortTimestamp.substring(0, Math.min(shortTimestamp.length(), 7));
}
Log.d(TAG, "🆕 New QR Order ID: " + newOrderId);
// Create enhanced payload
JSONObject payload = createQrRefreshPayload(transaction, newOrderId);
// Make API call
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", webhookUrl);
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0 QR-Refresh-Enhanced");
conn.setRequestProperty("X-QR-Refresh", "true");
conn.setRequestProperty("X-Parent-Transaction", transaction.getTransactionId());
conn.setRequestProperty("X-Provider", transaction.getDetectedProvider());
conn.setRequestProperty("X-Expiration-Minutes", String.valueOf(transaction.getQrExpirationMinutes()));
conn.setDoOutput(true);
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = payload.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
Log.d(TAG, "📥 QR refresh response code: " + responseCode);
if (responseCode == 200 || responseCode == 201) {
String response = readResponse(conn.getInputStream());
return parseQrRefreshResponse(response);
} else {
String errorResponse = readResponse(conn.getErrorStream());
throw new Exception("QR refresh failed: HTTP " + responseCode + " - " + errorResponse);
}
}
/**
* Check transaction status via Midtrans API
*/
public PaymentStatusResult checkTransactionStatus(QrisTransaction transaction) throws Exception {
String monitoringTransactionId = transaction.getCurrentQrTransactionId();
String statusUrl = MIDTRANS_STATUS_BASE_URL + monitoringTransactionId + "/status";
Log.d(TAG, "🔍 Checking status for: " + monitoringTransactionId);
URL url = new URI(statusUrl).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
conn.setConnectTimeout(8000);
conn.setReadTimeout(8000);
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
String response = readResponse(conn.getInputStream());
PaymentStatusResult result = parseStatusResponse(response, transaction);
JSONObject statusResponse = new JSONObject(response);
result.transactionId = statusResponse.optString("transaction_id", monitoringTransactionId);
return result;
} else {
throw new Exception("Status check failed: HTTP " + responseCode);
}
}
/**
* Simulate webhook payment
*/
public boolean simulateWebhook(QrisTransaction transaction) throws Exception {
Log.d(TAG, "🚀 Simulating webhook for: " + transaction.getOrderId());
String serverKey = getServerKey();
String signatureKey = generateSignature(
transaction.getOrderId(),
"200",
transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount()),
serverKey
);
JSONObject payload = createWebhookPayload(transaction, signatureKey);
URL url = new URL(webhookUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0-Enhanced-Simulation");
conn.setRequestProperty("X-Simulation", "true");
conn.setRequestProperty("X-Provider", transaction.getDetectedProvider());
conn.setDoOutput(true);
conn.setConnectTimeout(15000);
conn.setReadTimeout(15000);
try (OutputStream os = conn.getOutputStream()) {
os.write(payload.toString().getBytes("utf-8"));
}
int responseCode = conn.getResponseCode();
Log.d(TAG, "📥 Webhook simulation response: " + responseCode);
if (responseCode == 200 || responseCode == 201) {
String response = readResponse(conn.getInputStream());
Log.d(TAG, "✅ Webhook simulation successful");
return true;
} else {
String errorResponse = readResponse(conn.getErrorStream());
throw new Exception("Webhook simulation failed: HTTP " + responseCode + " - " + errorResponse);
}
}
/**
* Poll for pending payment logs
*/
public QrisRepository.PaymentLogResult pollPendingPaymentLog(String orderId) throws Exception {
Log.d(TAG, "📊 Polling payment logs for: " + orderId);
String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}";
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0-Enhanced");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
String response = readResponse(conn.getInputStream());
return parsePaymentLogResponse(response, orderId);
} else {
throw new Exception("Payment log polling failed: HTTP " + responseCode);
}
}
/**
* Helper methods
*/
private JSONObject createQrRefreshPayload(QrisTransaction transaction, String newOrderId) throws Exception {
JSONObject customField1 = new JSONObject();
customField1.put("parent_transaction_id", transaction.getTransactionId());
customField1.put("parent_order_id", transaction.getOrderId());
customField1.put("parent_reference_id", transaction.getReferenceId());
customField1.put("qr_refresh_time", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()).format(new Date()));
customField1.put("qr_refresh_count", System.currentTimeMillis());
customField1.put("is_qr_refresh", true);
customField1.put("detected_provider", transaction.getDetectedProvider());
customField1.put("expiration_minutes", transaction.getQrExpirationMinutes());
JSONObject payload = new JSONObject();
payload.put("payment_type", "qris");
JSONObject transactionDetails = new JSONObject();
transactionDetails.put("order_id", newOrderId);
transactionDetails.put("gross_amount", transaction.getOriginalAmount());
payload.put("transaction_details", transactionDetails);
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);
JSONArray itemDetails = new JSONArray();
JSONObject item = new JSONObject();
item.put("id", "item1_qr_refresh_" + System.currentTimeMillis());
item.put("price", transaction.getOriginalAmount());
item.put("quantity", 1);
item.put("name", "QRIS Payment QR Refresh - " + new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date()) +
" (" + transaction.getDetectedProvider().toUpperCase() + " - " + transaction.getQrExpirationMinutes() + "min)");
itemDetails.put(item);
payload.put("item_details", itemDetails);
payload.put("custom_field1", customField1.toString());
JSONObject qrisDetails = new JSONObject();
qrisDetails.put("acquirer", "gopay");
qrisDetails.put("qr_refresh", true);
qrisDetails.put("parent_transaction_id", transaction.getTransactionId());
qrisDetails.put("refresh_timestamp", System.currentTimeMillis());
qrisDetails.put("provider", transaction.getDetectedProvider());
qrisDetails.put("expiration_minutes", transaction.getQrExpirationMinutes());
payload.put("qris", qrisDetails);
return payload;
}
private JSONObject createWebhookPayload(QrisTransaction transaction, String signatureKey) throws Exception {
JSONObject payload = new JSONObject();
payload.put("transaction_type", "on-us");
payload.put("transaction_time", transaction.getTransactionTime() != null ? transaction.getTransactionTime() : getCurrentISOTime());
payload.put("transaction_status", "settlement");
payload.put("transaction_id", transaction.getCurrentQrTransactionId());
payload.put("status_message", "midtrans payment notification");
payload.put("status_code", "200");
payload.put("signature_key", signatureKey);
payload.put("settlement_time", getCurrentISOTime());
payload.put("payment_type", "qris");
payload.put("order_id", transaction.getOrderId());
payload.put("merchant_id", transaction.getMerchantId() != null ? transaction.getMerchantId() : "G616299250");
payload.put("issuer", transaction.getActualIssuer() != null ? transaction.getActualIssuer() : transaction.getAcquirer());
payload.put("gross_amount", transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount()));
payload.put("fraud_status", "accept");
payload.put("currency", "IDR");
payload.put("acquirer", transaction.getActualAcquirer() != null ? transaction.getActualAcquirer() : transaction.getAcquirer());
payload.put("shopeepay_reference_number", "");
payload.put("reference_id", transaction.getReferenceId() != null ? transaction.getReferenceId() : "DUMMY_REFERENCE_ID");
// Enhanced fields
payload.put("detected_provider", transaction.getDetectedProvider());
payload.put("qr_expiration_minutes", transaction.getQrExpirationMinutes());
payload.put("is_simulation", true);
payload.put("simulation_type", "enhanced_manual");
if (transaction.getQrString() != null && !transaction.getQrString().isEmpty()) {
payload.put("qr_string", transaction.getQrString());
}
return payload;
}
private QrisRepository.QrRefreshResult parseQrRefreshResponse(String response) throws Exception {
JSONObject jsonResponse = new JSONObject(response);
if (jsonResponse.has("status_code")) {
String statusCode = jsonResponse.getString("status_code");
if (!statusCode.equals("201")) {
String statusMessage = jsonResponse.optString("status_message", "Unknown error");
throw new Exception("QR refresh failed: " + statusCode + " - " + statusMessage);
}
}
String newQrUrl = null;
String newQrString = null;
String newTransactionId = jsonResponse.optString("transaction_id", "");
// Get QR URL from actions
if (jsonResponse.has("actions")) {
JSONArray actionsArray = jsonResponse.getJSONArray("actions");
if (actionsArray.length() > 0) {
JSONObject actions = actionsArray.getJSONObject(0);
newQrUrl = actions.getString("url");
}
}
// Get QR String
if (jsonResponse.has("qr_string")) {
newQrString = jsonResponse.getString("qr_string");
}
return new QrisRepository.QrRefreshResult(newQrUrl, newQrString, newTransactionId);
}
private QrisRepository.PaymentStatusResult parseStatusResponse(String response, QrisTransaction transaction) throws Exception {
JSONObject statusResponse = new JSONObject(response);
String transactionStatus = statusResponse.optString("transaction_status", "");
String paymentType = statusResponse.optString("payment_type", "");
String actualIssuer = statusResponse.optString("issuer", "");
String actualAcquirer = statusResponse.optString("acquirer", "");
String qrStringFromStatus = statusResponse.optString("qr_string", "");
QrisRepository.PaymentStatusResult result = new QrisRepository.PaymentStatusResult(transactionStatus);
result.paymentType = paymentType;
result.issuer = actualIssuer;
result.acquirer = actualAcquirer;
result.qrString = qrStringFromStatus;
result.statusChanged = !transactionStatus.equals(transaction.getCurrentStatus());
return result;
}
private QrisRepository.PaymentLogResult parsePaymentLogResponse(String response, String orderId) throws Exception {
JSONObject json = new JSONObject(response);
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) {
String transactionStatus = reqBody.optString("transaction_status");
String logOrderId = reqBody.optString("order_id");
if (orderId.equals(logOrderId) &&
(transactionStatus.equals("pending") ||
transactionStatus.equals("settlement") ||
transactionStatus.equals("capture") ||
transactionStatus.equals("success"))) {
return new QrisRepository.PaymentLogResult(true, transactionStatus, logOrderId);
}
}
}
}
return new QrisRepository.PaymentLogResult(false, "", orderId);
}
private String readResponse(InputStream inputStream) throws Exception {
if (inputStream == null) return "";
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
return response.toString();
}
private String getServerKey() {
try {
String base64 = MIDTRANS_AUTH.replace("Basic ", "");
byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
String decodedString = new String(decoded);
return decodedString.replace(":", "");
} catch (Exception e) {
Log.e(TAG, "Error decoding server key: " + e.getMessage());
return "";
}
}
private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
String input = orderId + statusCode + grossAmount + serverKey;
try {
MessageDigest md = 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 (Exception e) {
Log.e(TAG, "Error generating signature: " + e.getMessage());
return "dummy_signature";
}
}
private String getCurrentISOTime() {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
.format(new Date());
}
}

View File

@ -0,0 +1,543 @@
package com.example.bdkipoc.qris.presenter;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.example.bdkipoc.qris.model.QrisRepository;
import com.example.bdkipoc.qris.model.QrisTransaction;
import com.example.bdkipoc.qris.view.QrisResultContract;
/**
* Presenter untuk QrisResult module
* Menghandle semua business logic dan koordinasi antara Model dan View
*/
public class QrisResultPresenter implements QrisResultContract.Presenter {
private static final String TAG = "QrisResultPresenter";
private QrisResultContract.View view;
private QrisRepository repository;
private QrisTransaction transaction;
// Handlers untuk background tasks
private Handler timerHandler;
private Handler qrRefreshHandler;
private Handler paymentMonitorHandler;
// Runnables untuk periodic tasks
private Runnable timerRunnable;
private Runnable qrRefreshRunnable;
private Runnable paymentMonitorRunnable;
// State management
private boolean isTimerActive = false;
private boolean isQrRefreshActive = false;
private boolean isPaymentMonitorActive = false;
private String lastKnownStatus = "pending";
private int refreshCounter = 0;
private static final int MAX_REFRESH_ATTEMPTS = 5;
public QrisResultPresenter() {
this.repository = QrisRepository.getInstance();
this.transaction = new QrisTransaction();
// Initialize handlers
this.timerHandler = new Handler(Looper.getMainLooper());
this.qrRefreshHandler = new Handler(Looper.getMainLooper());
this.paymentMonitorHandler = new Handler(Looper.getMainLooper());
}
@Override
public void attachView(QrisResultContract.View view) {
this.view = view;
Log.d(TAG, "📎 View attached to presenter");
}
@Override
public void detachView() {
this.view = null;
Log.d(TAG, "📎 View detached from presenter");
}
@Override
public void onDestroy() {
stopAllTimers();
detachView();
Log.d(TAG, "💀 Presenter destroyed");
}
@Override
public void initializeTransaction(String orderId, String transactionId, String amount,
String qrImageUrl, String qrString, String acquirer) {
Log.d(TAG, "🚀 Initializing transaction");
try {
int amountInt = Integer.parseInt(amount);
transaction.initialize(orderId, transactionId, amountInt, qrImageUrl, qrString, acquirer);
if (view != null) {
view.showAmount(transaction.getFormattedAmount());
view.showQrImage(transaction.getQrImageUrl());
view.showProviderName(transaction.getDisplayProviderName());
view.showStatus("Waiting for payment...");
}
Log.d(TAG, "✅ Transaction initialized successfully");
Log.d(TAG, " Provider: " + transaction.getDetectedProvider());
Log.d(TAG, " Expiration: " + transaction.getQrExpirationMinutes() + " minutes");
} catch (Exception e) {
Log.e(TAG, "❌ Failed to initialize transaction: " + e.getMessage(), e);
if (view != null) {
view.showError("Failed to initialize transaction: " + e.getMessage());
}
}
}
@Override
public void startQrManagement() {
Log.d(TAG, "🔄 Starting QR management");
isQrRefreshActive = true;
startTimer();
startQrRefreshMonitoring();
}
@Override
public void stopQrManagement() {
Log.d(TAG, "🛑 Stopping QR management");
isQrRefreshActive = false;
stopTimer();
if (qrRefreshHandler != null && qrRefreshRunnable != null) {
qrRefreshHandler.removeCallbacks(qrRefreshRunnable);
}
}
@Override
public void startTimer() {
if (isTimerActive) {
stopTimer();
}
Log.d(TAG, "⏰ Starting timer");
isTimerActive = true;
// Reset creation time
transaction.setQrCreationTime(System.currentTimeMillis());
timerRunnable = new Runnable() {
@Override
public void run() {
if (!isTimerActive || transaction.isPaymentProcessed()) {
return;
}
int remainingSeconds = transaction.getRemainingTimeInSeconds();
if (remainingSeconds > 0) {
// Update UI di main thread
new Handler(Looper.getMainLooper()).post(() -> {
if (view != null) {
int displayMinutes = remainingSeconds / 60;
int displaySeconds = remainingSeconds % 60;
String timeDisplay = String.format("%d:%02d", displayMinutes, displaySeconds);
view.showTimer(timeDisplay);
}
});
// Schedule next update
timerHandler.postDelayed(this, 1000);
} else {
// Timer expired
Log.w(TAG, "⏰ Timer expired");
isTimerActive = false;
onQrExpired();
}
}
};
timerHandler.post(timerRunnable);
}
@Override
public void stopTimer() {
Log.d(TAG, "⏰ Stopping timer");
isTimerActive = false;
if (timerHandler != null && timerRunnable != null) {
timerHandler.removeCallbacks(timerRunnable);
}
}
@Override
public void refreshQrCode() {
Log.d(TAG, "🔄 Refreshing QR code - Attempt " + refreshCounter);
// Pastikan di Main Thread untuk UI updates
new Handler(Looper.getMainLooper()).post(() -> {
if (view != null) {
view.showQrRefreshing();
view.showLoading();
}
});
repository.refreshQrCode(transaction, new QrisRepository.RepositoryCallback<QrisRepository.QrRefreshResult>() {
@Override
public void onSuccess(QrisRepository.QrRefreshResult result) {
Log.d(TAG, "✅ QR refresh successful");
// Update transaction data
transaction.updateQrCode(result.qrUrl, result.qrString, result.transactionId);
transaction.setQrCreationTime(System.currentTimeMillis()); // Reset creation time
// Pastikan di Main Thread untuk UI updates
new Handler(Looper.getMainLooper()).post(() -> {
if (view != null) {
view.hideLoading();
view.updateQrImage(result.qrUrl);
view.updateQrUrl(result.qrUrl);
view.showQrRefreshSuccess();
view.showToast("QR Code berhasil diperbarui!");
}
// Stop dan restart timer dengan benar
stopTimer();
startTimer();
// Restart monitoring
isQrRefreshActive = true;
startQrRefreshMonitoring();
});
}
@Override
public void onError(String errorMessage) {
Log.e(TAG, "❌ QR refresh failed: " + errorMessage);
new Handler(Looper.getMainLooper()).post(() -> {
if (view != null) {
view.hideLoading();
view.showQrRefreshFailed(errorMessage);
if (refreshCounter >= MAX_REFRESH_ATTEMPTS) {
view.navigateToMain();
}
}
});
}
});
}
@Override
public void onQrExpired() {
Log.w(TAG, "⏰ Handling QR expiration");
// Stop current timers to prevent race conditions
stopTimer();
if (view != null) {
view.showQrExpired();
}
// Cek apakah sudah mencapai limit refresh
if (refreshCounter >= MAX_REFRESH_ATTEMPTS) {
Log.w(TAG, "🛑 Maximum refresh attempts reached");
if (view != null) {
view.showToast("Maksimum percobaan refresh QR tercapai");
view.navigateToMain();
}
return;
}
// Increment counter
refreshCounter++;
Log.d(TAG, "🔄 Refresh attempt #" + refreshCounter);
// Auto-refresh tanpa delay
refreshQrCode();
}
private void startQrRefreshMonitoring() {
Log.d(TAG, "🔄 Starting QR refresh monitoring");
qrRefreshRunnable = new Runnable() {
@Override
public void run() {
if (!isQrRefreshActive || transaction.isPaymentProcessed()) {
return;
}
// Check if QR expired
if (transaction.isQrExpired()) {
Log.w(TAG, "⏰ QR Code expired during monitoring");
onQrExpired();
return;
}
// Schedule next check in 30 seconds
if (isQrRefreshActive) {
qrRefreshHandler.postDelayed(this, 30000);
}
}
};
qrRefreshHandler.post(qrRefreshRunnable);
}
@Override
public void startPaymentMonitoring() {
Log.d(TAG, "🔍 Starting payment monitoring");
isPaymentMonitorActive = true;
paymentMonitorRunnable = new Runnable() {
@Override
public void run() {
if (!isPaymentMonitorActive || transaction.isPaymentProcessed()) {
return;
}
checkPaymentStatus();
// Schedule next check in 3 seconds
if (isPaymentMonitorActive && !transaction.isPaymentProcessed()) {
paymentMonitorHandler.postDelayed(this, 3000);
}
}
};
paymentMonitorHandler.post(paymentMonitorRunnable);
}
@Override
public void stopPaymentMonitoring() {
Log.d(TAG, "🔍 Stopping payment monitoring");
isPaymentMonitorActive = false;
if (paymentMonitorHandler != null && paymentMonitorRunnable != null) {
paymentMonitorHandler.removeCallbacks(paymentMonitorRunnable);
}
}
@Override
public void checkPaymentStatus() {
repository.checkPaymentStatus(transaction, new QrisRepository.RepositoryCallback<QrisRepository.PaymentStatusResult>() {
@Override
public void onSuccess(QrisRepository.PaymentStatusResult result) {
handlePaymentStatusResult(result);
}
@Override
public void onError(String errorMessage) {
Log.w(TAG, "⚠️ Payment status check failed: " + errorMessage);
// Don't show error to user untuk status check failures
}
});
}
private void handlePaymentStatusResult(QrisRepository.PaymentStatusResult result) {
Log.d(TAG, "💳 Payment status result: " + result.status);
// Update transaction dengan actual issuer/acquirer
if (result.issuer != null && !result.issuer.isEmpty()) {
transaction.setActualIssuer(result.issuer);
}
if (result.acquirer != null && !result.acquirer.isEmpty()) {
transaction.setActualAcquirer(result.acquirer);
}
// Update QR string jika ada
if (result.qrString != null && !result.qrString.isEmpty()) {
transaction.setQrString(result.qrString);
}
// Handle status changes
if (!result.status.equals(lastKnownStatus)) {
Log.d(TAG, "📊 Status changed: " + lastKnownStatus + " -> " + result.status);
lastKnownStatus = result.status;
transaction.setCurrentStatus(result.status);
if (view != null) {
switch (result.status) {
case "settlement":
case "capture":
case "success":
if (!transaction.isPaymentProcessed()) {
handlePaymentSuccess();
}
break;
case "expire":
case "cancel":
view.showPaymentFailed("Payment " + result.status);
stopAllTimers();
break;
case "pending":
view.showPaymentPending();
break;
default:
view.showStatus("Status: " + result.status);
break;
}
}
}
}
private void handlePaymentSuccess() {
Log.d(TAG, "🎉 Payment successful!");
transaction.setPaymentProcessed(true);
stopAllTimers();
if (view != null) {
String providerName = transaction.getDisplayProviderName();
view.showPaymentSuccess(providerName);
view.startSuccessAnimation();
view.showToast("Pembayaran " + providerName + " berhasil! 🎉");
}
// Don't auto-navigate here - let the view handle the navigation timing
}
@Override
public void onCancelClicked() {
Log.d(TAG, "❌ Cancel clicked");
stopAllTimers();
if (view != null) {
view.finishActivity();
}
}
@Override
public void onBackPressed() {
Log.d(TAG, "⬅️ Back pressed");
stopAllTimers();
if (view != null) {
view.finishActivity();
}
}
@Override
public void onSimulatePayment() {
Log.d(TAG, "🚀 Simulating payment");
if (transaction.isPaymentProcessed()) {
Log.w(TAG, "⚠️ Payment already processed");
if (view != null) {
view.showToast("Pembayaran sudah diproses");
}
return;
}
stopAllTimers();
if (view != null) {
view.showToast("Mensimulasikan pembayaran...");
view.showLoading();
}
repository.simulatePayment(transaction, new QrisRepository.RepositoryCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
Log.d(TAG, "✅ Payment simulation successful");
if (view != null) {
view.hideLoading();
}
// Wait a bit then trigger success
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!transaction.isPaymentProcessed()) {
handlePaymentSuccess();
}
}, 2000);
}
@Override
public void onError(String errorMessage) {
Log.e(TAG, "❌ Payment simulation failed: " + errorMessage);
if (view != null) {
view.hideLoading();
view.showError("Simulasi gagal: " + errorMessage);
}
// Restart monitoring after simulation failure
startPaymentMonitoring();
}
});
}
/**
* Stop all timers dan background tasks
*/
private void stopAllTimers() {
Log.d(TAG, "🛑 Stopping all timers");
stopTimer();
stopQrManagement();
stopPaymentMonitoring();
// Clear all pending callbacks
if (timerHandler != null) {
timerHandler.removeCallbacksAndMessages(null);
}
if (qrRefreshHandler != null) {
qrRefreshHandler.removeCallbacksAndMessages(null);
}
if (paymentMonitorHandler != null) {
paymentMonitorHandler.removeCallbacksAndMessages(null);
}
}
/**
* Public method untuk start semua monitoring
*/
public void startAllMonitoring() {
Log.d(TAG, "🚀 Starting all monitoring");
startQrManagement();
startPaymentMonitoring();
// Start polling untuk payment logs
repository.pollPaymentLogs(transaction.getOrderId(), new QrisRepository.RepositoryCallback<QrisRepository.PaymentLogResult>() {
@Override
public void onSuccess(QrisRepository.PaymentLogResult result) {
if (result.found) {
Log.d(TAG, "📊 Payment log found with status: " + result.status);
if ("settlement".equals(result.status) ||
"capture".equals(result.status) ||
"success".equals(result.status)) {
if (!transaction.isPaymentProcessed()) {
handlePaymentSuccess();
}
}
if (view != null) {
view.showToast("Payment log found!");
}
}
}
@Override
public void onError(String errorMessage) {
Log.w(TAG, "⚠️ Payment log polling failed: " + errorMessage);
// Don't show error to user
}
});
}
// Getter untuk transaction (untuk testing atau debugging)
public QrisTransaction getTransaction() {
return transaction;
}
}

View File

@ -0,0 +1,3 @@
public class PaymentStatussMonitor {
}

View File

@ -0,0 +1,162 @@
package com.example.bdkipoc.qris.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
/**
* Utility class untuk loading QR images secara asynchronous
* Dengan error handling dan validation yang proper
*/
public class QrImageLoader {
private static final String TAG = "QrImageLoader";
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=";
/**
* Interface untuk callback hasil loading image
*/
public interface ImageLoadCallback {
void onImageLoaded(Bitmap bitmap);
void onImageLoadFailed(String errorMessage);
}
/**
* Load QR image dari URL dengan callback
*/
public static void loadQrImage(String qrImageUrl, ImageLoadCallback callback) {
if (qrImageUrl == null || qrImageUrl.isEmpty()) {
Log.w(TAG, "⚠️ QR image URL is empty");
callback.onImageLoadFailed("QR image URL is empty");
return;
}
if (!qrImageUrl.startsWith("http")) {
Log.e(TAG, "❌ Invalid QR URL format: " + qrImageUrl);
callback.onImageLoadFailed("Invalid QR code URL format");
return;
}
Log.d(TAG, "🖼️ Loading QR image from: " + qrImageUrl);
new EnhancedDownloadImageTask(callback).execute(qrImageUrl);
}
/**
* Load QR image langsung ke ImageView (legacy support)
*/
public static void loadQrImageToView(String qrImageUrl, ImageView imageView) {
loadQrImage(qrImageUrl, new ImageLoadCallback() {
@Override
public void onImageLoaded(Bitmap bitmap) {
if (imageView != null) {
imageView.setImageBitmap(bitmap);
Log.d(TAG, "✅ QR code image displayed successfully");
}
}
@Override
public void onImageLoadFailed(String errorMessage) {
Log.e(TAG, "❌ Failed to display QR code image: " + errorMessage);
if (imageView != null) {
imageView.setImageResource(android.R.drawable.ic_menu_report_image);
if (imageView.getContext() != null) {
Toast.makeText(imageView.getContext(), "QR Error: " + errorMessage, Toast.LENGTH_LONG).show();
}
}
}
});
}
/**
* Enhanced AsyncTask untuk download image dengan proper error handling
*/
private static class EnhancedDownloadImageTask extends AsyncTask<String, Void, Bitmap> {
private ImageLoadCallback callback;
private String errorMessage;
EnhancedDownloadImageTask(ImageLoadCallback callback) {
this.callback = callback;
}
@Override
protected Bitmap doInBackground(String... urls) {
String urlDisplay = urls[0];
Bitmap bitmap = null;
try {
if (urlDisplay == null || urlDisplay.isEmpty()) {
Log.e(TAG, "❌ Empty QR URL provided");
errorMessage = "QR URL is empty";
return null;
}
if (!urlDisplay.startsWith("http")) {
Log.e(TAG, "❌ Invalid QR URL format: " + urlDisplay);
errorMessage = "Invalid QR URL format";
return null;
}
Log.d(TAG, "📥 Downloading image from: " + urlDisplay);
URL url = new URI(urlDisplay).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
connection.setRequestProperty("Accept", "image/*");
// Add auth header untuk Midtrans URLs
if (urlDisplay.contains("midtrans.com")) {
connection.setRequestProperty("Authorization", MIDTRANS_AUTH);
}
connection.connect();
int responseCode = connection.getResponseCode();
Log.d(TAG, "📥 Image download response code: " + responseCode);
if (responseCode == 200) {
InputStream input = connection.getInputStream();
bitmap = BitmapFactory.decodeStream(input);
if (bitmap != null) {
Log.d(TAG, "✅ Image downloaded successfully. Size: " +
bitmap.getWidth() + "x" + bitmap.getHeight());
} else {
Log.e(TAG, "❌ Failed to decode bitmap from stream");
errorMessage = "Failed to decode QR code image";
}
} else {
Log.e(TAG, "❌ Failed to download image. HTTP code: " + responseCode);
errorMessage = "Failed to download QR code (HTTP " + responseCode + ")";
}
} catch (Exception e) {
Log.e(TAG, "❌ Exception downloading image: " + e.getMessage(), e);
errorMessage = "Error downloading QR code: " + e.getMessage();
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
if (callback != null) {
if (result != null) {
callback.onImageLoaded(result);
} else {
callback.onImageLoadFailed(errorMessage != null ? errorMessage : "Unknown error");
}
}
}
}
}

View File

@ -0,0 +1,3 @@
public class QrisValidator {
}

View File

@ -0,0 +1,757 @@
package com.example.bdkipoc.qris.view;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
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 android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import com.example.bdkipoc.R;
import com.example.bdkipoc.ReceiptActivity;
import com.example.bdkipoc.qris.model.QrisTransaction;
import com.example.bdkipoc.qris.presenter.QrisResultPresenter;
import com.example.bdkipoc.qris.utils.QrImageLoader;
/**
* QrisResultActivity - refactored menggunakan MVP pattern
* Hanya menghandle UI logic, business logic ada di Presenter
*/
public class QrisResultActivity extends AppCompatActivity implements QrisResultContract.View {
private static final String TAG = "QrisResultActivity";
// Presenter
private QrisResultPresenter presenter;
// Main UI Components
private ImageView qrImageView;
private TextView amountTextView;
private TextView timerTextView;
private Button cancelButton;
private TextView qrisLogo;
private CardView mainCard;
private View headerBackground;
private View backNavigation;
// Hidden components for functionality
private TextView referenceTextView;
private TextView statusTextView;
private TextView qrStatusTextView;
private ProgressBar progressBar;
private Button downloadQrisButton;
private Button checkStatusButton;
private Button returnMainButton;
// Success screen views
private View successScreen;
private ImageView successIcon;
private TextView successMessage;
private TextView qrUrlTextView;
private Button simulatorButton;
// Animation handler
private Handler animationHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qris_result);
Log.d(TAG, "=== QRIS RESULT ACTIVITY STARTED (MVP) ===");
// Initialize presenter
presenter = new QrisResultPresenter();
presenter.attachView(this);
// Initialize views
initializeViews();
// Setup UI components
setupUI();
setupClickListeners();
// Get intent data dan initialize transaction
initializeFromIntent();
// Start monitoring
presenter.startAllMonitoring();
}
/**
* Initialize all view components
*/
private void initializeViews() {
// Main visible components
qrImageView = findViewById(R.id.qrImageView);
amountTextView = findViewById(R.id.amountTextView);
timerTextView = findViewById(R.id.timerTextView);
cancelButton = findViewById(R.id.cancel_button);
qrisLogo = findViewById(R.id.qris_logo);
mainCard = findViewById(R.id.main_card);
headerBackground = findViewById(R.id.header_background);
backNavigation = findViewById(R.id.back_navigation);
// Hidden components for functionality
referenceTextView = findViewById(R.id.referenceTextView);
statusTextView = findViewById(R.id.statusTextView);
qrStatusTextView = findViewById(R.id.qrStatusTextView);
progressBar = findViewById(R.id.progressBar);
downloadQrisButton = findViewById(R.id.downloadQrisButton);
checkStatusButton = findViewById(R.id.checkStatusButton);
returnMainButton = findViewById(R.id.returnMainButton);
// Success screen views
successScreen = findViewById(R.id.success_screen);
successIcon = findViewById(R.id.success_icon);
successMessage = findViewById(R.id.success_message);
qrUrlTextView = findViewById(R.id.qrUrlTextView);
simulatorButton = findViewById(R.id.simulatorButton);
}
/**
* Setup basic UI components
*/
private void setupUI() {
// Hide success screen initially
if (successScreen != null) {
successScreen.setVisibility(View.GONE);
}
// Disable check status button initially
if (checkStatusButton != null) {
checkStatusButton.setEnabled(false);
}
// Setup URL copy functionality
setupUrlCopyFunctionality();
// Setup simulator button
setupSimulatorButton();
}
/**
* Get data dari intent dan initialize transaction
*/
private void initializeFromIntent() {
Intent intent = getIntent();
String orderId = intent.getStringExtra("orderId");
String transactionId = intent.getStringExtra("transactionId");
String amount = String.valueOf(intent.getIntExtra("amount", 0));
String qrImageUrl = intent.getStringExtra("qrImageUrl");
String qrString = intent.getStringExtra("qrString");
String acquirer = intent.getStringExtra("acquirer");
Log.d(TAG, "Initializing transaction with data:");
Log.d(TAG, " Order ID: " + orderId);
Log.d(TAG, " Transaction ID: " + transactionId);
Log.d(TAG, " Amount: " + amount);
Log.d(TAG, " QR URL: " + qrImageUrl);
Log.d(TAG, " Acquirer: " + acquirer);
// Validate required data
if (orderId == null || transactionId == null) {
Log.e(TAG, "❌ Critical error: orderId or transactionId is null!");
showError("Missing transaction details! Cannot proceed.");
finish();
return;
}
// Initialize via presenter
presenter.initializeTransaction(orderId, transactionId, amount, qrImageUrl, qrString, acquirer);
// Set additional data
if (referenceTextView != null) {
String referenceId = intent.getStringExtra("referenceId");
referenceTextView.setText("Reference ID: " + referenceId);
}
}
/**
* Setup click listeners untuk semua buttons dan views
*/
private void setupClickListeners() {
// Cancel button
if (cancelButton != null) {
cancelButton.setOnClickListener(v -> {
addClickAnimation(v);
presenter.onCancelClicked();
});
}
// Back navigation
if (backNavigation != null) {
backNavigation.setOnClickListener(v -> {
addClickAnimation(v);
presenter.onBackPressed();
});
}
// Hidden check status button untuk testing
if (checkStatusButton != null) {
checkStatusButton.setOnClickListener(v -> {
Log.d(TAG, "Manual payment simulation triggered");
presenter.onSimulatePayment();
});
}
// Hidden return main button
if (returnMainButton != null) {
returnMainButton.setOnClickListener(v -> {
navigateToMain();
});
}
// Double tap pada QR logo untuk testing
if (qrisLogo != null) {
qrisLogo.setOnClickListener(new View.OnClickListener() {
private int clickCount = 0;
private Handler handler = new Handler();
private final int DOUBLE_TAP_TIMEOUT = 300;
@Override
public void onClick(View v) {
clickCount++;
if (clickCount == 1) {
handler.postDelayed(() -> clickCount = 0, DOUBLE_TAP_TIMEOUT);
} else if (clickCount == 2) {
// Double tap detected - simulate payment
clickCount = 0;
Log.d(TAG, "Double tap detected - simulating payment");
presenter.onSimulatePayment();
}
}
});
}
}
private void setupUrlCopyFunctionality() {
if (qrUrlTextView != null) {
qrUrlTextView.setOnClickListener(v -> {
if (presenter.getTransaction() != null) {
String qrUrl = presenter.getTransaction().getQrImageUrl();
if (qrUrl != null) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("QR URL", qrUrl);
clipboard.setPrimaryClip(clip);
showToast("URL copied to clipboard");
}
}
});
}
}
private void setupSimulatorButton() {
if (simulatorButton != null) {
simulatorButton.setOnClickListener(v -> {
try {
String simulatorUrl = "https://simulator.sandbox.midtrans.com/v2/qris/index";
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(simulatorUrl));
startActivity(browserIntent);
} catch (Exception e) {
showToast("Could not open browser");
Log.e(TAG, "Error opening simulator URL", e);
}
});
}
}
// ========================================================================================
// MVP CONTRACT VIEW IMPLEMENTATIONS
// ========================================================================================
@Override
public void showQrImage(String qrImageUrl) {
Log.d(TAG, "🖼️ Showing QR image: " + qrImageUrl);
if (qrImageUrl != null && !qrImageUrl.isEmpty()) {
QrImageLoader.loadQrImage(qrImageUrl, new QrImageLoader.ImageLoadCallback() {
@Override
public void onImageLoaded(Bitmap bitmap) {
if (qrImageView != null) {
qrImageView.setImageBitmap(bitmap);
qrImageView.setAlpha(1.0f); // Ensure fully visible
}
}
@Override
public void onImageLoadFailed(String errorMessage) {
Log.e(TAG, "❌ Failed to load QR image: " + errorMessage);
if (qrImageView != null) {
qrImageView.setVisibility(View.GONE);
}
showError("Failed to load QR code: " + errorMessage);
}
});
// Update URL display
if (qrUrlTextView != null) {
qrUrlTextView.setText(qrImageUrl);
}
} else {
Log.w(TAG, "⚠️ QR image URL is not available");
if (qrImageView != null) {
qrImageView.setVisibility(View.GONE);
}
showToast("QR code URL not available");
}
}
@Override
public void showAmount(String formattedAmount) {
Log.d(TAG, "💰 Showing amount: " + formattedAmount);
if (amountTextView != null) {
amountTextView.setText(formattedAmount);
}
}
@Override
public void showTimer(String timeDisplay) {
if (timerTextView != null) {
timerTextView.setText(timeDisplay);
timerTextView.setTextColor(getResources().getColor(android.R.color.black));
}
}
@Override
public void showStatus(String status) {
Log.d(TAG, "📊 Showing status: " + status);
if (statusTextView != null) {
statusTextView.setText(status);
}
}
@Override
public void showProviderName(String providerName) {
Log.d(TAG, "🏷️ Showing provider: " + providerName);
// Provider name bisa ditampilkan di UI jika ada komponen khusus
}
@Override
public void updateQrImage(String newQrImageUrl) {
Log.d(TAG, "🔄 Updating QR image: " + newQrImageUrl);
runOnUiThread(() -> {
// Reset QR image appearance first
if (qrImageView != null) {
qrImageView.setAlpha(1.0f);
qrImageView.setVisibility(View.VISIBLE);
}
// Load new QR image
showQrImage(newQrImageUrl);
// Update timer display
if (timerTextView != null) {
timerTextView.setTextColor(getResources().getColor(android.R.color.black));
}
});
}
@Override
public void updateQrUrl(String newQrUrl) {
Log.d(TAG, "🔄 Updating QR URL: " + newQrUrl);
runOnUiThread(() -> {
if (qrUrlTextView != null) {
qrUrlTextView.setText(newQrUrl);
qrUrlTextView.setVisibility(View.VISIBLE);
}
});
}
@Override
public void showQrExpired() {
Log.w(TAG, "⏰ Showing QR expired");
runOnUiThread(() -> {
// Make QR semi-transparent
if (qrImageView != null) {
qrImageView.setAlpha(0.5f);
}
if (timerTextView != null) {
timerTextView.setText("EXPIRED");
timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
}
});
}
@Override
public void showQrRefreshing() {
Log.d(TAG, "🔄 Showing QR refreshing");
runOnUiThread(() -> {
if (timerTextView != null) {
timerTextView.setText("Refreshing...");
timerTextView.setTextColor(getResources().getColor(android.R.color.holo_orange_dark));
}
});
}
@Override
public void showQrRefreshFailed(String errorMessage) {
Log.e(TAG, "❌ QR refresh failed: " + errorMessage);
runOnUiThread(() -> {
if (timerTextView != null) {
timerTextView.setText("Refresh Gagal");
timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
}
// Tidak langsung navigate, biarkan presenter handle
showToast("Gagal refresh QR: " + errorMessage);
});
}
@Override
public void showQrRefreshSuccess() {
Log.d(TAG, "✅ QR refresh successful");
runOnUiThread(() -> {
// Reset QR image appearance
if (qrImageView != null) {
qrImageView.setAlpha(1.0f);
qrImageView.setVisibility(View.VISIBLE);
}
// Reset timer color and show success message
if (timerTextView != null) {
timerTextView.setTextColor(getResources().getColor(android.R.color.black));
}
// Show success toast
showToast("QR Code berhasil diperbarui!");
});
}
@Override
public void showPaymentSuccess(String providerName) {
Log.d(TAG, "🎉 Showing payment success for: " + providerName);
runOnUiThread(() -> {
showFullScreenSuccess(providerName);
// Navigate to receipt after 3 seconds, then to main activity
new Handler(Looper.getMainLooper()).postDelayed(() -> {
// Fixed: Remove the undefined 'view' variable and just check if activity is still valid
if (!isFinishing() && !isDestroyed()) {
navigateToReceipt(presenter.getTransaction());
}
}, 3000);
});
}
@Override
public void showPaymentFailed(String reason) {
Log.w(TAG, "❌ Payment failed: " + reason);
showToast("Payment failed: " + reason);
}
@Override
public void showPaymentPending() {
Log.d(TAG, "⏳ Payment pending");
showStatus("Payment pending...");
}
@Override
public void showLoading() {
if (progressBar != null) {
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
public void hideLoading() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1001) { // Receipt activity result
Log.d(TAG, "📄 Receipt activity finished, navigating to main");
navigateToMainWithTransactionComplete();
}
}
private void navigateToMainWithTransactionComplete() {
Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
// Add transaction completion data
intent.putExtra("transaction_completed", true);
if (presenter != null && presenter.getTransaction() != null) {
intent.putExtra("transaction_amount", String.valueOf(presenter.getTransaction().getOriginalAmount()));
intent.putExtra("payment_provider", presenter.getTransaction().getDisplayProviderName());
}
startActivity(intent);
finishAffinity(); // Clear all activities in the task
}
@Override
public void navigateToReceipt(QrisTransaction transaction) {
Log.d(TAG, "📄 Navigating to receipt");
Intent intent = new Intent(this, ReceiptActivity.class);
// Put transaction data
intent.putExtra("calling_activity", "QrisResultActivity");
intent.putExtra("transaction_id", transaction.getTransactionId());
intent.putExtra("reference_id", transaction.getReferenceId());
intent.putExtra("order_id", transaction.getOrderId());
intent.putExtra("transaction_amount", String.valueOf(transaction.getOriginalAmount()));
intent.putExtra("gross_amount", transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount()));
intent.putExtra("created_at", getCurrentDateTime());
intent.putExtra("transaction_date", getCurrentDateTime());
intent.putExtra("payment_method", "QRIS");
intent.putExtra("channel_code", "QRIS");
intent.putExtra("channel_category", "RETAIL_OUTLET");
intent.putExtra("card_type", transaction.getDisplayProviderName());
intent.putExtra("merchant_name", "Marcel Panjaitan");
intent.putExtra("merchant_location", "Jakarta, Indonesia");
intent.putExtra("acquirer", transaction.getActualIssuer() != null ? transaction.getActualIssuer() : transaction.getAcquirer());
intent.putExtra("mid", "71000026521");
intent.putExtra("tid", "73001500");
// Enhanced data
intent.putExtra("detected_provider", transaction.getDetectedProvider());
intent.putExtra("qr_expiration_minutes", transaction.getQrExpirationMinutes());
intent.putExtra("was_qr_refresh_transaction", transaction.isQrRefreshTransaction());
// QR string
if (transaction.getQrString() != null && !transaction.getQrString().isEmpty()) {
intent.putExtra("qr_string", transaction.getQrString());
}
// Add flag to automatically return to main after receipt
intent.putExtra("auto_return_to_main", true);
startActivityForResult(intent, 1001);
}
@Override
public void navigateToMain() {
Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finishAffinity();
}
@Override
public void finishActivity() {
finish();
}
@Override
public void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
public void showError(String errorMessage) {
Log.e(TAG, "❌ Error: " + errorMessage);
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
}
@Override
public void startSuccessAnimation() {
Log.d(TAG, "🎬 Starting success animation");
// Animation akan di-handle oleh showFullScreenSuccess
}
@Override
public void stopAllAnimations() {
Log.d(TAG, "🛑 Stopping all animations");
if (animationHandler != null) {
animationHandler.removeCallbacksAndMessages(null);
}
}
// ========================================================================================
// PRIVATE HELPER METHODS
// ========================================================================================
/**
* Show full screen success dengan animations
*/
private void showFullScreenSuccess(String providerName) {
if (successScreen != null && !isFinishing()) {
// Hide main UI components
hideMainUIComponents();
// Set success message
if (successMessage != null) {
successMessage.setText("Pembayaran " + providerName + " Berhasil");
}
// Show success screen dengan fade in animation
successScreen.setVisibility(View.VISIBLE);
successScreen.setAlpha(0f);
// Fade in background
ObjectAnimator backgroundFadeIn = ObjectAnimator.ofFloat(successScreen, "alpha", 0f, 1f);
backgroundFadeIn.setDuration(500);
backgroundFadeIn.start();
// Success icon animation
if (successIcon != null) {
successIcon.setScaleX(0f);
successIcon.setScaleY(0f);
successIcon.setAlpha(0f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(successIcon, "scaleX", 0f, 1.2f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(successIcon, "scaleY", 0f, 1.2f, 1f);
ObjectAnimator iconFadeIn = ObjectAnimator.ofFloat(successIcon, "alpha", 0f, 1f);
AnimatorSet iconAnimation = new AnimatorSet();
iconAnimation.playTogether(scaleX, scaleY, iconFadeIn);
iconAnimation.setDuration(800);
iconAnimation.setStartDelay(300);
iconAnimation.setInterpolator(new android.view.animation.OvershootInterpolator(1.2f));
iconAnimation.start();
}
// Success message animation
if (successMessage != null) {
successMessage.setAlpha(0f);
successMessage.setTranslationY(50f);
ObjectAnimator messageSlideUp = ObjectAnimator.ofFloat(successMessage, "translationY", 50f, 0f);
ObjectAnimator messageFadeIn = ObjectAnimator.ofFloat(successMessage, "alpha", 0f, 1f);
AnimatorSet messageAnimation = new AnimatorSet();
messageAnimation.playTogether(messageSlideUp, messageFadeIn);
messageAnimation.setDuration(600);
messageAnimation.setStartDelay(600);
messageAnimation.setInterpolator(new android.view.animation.DecelerateInterpolator());
messageAnimation.start();
}
}
}
/**
* Hide main UI components untuk clean success screen
*/
private void hideMainUIComponents() {
if (mainCard != null) {
mainCard.setVisibility(View.GONE);
}
if (headerBackground != null) {
headerBackground.setVisibility(View.GONE);
}
if (backNavigation != null) {
backNavigation.setVisibility(View.GONE);
}
if (cancelButton != null) {
cancelButton.setVisibility(View.GONE);
}
}
/**
* Add click animation ke view
*/
private void addClickAnimation(View view) {
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(scaleX, scaleY);
animatorSet.setDuration(150);
animatorSet.start();
}
/**
* Get current date time untuk receipt
*/
private String getCurrentDateTime() {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("d/M/y H:m:s", new java.util.Locale("id", "ID"));
return sdf.format(new java.util.Date());
}
// ========================================================================================
// LIFECYCLE METHODS
// ========================================================================================
@Override
protected void onDestroy() {
super.onDestroy();
// Cleanup presenter
if (presenter != null) {
presenter.onDestroy();
}
// Cleanup animation handler
if (animationHandler != null) {
animationHandler.removeCallbacksAndMessages(null);
}
Log.d(TAG, "💀 QrisResultActivity destroyed");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "⏸️ QrisResultActivity paused");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "▶️ QrisResultActivity resumed");
}
@Override
public void onBackPressed() {
// Prevent back press during success screen animation
if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) {
Log.d(TAG, "⬅️ Back press blocked during success screen");
return;
}
// Show confirmation dialog before leaving
androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this);
builder.setTitle("Batalkan Transaksi");
builder.setMessage("Apakah Anda yakin ingin membatalkan transaksi ini?");
builder.setPositiveButton("Ya, Batalkan", (dialog, which) -> {
if (presenter != null) {
presenter.onBackPressed();
} else {
super.onBackPressed();
}
});
builder.setNegativeButton("Tidak", (dialog, which) -> {
dialog.dismiss();
});
builder.show();
}
}

View File

@ -0,0 +1,86 @@
package com.example.bdkipoc.qris.view;
import com.example.bdkipoc.qris.model.QrisTransaction;
/**
* Contract interface untuk QrisResult module
* Mendefinisikan komunikasi antara View dan Presenter
*/
public interface QrisResultContract {
/**
* View interface - apa yang bisa dilakukan oleh View (Activity)
*/
interface View {
// UI Display methods
void showQrImage(String qrImageUrl);
void showAmount(String formattedAmount);
void showTimer(String timeDisplay);
void showStatus(String status);
void showProviderName(String providerName);
// QR Management
void updateQrImage(String newQrImageUrl);
void updateQrUrl(String newQrUrl);
void showQrExpired();
void showQrRefreshing();
void showQrRefreshFailed(String errorMessage);
void showQrRefreshSuccess();
// Payment Status
void showPaymentSuccess(String providerName);
void showPaymentFailed(String reason);
void showPaymentPending();
// Loading states
void showLoading();
void hideLoading();
// Navigation
void navigateToReceipt(QrisTransaction transaction);
void navigateToMain();
void finishActivity();
// User feedback
void showToast(String message);
void showError(String errorMessage);
// Animation
void startSuccessAnimation();
void stopAllAnimations();
}
/**
* Presenter interface - apa yang bisa dilakukan oleh Presenter
*/
interface Presenter {
// Lifecycle
void attachView(View view);
void detachView();
void onDestroy();
// Initialization
void initializeTransaction(String orderId, String transactionId, String amount,
String qrImageUrl, String qrString, String acquirer);
// QR Management
void startQrManagement();
void stopQrManagement();
void refreshQrCode();
void onQrExpired();
// Payment Monitoring
void startPaymentMonitoring();
void stopPaymentMonitoring();
void checkPaymentStatus();
// User Actions
void onCancelClicked();
void onBackPressed();
void onSimulatePayment(); // For testing
// Timer
void startTimer();
void stopTimer();
}
}

View File

@ -0,0 +1,356 @@
package com.example.bdkipoc.settlement;
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;
import com.example.bdkipoc.R;
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,832 @@
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;
import com.sunmi.peripheral.printer.InnerResultCallback;
import android.os.RemoteException;
import com.example.bdkipoc.MyApplication;
import com.sunmi.peripheral.printer.SunmiPrinterService;
/**
* 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
private final InnerResultCallback printCallback = new InnerResultCallback() {
@Override
public void onRunResult(boolean isSuccess) throws RemoteException {
runOnUiThread(() -> {
if (isSuccess) {
showToast("Struk berhasil dicetak");
} else {
showToast("Gagal mencetak struk");
}
});
}
@Override
public void onReturnString(String result) throws RemoteException {
Log.d(TAG, "Print result: " + result);
}
@Override
public void onRaiseException(int code, String msg) throws RemoteException {
runOnUiThread(() -> showToast("Printer error: " + msg));
Log.e(TAG, "Printer exception: " + code + " - " + msg);
}
@Override
public void onPrintResult(int code, String msg) throws RemoteException {
Log.d(TAG, "Print result code: " + code + ", msg: " + msg);
}
};
@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() {
try {
// Check if printer service is available
if (com.example.bdkipoc.MyApplication.app.sunmiPrinterService == null) {
showToast("Printer tidak tersedia");
return;
}
SunmiPrinterService printerService = com.example.bdkipoc.MyApplication.app.sunmiPrinterService;
// Get all the data from the views
String merchantNameText = merchantName.getText().toString();
String merchantLocationText = merchantLocation.getText().toString();
String midTextValue = midText.getText().toString();
String tidTextValue = tidText.getText().toString();
String transactionNumberText = transactionNumber.getText().toString();
String transactionDateText = transactionDate.getText().toString();
String paymentMethodText = paymentMethod.getText().toString();
String cardTypeText = cardType.getText().toString();
String transactionTotalText = transactionTotal.getText().toString();
String taxPercentageText = taxPercentage.getText().toString();
String serviceFeeText = serviceFee.getText().toString();
String finalTotalText = finalTotal.getText().toString();
showToast("Mencetak struk...");
try {
// Start printing
printerService.enterPrinterBuffer(true);
// Set alignment to center
printerService.setAlignment(1, null);
// Print header
printerService.printText("# Payvora PRO\n\n", null);
// Set alignment to left
printerService.setAlignment(0, null);
// Print merchant info
printerService.printText(merchantNameText + "\n", null);
printerService.printText(merchantLocationText + "\n\n", null);
// Print MID/TID
printerService.printText(midTextValue + " | " + tidTextValue + "\n\n", null);
// Print transaction details
printerService.printText("Nomor transaksi " + transactionNumberText + "\n", null);
printerService.printText("Tanggal transaksi " + transactionDateText + "\n", null);
printerService.printText("Metode pembayaran " + paymentMethodText + "\n", null);
printerService.printText("Jenis Kartu " + cardTypeText + "\n\n", null);
// Print amounts
printerService.printText("Total transaksi " + transactionTotalText + "\n", null);
printerService.printText("Pajak (%) " + taxPercentageText + "\n", null);
printerService.printText("Biaya Layanan " + serviceFeeText + "\n\n", null);
// Print total in bold
printerService.printText("**TOTAL** **" + finalTotalText + "**\n", null);
// Add EMV specific details if available
// if (emvMode && emvCardholderName != null) {
// printerService.printText("\nDETAIL EMV:\n", null);
// printerService.printText("Cardholder: " + emvCardholderName + "\n", null);
// if (cardNo != null) {
// printerService.printText("Card: " + maskCardNumber(cardNo) + "\n", null);
// }
// if (emvAid != null) {
// printerService.printText("AID: " + emvAid + "\n", null);
// }
// if (emvExpiry != null) {
// printerService.printText("Expiry: " + emvExpiry + "\n", null);
// }
// }
// Add some line feeds
printerService.lineWrap(4, null);
// Exit buffer mode
printerService.exitPrinterBuffer(true);
} catch (RemoteException e) {
e.printStackTrace();
showToast("Error printer: " + e.getMessage());
}
} catch (Exception e) {
Log.e(TAG, "Print error", e);
showToast("Error saat mencetak: " + e.getMessage());
}
}
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,427 @@
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) {
// Force reset jika masih ada proses berjalan
if (mProcessStep > 0) {
Log.w(TAG, "Forcing EMV reset - previous step: " + mProcessStep);
resetEMVProcess();
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
Log.d(TAG, "Starting fresh EMV transaction");
mProcessStep = 1;
mCardType = cardType;
try {
// Extended initialization
mEMVOptV2.initEmvProcess();
Thread.sleep(500);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
try {
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 EMV with reset state");
mEMVOptV2.transactProcessEx(bundle, mEMVListener);
} catch (Exception e) {
Log.e(TAG, "Error starting EMV: " + e.getMessage());
if (callback != null) {
callback.onTransactionFailed(-1, "EMV start error: " + e.getMessage());
}
}
}, 800); // Longer delay
} catch (Exception e) {
Log.e(TAG, "Error in EMV transaction setup: " + e.getMessage());
if (callback != null) {
callback.onTransactionFailed(-1, e.getMessage());
}
}
}
public void resetEMVProcess() {
try {
Log.d(TAG, "Resetting EMV process - current step: " + mProcessStep);
// Reset semua state variables FIRST
mProcessStep = 0;
mCardNo = null;
mPinType = 0;
mCertInfo = null;
mCardType = 0;
if (mEMVOptV2 != null) {
// Double reset untuk memastikan
mEMVOptV2.initEmvProcess();
Thread.sleep(300);
mEMVOptV2.initEmvProcess();
}
Log.d(TAG, "EMV process reset completed");
} 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 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white"/>
<stroke android:width="1dp" android:color="#DE0701"/>
<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="#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,12 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- Warna solid (biru cerah seperti #4299E1) -->
<solid android:color="#4299E1" />
<!-- Sudut melengkung -->
<corners android:radius="16dp" />
<!-- Hilangkan stroke jika tidak dibutuhkan -->
<stroke android:width="0dp" android:color="#00000000" />
</shape>

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="#DDDDDD" />
</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

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