Compare commits
92 Commits
master
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| ad08e80ae0 | |||
| ccfd3a09eb | |||
| dd57975908 | |||
| a49aab14f8 | |||
| 72b39fd9c8 | |||
| 40d0fc2402 | |||
| 78f9e95c3f | |||
| 69fd69ac3a | |||
| 0e86870b8b | |||
| 1c1d580a38 | |||
| 6d519d96cf | |||
| 64b666869e | |||
| a674574031 | |||
| 8cef8fdb22 | |||
| e0aec6e840 | |||
| 538249fc57 | |||
| a38cea065f | |||
| 671b585fe5 | |||
| c18fd2d831 | |||
| c033a26516 | |||
| f64779755a | |||
| 22d0409c0a | |||
| 2803182a02 | |||
| 960f64ee81 | |||
| 9dac55d07a | |||
| ddf76d2540 | |||
| b2442ada48 | |||
| a52f56e154 | |||
| 4209b193d7 | |||
| 44225f1d67 | |||
| 88069c0b56 | |||
| 6f98a91372 | |||
| 597921e32b | |||
| 3ac3598359 | |||
| 6660fca373 | |||
| edb1c6d09b | |||
| 6f78b6df3f | |||
| da312ec3ae | |||
| 53964211c2 | |||
| b66ef4bb00 | |||
| 7a2ddc3f15 | |||
| 8a73206a76 | |||
| f6650f99d0 | |||
| 8ac97437a2 | |||
| 2b57d35553 | |||
| f2c3de9f5f | |||
| f5d9e53118 | |||
| ece79942c1 | |||
| 0af0e836b1 | |||
| f403358554 | |||
| d43c4bad0c | |||
| 174a1461fd | |||
| f4e5e03077 | |||
| f48e3e64a4 | |||
| 2ea0792d28 | |||
| 9834d4b841 | |||
| 8add903edb | |||
| 124da43a1e | |||
| d7617186a6 | |||
| 93fc410e37 | |||
| 448dfd9835 | |||
| eac3179d8a | |||
| 729bdddad4 | |||
| c56cae64b9 | |||
| d4245c5906 | |||
| eddade3200 | |||
| 13ab6b717e | |||
| 991f77dabe | |||
| da8bcf17cc | |||
| b0ee2e8ee6 | |||
| 4aaa9957e7 | |||
| 99fab68e71 | |||
| 074a4b1f53 | |||
| a1f536b03e | |||
| edca7f92ec | |||
| 3f189f5975 | |||
| 5a03fc3aec | |||
| a30e767adc | |||
| 74f95e0374 | |||
| 1799e7eb0e | |||
| 2a24016637 | |||
| 459d9ab0f1 | |||
| 191966a2e4 | |||
| 46fb81b6a7 | |||
| 290f3015d9 | |||
| f1228db89a | |||
| 810964b4be | |||
| a7fa40d60a | |||
| a07e7a99ac | |||
| c55af6141f | |||
| 6d681f5e41 | |||
| 1ca26371a1 |
27
.env.example
Normal file
@ -0,0 +1,27 @@
|
||||
# ==============================================
|
||||
# QRIS PAYMENT CONFIGURATION - ENVIRONMENT VARIABLES
|
||||
# ==============================================
|
||||
# Copy this file to .env and fill in the values
|
||||
# ==============================================
|
||||
|
||||
# Midtrans API Configuration
|
||||
MIDTRANS_SANDBOX_AUTH=your_midtrans_sandbox_auth_here
|
||||
MIDTRANS_PRODUCTION_AUTH=your_midtrans_production_auth_here
|
||||
MIDTRANS_CHARGE_URL=https://api.sandbox.midtrans.com/v2/charge
|
||||
MIDTRANS_STATUS_BASE_URL=https://api.sandbox.midtrans.com/v2/
|
||||
MIDTRANS_SIMULATOR_URL=https://simulator.sandbox.midtrans.com/v2/qris/index
|
||||
|
||||
# Backend Configuration
|
||||
BACKEND_BASE_URL=your_backend_base_url_here
|
||||
WEBHOOK_URL=your_webhook_url_here
|
||||
|
||||
# Application Settings
|
||||
MAX_REFRESH_ATTEMPTS=5
|
||||
DEFAULT_QR_EXPIRATION_MINUTES=1
|
||||
|
||||
# ==============================================
|
||||
# INSTRUCTIONS:
|
||||
# 1. Copy this file to .env in the same directory
|
||||
# 2. Fill in the actual values
|
||||
# 3. NEVER commit .env to version control!
|
||||
# ==============================================
|
||||
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.env
|
||||
*.env
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "automatic"
|
||||
}
|
||||
@ -6,10 +6,17 @@ android {
|
||||
namespace 'com.example.bdkipoc'
|
||||
compileSdk 35
|
||||
|
||||
// Tambahkan lint options
|
||||
lint {
|
||||
abortOnError false
|
||||
disable 'GoogleAppIndexingWarning'
|
||||
disable 'NonConstantResourceId'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.bdkipoc"
|
||||
minSdk 21
|
||||
targetSdk 30
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
@ -22,19 +29,51 @@ 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']
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true // Ini yang mengaktifkan fitur BuildConfig
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// Tambahkan semua buildConfigField yang dibutuhkan
|
||||
buildConfigField "int", "MAX_REFRESH_ATTEMPTS", "3"
|
||||
buildConfigField "int", "DEFAULT_QR_EXPIRATION_MINUTES", "15"
|
||||
buildConfigField "String", "MIDTRANS_SANDBOX_AUTH", "\"Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=\""
|
||||
buildConfigField "String", "MIDTRANS_PRODUCTION_AUTH", "\"TWlkLXNlcnZlci1sMlZPalotdVlVanpvNnU4VzAtYmF1a2o=\""
|
||||
buildConfigField "String", "MIDTRANS_CHARGE_URL", "\"https://api.sandbox.midtrans.com/v2/charge\""
|
||||
buildConfigField "String", "MIDTRANS_STATUS_BASE_URL", "\"https://api.sandbox.midtrans.com/v2/\""
|
||||
buildConfigField "String", "BACKEND_BASE_URL", "\"https://be-edc.msvc.app\""
|
||||
buildConfigField "String", "WEBHOOK_URL", "\"https://be-edc.msvc.app/webhooks/midtrans\""
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
BIN
app/libs/PayLib-release-2.0.17-sources.jar
Normal file
BIN
app/libs/PayLib-release-2.0.17.aar
Normal file
BIN
app/libs/armeabi-v7a/libAE_100.so
Normal file
BIN
app/libs/armeabi-v7a/libCPACE_100.so
Normal file
BIN
app/libs/armeabi-v7a/libDPAS_100.so
Normal file
BIN
app/libs/armeabi-v7a/libEFTPOS_001.so
Normal file
BIN
app/libs/armeabi-v7a/libEMVL2Base.so
Normal file
BIN
app/libs/armeabi-v7a/libEMVL2Dirct.so
Normal file
BIN
app/libs/armeabi-v7a/libEMV_100.so
Normal file
BIN
app/libs/armeabi-v7a/libEntry.so
Normal file
BIN
app/libs/armeabi-v7a/libFLASH_001.so
Normal file
BIN
app/libs/armeabi-v7a/libJCB_100.so
Normal file
BIN
app/libs/armeabi-v7a/libMIR_001.so
Normal file
BIN
app/libs/armeabi-v7a/libPAGO_001.so
Normal file
BIN
app/libs/armeabi-v7a/libPURE_001.so
Normal file
BIN
app/libs/armeabi-v7a/libPaypass_100.so
Normal file
BIN
app/libs/armeabi-v7a/libPaywave_100.so
Normal file
BIN
app/libs/armeabi-v7a/libQPBOC_100.so
Normal file
BIN
app/libs/armeabi-v7a/libRupay_001.so
Normal file
BIN
app/libs/armeabi-v7a/libSamsungPay_001.so
Normal file
BIN
app/libs/armeabi-v7a/libsunmiemvl2.so
Normal file
BIN
app/libs/sunmiemvl2split-1.0.1.jar
Normal file
@ -8,7 +8,23 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<uses-permission android:name="com.sunmi.perm.LED" />
|
||||
<uses-permission android:name="com.sunmi.perm.MSR" />
|
||||
<uses-permission android:name="com.sunmi.perm.ICC" />
|
||||
<uses-permission android:name="com.sunmi.perm.PINPAD" />
|
||||
<uses-permission android:name="com.sunmi.perm.SECURITY" />
|
||||
<uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@ -17,6 +33,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.BDKIPOC"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@ -28,12 +45,72 @@
|
||||
</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=".settlement.SettlementDetailActivity"
|
||||
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>
|
||||
|
||||
|
||||
27
app/src/main/java/com/example/bdkipoc/CacheHelper.java
Normal file
@ -0,0 +1,27 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class CacheHelper {
|
||||
|
||||
private static final String PREFERENCE_FILE_NAME = "sm_pay_demo_obj";
|
||||
|
||||
private static final String KEY_LANGUAGE = "key_language";
|
||||
|
||||
public static void saveCurrentLanguage(int language) {
|
||||
SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);
|
||||
int value = sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO);
|
||||
if (value == language) return;
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.putInt(KEY_LANGUAGE, language);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static int getCurrentLanguage() {
|
||||
SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);
|
||||
return sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
17
app/src/main/java/com/example/bdkipoc/Constant.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
public class Constant {
|
||||
|
||||
public static final String TAG = "SDKTestDemo";
|
||||
|
||||
public static final int LANGUAGE_AUTO = 0;
|
||||
public static final int LANGUAGE_ZH_CN = 1;
|
||||
public static final int LANGUAGE_EN_US = 2;
|
||||
public static final int LANGUAGE_JA_JP = 3;
|
||||
|
||||
public static final int SCAN_MODEL_NONE = 100;
|
||||
public static final int SCAN_MODEL_P2Lite = 101;
|
||||
|
||||
public static final String SCAN_MODEL_NONE_VALUE = "NONE";
|
||||
public static final String SCAN_MODEL_P2Lite_VALUE = "P2Lite";
|
||||
}
|
||||
323
app/src/main/java/com/example/bdkipoc/LoginActivity.java
Normal file
@ -0,0 +1,323 @@
|
||||
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 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 using BuildConfig
|
||||
URL url = new URL(BuildConfig.BACKEND_BASE_URL + "/users/auth");
|
||||
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");
|
||||
|
||||
// Log user data to console
|
||||
logUserData(userData);
|
||||
|
||||
// Save login data
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to log user data to console
|
||||
private void logUserData(JSONObject userData) {
|
||||
try {
|
||||
StringBuilder userInfo = new StringBuilder();
|
||||
userInfo.append("\n=== USER LOGIN DETAILS ===");
|
||||
userInfo.append("\nID: ").append(userData.optString("id", "N/A"));
|
||||
userInfo.append("\nName: ").append(userData.optString("name", "N/A"));
|
||||
userInfo.append("\nEmail: ").append(userData.optString("email", "N/A"));
|
||||
userInfo.append("\nRole: ").append(userData.optString("role", "N/A"));
|
||||
userInfo.append("\nPhone: ").append(userData.optString("phone", "N/A"));
|
||||
userInfo.append("\nPosition: ").append(userData.optString("position", "N/A"));
|
||||
userInfo.append("\nMID: ").append(userData.optString("mid", "N/A"));
|
||||
userInfo.append("\nTID: ").append(userData.optString("tid", "N/A"));
|
||||
userInfo.append("\nLast Login: ").append(userData.optString("last_login", "N/A"));
|
||||
userInfo.append("\n==========================");
|
||||
|
||||
Log.i(TAG, userInfo.toString());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error logging user data: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,41 +1,675 @@
|
||||
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);
|
||||
|
||||
if (userData != null) {
|
||||
StringBuilder userInfo = new StringBuilder();
|
||||
userInfo.append("\n=== CURRENT USER DETAILS ===");
|
||||
userInfo.append("\nID: ").append(userData.optString("id", "N/A"));
|
||||
userInfo.append("\nName: ").append(userData.optString("name", "N/A"));
|
||||
userInfo.append("\nEmail: ").append(userData.optString("email", "N/A"));
|
||||
userInfo.append("\nRole: ").append(userData.optString("role", "N/A"));
|
||||
userInfo.append("\nPhone: ").append(userData.optString("phone", "N/A"));
|
||||
userInfo.append("\nPosition: ").append(userData.optString("position", "N/A"));
|
||||
userInfo.append("\nMID: ").append(userData.optString("mid", "N/A"));
|
||||
userInfo.append("\nTID: ").append(userData.optString("tid", "N/A"));
|
||||
userInfo.append("\nLast Login: ").append(userData.optString("last_login", "N/A"));
|
||||
userInfo.append("\n==========================");
|
||||
|
||||
Log.i(TAG, userInfo.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void displayUserInfo() {
|
||||
if (userData != null) {
|
||||
String userName = userData.optString("name", "User");
|
||||
String userRole = userData.optString("role", "");
|
||||
String mid = userData.optString("mid", "");
|
||||
String tid = userData.optString("tid", "");
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Update MID and TID
|
||||
TextView tvStoreName = findViewById(R.id.tv_store_name);
|
||||
TextView tvStoreAddress = findViewById(R.id.tv_store_address);
|
||||
TextView tvMid = findViewById(R.id.tv_mid);
|
||||
TextView tvTid = findViewById(R.id.tv_tid);
|
||||
|
||||
if (tvStoreName != null) {
|
||||
String storeName = userData.optString("store_name", "TOKO KLONTONG PAK EKO");
|
||||
tvStoreName.setText(storeName);
|
||||
}
|
||||
|
||||
if (tvStoreAddress != null) {
|
||||
String storeAddress = userData.optString("store_address", "Ciputat Baru, Tangsel");
|
||||
tvStoreAddress.setText(storeAddress);
|
||||
}
|
||||
if (tvMid != null && !mid.isEmpty()) {
|
||||
tvMid.setText("MID: " + mid);
|
||||
}
|
||||
if (tvTid != null && !tid.isEmpty()) {
|
||||
tvTid.setText("TID: " + tid);
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
transactionsCard.setOnClickListener(v -> {
|
||||
// Launch transactions activity
|
||||
startActivity(new android.content.Intent(MainActivity.this, TransactionActivity.class));
|
||||
// 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");
|
||||
}
|
||||
});
|
||||
|
||||
// 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, "==================================");
|
||||
}
|
||||
}
|
||||
197
app/src/main/java/com/example/bdkipoc/MyApplication.java
Normal file
@ -0,0 +1,197 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.IBinder;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import com.example.bdkipoc.emv.EmvTTS;
|
||||
import com.example.bdkipoc.utils.LogUtil;
|
||||
import com.example.bdkipoc.utils.Utility;
|
||||
import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.etc.ETCOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.print.PrinterOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.readcard.ReadCardOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.rfid.RFIDOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.security.BiometricManagerV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.security.DevCertManagerV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.security.NoLostKeyManagerV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.system.BasicOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.tax.TaxOptV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.test.TestOptV2;
|
||||
import com.sunmi.pay.hardware.wrapper.HCEManagerV2Wrapper;
|
||||
import com.sunmi.peripheral.printer.InnerPrinterCallback;
|
||||
import com.sunmi.peripheral.printer.InnerPrinterException;
|
||||
import com.sunmi.peripheral.printer.InnerPrinterManager;
|
||||
import com.sunmi.peripheral.printer.SunmiPrinterService;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import sunmi.paylib.SunmiPayKernel;
|
||||
|
||||
public class MyApplication extends Application {
|
||||
public static MyApplication app;
|
||||
|
||||
public BasicOptV2 basicOptV2; // 获取基础操作模块
|
||||
public ReadCardOptV2 readCardOptV2; // 获取读卡模块
|
||||
public PinPadOptV2 pinPadOptV2; // 获取PinPad操作模块
|
||||
public SecurityOptV2 securityOptV2; // 获取安全操作模块
|
||||
public EMVOptV2 emvOptV2; // 获取EMV操作模块
|
||||
public TaxOptV2 taxOptV2; // 获取税控操作模块
|
||||
public ETCOptV2 etcOptV2; // 获取ETC操作模块
|
||||
public PrinterOptV2 printerOptV2; // 获取打印操作模块
|
||||
public TestOptV2 testOptV2; // 获取测试操作模块
|
||||
public DevCertManagerV2 devCertManagerV2; // 设备证书操作模块
|
||||
public NoLostKeyManagerV2 noLostKeyManagerV2; // NoLostKey操作模块
|
||||
public HCEManagerV2Wrapper hceV2Wrapper; // HCE操作模块
|
||||
public RFIDOptV2 rfidOptV2; // RFID操作模块
|
||||
public SunmiPrinterService sunmiPrinterService; // 打印模块
|
||||
//public IScanInterface scanInterface; // 扫码模块 (commented out)
|
||||
public BiometricManagerV2 mBiometricManagerV2; // 生物特征模块
|
||||
|
||||
private boolean connectPaySDK;//是否已连接PaySDK
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
app = this;
|
||||
initLocaleLanguage();
|
||||
initEmvTTS();
|
||||
bindPrintService();
|
||||
bindPaySDKService();
|
||||
//bindScannerService(); // Commented out scanner service binding
|
||||
}
|
||||
|
||||
public static void initLocaleLanguage() {
|
||||
Resources resources = app.getResources();
|
||||
DisplayMetrics dm = resources.getDisplayMetrics();
|
||||
Configuration config = resources.getConfiguration();
|
||||
int showLanguage = CacheHelper.getCurrentLanguage();
|
||||
if (showLanguage == Constant.LANGUAGE_AUTO) {
|
||||
LogUtil.e(Constant.TAG, config.locale.getCountry() + "---这是系统语言");
|
||||
config.locale = Resources.getSystem().getConfiguration().locale;
|
||||
} else if (showLanguage == Constant.LANGUAGE_ZH_CN) {
|
||||
LogUtil.e(Constant.TAG, "这是中文");
|
||||
config.locale = Locale.SIMPLIFIED_CHINESE;
|
||||
} else if (showLanguage == Constant.LANGUAGE_EN_US) {
|
||||
LogUtil.e(Constant.TAG, "这是英文");
|
||||
config.locale = Locale.ENGLISH;
|
||||
} else if (showLanguage == Constant.LANGUAGE_JA_JP) {
|
||||
LogUtil.e(Constant.TAG, "这是日文");
|
||||
config.locale = Locale.JAPAN;
|
||||
}
|
||||
resources.updateConfiguration(config, dm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
LogUtil.e(Constant.TAG, "onConfigurationChanged");
|
||||
}
|
||||
|
||||
public boolean isConnectPaySDK() {
|
||||
return connectPaySDK;
|
||||
}
|
||||
|
||||
/**
|
||||
* bind PaySDK service
|
||||
*/
|
||||
public void bindPaySDKService() {
|
||||
final SunmiPayKernel payKernel = SunmiPayKernel.getInstance();
|
||||
payKernel.setEmvL2Split(true);
|
||||
payKernel.initPaySDK(this, new SunmiPayKernel.ConnectCallback() {
|
||||
@Override
|
||||
public void onConnectPaySDK() {
|
||||
LogUtil.e(Constant.TAG, "onConnectPaySDK...");
|
||||
emvOptV2 = payKernel.mEMVOptV2;
|
||||
basicOptV2 = payKernel.mBasicOptV2;
|
||||
pinPadOptV2 = payKernel.mPinPadOptV2;
|
||||
readCardOptV2 = payKernel.mReadCardOptV2;
|
||||
securityOptV2 = payKernel.mSecurityOptV2;
|
||||
taxOptV2 = payKernel.mTaxOptV2;
|
||||
etcOptV2 = payKernel.mETCOptV2;
|
||||
printerOptV2 = payKernel.mPrinterOptV2;
|
||||
testOptV2 = payKernel.mTestOptV2;
|
||||
devCertManagerV2 = payKernel.mDevCertManagerV2;
|
||||
noLostKeyManagerV2 = payKernel.mNoLostKeyManagerV2;
|
||||
mBiometricManagerV2 = payKernel.mBiometricManagerV2;
|
||||
hceV2Wrapper = payKernel.mHCEManagerV2Wrapper;
|
||||
rfidOptV2 = payKernel.mRFIDOptV2;
|
||||
connectPaySDK = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnectPaySDK() {
|
||||
LogUtil.e(Constant.TAG, "onDisconnectPaySDK...");
|
||||
connectPaySDK = false;
|
||||
emvOptV2 = null;
|
||||
basicOptV2 = null;
|
||||
pinPadOptV2 = null;
|
||||
readCardOptV2 = null;
|
||||
securityOptV2 = null;
|
||||
taxOptV2 = null;
|
||||
etcOptV2 = null;
|
||||
printerOptV2 = null;
|
||||
devCertManagerV2 = null;
|
||||
noLostKeyManagerV2 = null;
|
||||
mBiometricManagerV2 = null;
|
||||
rfidOptV2 = null;
|
||||
Utility.showToast(R.string.connect_fail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* bind printer service
|
||||
*/
|
||||
private void bindPrintService() {
|
||||
try {
|
||||
InnerPrinterManager.getInstance().bindService(this, new InnerPrinterCallback() {
|
||||
@Override
|
||||
protected void onConnected(SunmiPrinterService service) {
|
||||
sunmiPrinterService = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisconnected() {
|
||||
sunmiPrinterService = null;
|
||||
}
|
||||
});
|
||||
} catch (InnerPrinterException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* bind scanner service (commented out)
|
||||
*/
|
||||
/*
|
||||
public void bindScannerService() {
|
||||
Intent intent = new Intent();
|
||||
intent.setPackage("com.sunmi.scanner");
|
||||
intent.setAction("com.sunmi.scanner.IScanInterface");
|
||||
bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
scanInterface = IScanInterface.Stub.asInterface(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
scanInterface = null;
|
||||
}
|
||||
}, Service.BIND_AUTO_CREATE);
|
||||
}
|
||||
*/
|
||||
|
||||
private void initEmvTTS() {
|
||||
EmvTTS.getInstance().init();
|
||||
}
|
||||
}
|
||||
@ -1,557 +0,0 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PaymentActivity extends AppCompatActivity {
|
||||
|
||||
private ProgressBar progressBar;
|
||||
private Button initiatePaymentButton;
|
||||
private Button simulatePaymentButton;
|
||||
private ImageView qrCodeImageView;
|
||||
private TextView statusTextView;
|
||||
private EditText editTextAmount;
|
||||
private TextView referenceIdTextView;
|
||||
private View paymentDetailsLayout;
|
||||
private View paymentSuccessLayout;
|
||||
private Button returnToMainButton;
|
||||
|
||||
private String transactionId;
|
||||
private String transactionUuid;
|
||||
private String referenceId;
|
||||
private int amount;
|
||||
private JSONObject midtransResponse;
|
||||
|
||||
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
|
||||
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
|
||||
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; // Replace with your actual key
|
||||
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_payment);
|
||||
|
||||
// Set up the toolbar
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
getSupportActionBar().setTitle("QRIS Payment");
|
||||
}
|
||||
|
||||
// Initialize views
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
initiatePaymentButton = findViewById(R.id.initiatePaymentButton);
|
||||
simulatePaymentButton = findViewById(R.id.simulatePaymentButton);
|
||||
qrCodeImageView = findViewById(R.id.qrCodeImageView);
|
||||
statusTextView = findViewById(R.id.statusTextView);
|
||||
editTextAmount = findViewById(R.id.editTextAmount);
|
||||
referenceIdTextView = findViewById(R.id.referenceIdTextView);
|
||||
paymentDetailsLayout = findViewById(R.id.paymentDetailsLayout);
|
||||
paymentSuccessLayout = findViewById(R.id.paymentSuccessLayout);
|
||||
returnToMainButton = findViewById(R.id.returnToMainButton);
|
||||
|
||||
// Generate a random amount between 100,000 and 999,999
|
||||
amount = new Random().nextInt(900000) + 100000;
|
||||
|
||||
// Format and display the amount
|
||||
editTextAmount.setText("");
|
||||
editTextAmount.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(editTextAmount, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
|
||||
// Generate reference ID
|
||||
referenceId = "ref-" + generateRandomString(8);
|
||||
referenceIdTextView.setText(referenceId);
|
||||
|
||||
// Set up click listeners
|
||||
initiatePaymentButton.setOnClickListener(v -> createTransaction());
|
||||
simulatePaymentButton.setOnClickListener(v -> simulateWebhook());
|
||||
returnToMainButton.setOnClickListener(v -> finish());
|
||||
|
||||
// Initially hide the QR code and payment success views
|
||||
paymentDetailsLayout.setVisibility(View.GONE);
|
||||
paymentSuccessLayout.setVisibility(View.GONE);
|
||||
simulatePaymentButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void createTransaction() {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
initiatePaymentButton.setEnabled(false);
|
||||
statusTextView.setText("Creating transaction...");
|
||||
|
||||
new CreateTransactionTask().execute();
|
||||
}
|
||||
|
||||
private void displayQrCode(String qrImageUrl) {
|
||||
new DownloadImageTask().execute(qrImageUrl);
|
||||
}
|
||||
|
||||
private void simulateWebhook() {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
simulatePaymentButton.setEnabled(false);
|
||||
statusTextView.setText("Processing payment...");
|
||||
|
||||
new SimulateWebhookTask().execute();
|
||||
}
|
||||
|
||||
private void showSuccessScreen() {
|
||||
paymentDetailsLayout.setVisibility(View.GONE);
|
||||
paymentSuccessLayout.setVisibility(View.VISIBLE);
|
||||
statusTextView.setText("Payment successful!");
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private String generateRandomString(int length) {
|
||||
String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Random random = new Random();
|
||||
for (int i = 0; i < length; i++) {
|
||||
int index = random.nextInt(chars.length());
|
||||
sb.append(chars.charAt(index));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String getServerKey() {
|
||||
// MIDTRANS_AUTH = 'Basic base64string'
|
||||
String base64 = MIDTRANS_AUTH.replace("Basic ", "");
|
||||
String decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT).toString();
|
||||
// Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present.
|
||||
return decoded.replace(":\n", "");
|
||||
}
|
||||
|
||||
private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
|
||||
String input = orderId + statusCode + grossAmount + serverKey;
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512");
|
||||
byte[] messageDigest = md.digest(input.getBytes());
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : messageDigest) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) hexString.append('0');
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (java.security.NoSuchAlgorithmException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> {
|
||||
private String errorMessage;
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
// Generate a UUID for the transaction
|
||||
transactionUuid = UUID.randomUUID().toString();
|
||||
|
||||
// Create transaction JSON payload
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("type", "PAYMENT");
|
||||
payload.put("channel_category", "RETAIL_OUTLET");
|
||||
payload.put("channel_code", "QRIS");
|
||||
payload.put("reference_id", referenceId);
|
||||
|
||||
// Read amount from EditText and log it
|
||||
String amountText = editTextAmount.getText().toString().trim();
|
||||
Log.d("MidtransCharge", "Raw amount text: " + amountText);
|
||||
|
||||
try {
|
||||
// Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
|
||||
amount = Integer.parseInt(amountText);
|
||||
Log.d("MidtransCharge", "Parsed amount: " + amount);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e("MidtransCharge", "Amount parsing error: " + e.getMessage());
|
||||
errorMessage = "Invalid amount format";
|
||||
return false;
|
||||
}
|
||||
|
||||
payload.put("amount", amount);
|
||||
payload.put("cashflow", "MONEY_IN");
|
||||
payload.put("status", "INIT");
|
||||
payload.put("device_id", 1);
|
||||
payload.put("transaction_uuid", transactionUuid);
|
||||
payload.put("transaction_time_seconds", 0.0);
|
||||
payload.put("device_code", "PB4K252T00021");
|
||||
payload.put("merchant_name", "Marcel Panjaitan");
|
||||
payload.put("mid", "71000026521");
|
||||
payload.put("tid", "73001500");
|
||||
|
||||
// Make the API call
|
||||
URL url = new URI(BACKEND_BASE + "/transactions").toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input = payload.toString().getBytes("utf-8");
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == 200 || responseCode == 201) {
|
||||
// Read the response
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String responseLine;
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
|
||||
// Parse the response to get transaction ID
|
||||
JSONObject jsonResponse = new JSONObject(response.toString());
|
||||
JSONObject data = jsonResponse.getJSONObject("data");
|
||||
transactionId = String.valueOf(data.getInt("id"));
|
||||
|
||||
// Now generate QRIS via Midtrans
|
||||
return generateQris(amount);
|
||||
} else {
|
||||
// Read error response
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String responseLine;
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
errorMessage = "Error creating transaction: " + response.toString();
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
|
||||
errorMessage = "Unexpected error: " + e.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean generateQris(int amount) {
|
||||
try {
|
||||
// Create QRIS charge JSON payload
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("payment_type", "qris");
|
||||
|
||||
JSONObject transactionDetails = new JSONObject();
|
||||
transactionDetails.put("order_id", transactionUuid);
|
||||
transactionDetails.put("gross_amount", amount);
|
||||
payload.put("transaction_details", transactionDetails);
|
||||
|
||||
// Log the request details
|
||||
Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
|
||||
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
|
||||
Log.d("MidtransCharge", "Accept: application/json");
|
||||
Log.d("MidtransCharge", "Content-Type: application/json");
|
||||
Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL);
|
||||
Log.d("MidtransCharge", "Payload: " + payload.toString());
|
||||
|
||||
// Make the API call to Midtrans
|
||||
URL url = new URI(MIDTRANS_CHARGE_URL).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
|
||||
conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL);
|
||||
conn.setDoOutput(true);
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input = payload.toString().getBytes("utf-8");
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == 200 || responseCode == 201) {
|
||||
InputStream inputStream = conn.getInputStream();
|
||||
if (inputStream != null) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String responseLine;
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
midtransResponse = new JSONObject(response.toString());
|
||||
return true;
|
||||
} else {
|
||||
Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
|
||||
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No input stream available";
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
InputStream errorStream = conn.getErrorStream();
|
||||
if (errorStream != null) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8"));
|
||||
StringBuilder errorResponse = new StringBuilder();
|
||||
String responseLine;
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
errorResponse.append(responseLine.trim());
|
||||
}
|
||||
Log.e("MidtransCharge", "HTTP " + responseCode + ": " + errorResponse.toString());
|
||||
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": " + errorResponse.toString();
|
||||
} else {
|
||||
Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available");
|
||||
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
|
||||
errorMessage = "Unexpected error: " + e.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean success) {
|
||||
if (success && midtransResponse != null) {
|
||||
try {
|
||||
// Extract needed values from midtransResponse
|
||||
JSONObject actions = midtransResponse.getJSONArray("actions").getJSONObject(0);
|
||||
String qrImageUrl = actions.getString("url");
|
||||
|
||||
// Extract transaction_id
|
||||
String transactionId = midtransResponse.getString("transaction_id");
|
||||
String transactionTime = midtransResponse.getString("transaction_time");
|
||||
String acquirer = midtransResponse.getString("acquirer");
|
||||
String merchantId = midtransResponse.getString("merchant_id");
|
||||
String exactGrossAmount = midtransResponse.getString("gross_amount");
|
||||
|
||||
// Log everything before launching activity
|
||||
Log.d("MidtransCharge", "Creating QrisResultActivity intent with:");
|
||||
Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl);
|
||||
Log.d("MidtransCharge", "amount: " + amount);
|
||||
Log.d("MidtransCharge", "referenceId: " + referenceId);
|
||||
Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid);
|
||||
Log.d("MidtransCharge", "transaction_id: " + transactionId);
|
||||
Log.d("MidtransCharge", "exactGrossAmount: " + exactGrossAmount);
|
||||
|
||||
// Instead of showing QR inline, launch QrisResultActivity
|
||||
Intent intent = new Intent(PaymentActivity.this, QrisResultActivity.class);
|
||||
intent.putExtra("qrImageUrl", qrImageUrl);
|
||||
intent.putExtra("amount", amount);
|
||||
intent.putExtra("referenceId", referenceId);
|
||||
intent.putExtra("orderId", transactionUuid); // Order ID
|
||||
intent.putExtra("transactionId", transactionId); // Actual Midtrans transaction_id
|
||||
intent.putExtra("grossAmount", exactGrossAmount); // Exact gross amount from response
|
||||
intent.putExtra("transactionTime", transactionTime); // For timestamp
|
||||
intent.putExtra("acquirer", acquirer);
|
||||
intent.putExtra("merchantId", merchantId);
|
||||
|
||||
try {
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e);
|
||||
Toast.makeText(PaymentActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return;
|
||||
} catch (JSONException e) {
|
||||
Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
|
||||
Toast.makeText(PaymentActivity.this, "Error processing QRIS response", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
|
||||
Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
|
||||
initiatePaymentButton.setEnabled(true);
|
||||
}
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
|
||||
@Override
|
||||
protected Bitmap doInBackground(String... urls) {
|
||||
String urlDisplay = urls[0];
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
URL url = new URI(urlDisplay).toURL();
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
java.io.InputStream input = connection.getInputStream();
|
||||
bitmap = android.graphics.BitmapFactory.decodeStream(input);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap result) {
|
||||
if (result != null) {
|
||||
qrCodeImageView.setImageBitmap(result);
|
||||
} else {
|
||||
Toast.makeText(PaymentActivity.this, "Error loading QR code image", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SimulateWebhookTask extends AsyncTask<Void, Void, Boolean> {
|
||||
private String errorMessage;
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
// Wait a moment to simulate real-world timing
|
||||
Thread.sleep(1500);
|
||||
|
||||
// Get server key and prepare signature
|
||||
String serverKey = getServerKey();
|
||||
String grossAmount = String.valueOf(amount) + ".00";
|
||||
String signatureKey = generateSignature(
|
||||
transactionUuid,
|
||||
"200",
|
||||
grossAmount,
|
||||
serverKey
|
||||
);
|
||||
|
||||
// Create webhook payload
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("transaction_type", "on-us");
|
||||
payload.put("transaction_time", midtransResponse.getString("transaction_time"));
|
||||
payload.put("transaction_status", "settlement");
|
||||
payload.put("transaction_id", midtransResponse.getString("transaction_id"));
|
||||
payload.put("status_message", "midtrans payment notification");
|
||||
payload.put("status_code", "200");
|
||||
payload.put("signature_key", signatureKey);
|
||||
payload.put("settlement_time", midtransResponse.getString("transaction_time"));
|
||||
payload.put("payment_type", "qris");
|
||||
payload.put("order_id", transactionUuid);
|
||||
payload.put("merchant_id", midtransResponse.getString("merchant_id"));
|
||||
payload.put("issuer", midtransResponse.getString("acquirer"));
|
||||
payload.put("gross_amount", grossAmount);
|
||||
payload.put("fraud_status", "accept");
|
||||
payload.put("currency", "IDR");
|
||||
payload.put("acquirer", midtransResponse.getString("acquirer"));
|
||||
payload.put("shopeepay_reference_number", "");
|
||||
payload.put("reference_id", referenceId);
|
||||
|
||||
// Call the webhook URL
|
||||
URL url = new URI(WEBHOOK_URL).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input = payload.toString().getBytes("utf-8");
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == 200 || responseCode == 201) {
|
||||
// Wait briefly to allow the backend to process
|
||||
Thread.sleep(2000);
|
||||
return checkTransactionStatus();
|
||||
} else {
|
||||
// Read error response
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String responseLine;
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
errorMessage = "Error simulating payment: " + response.toString();
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errorMessage = "Error: " + e.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkTransactionStatus() {
|
||||
try {
|
||||
// Check transaction status
|
||||
URL url = new URI(BACKEND_BASE + "/transactions/" + transactionId).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == 200) {
|
||||
// Read the response
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String responseLine;
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
JSONObject jsonResponse = new JSONObject(response.toString());
|
||||
JSONObject data = jsonResponse.getJSONObject("data");
|
||||
String status = data.getString("status");
|
||||
|
||||
return status.equalsIgnoreCase("SUCCESS");
|
||||
} else {
|
||||
errorMessage = "Error checking transaction status. HTTP response code: " + responseCode;
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errorMessage = "Error checking transaction status: " + e.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean success) {
|
||||
if (success) {
|
||||
showSuccessScreen();
|
||||
} else {
|
||||
String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
|
||||
Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
|
||||
simulatePaymentButton.setEnabled(true);
|
||||
}
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,287 +0,0 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
public class QrisResultActivity extends AppCompatActivity {
|
||||
private ImageView qrImageView;
|
||||
private TextView amountTextView;
|
||||
private TextView referenceTextView;
|
||||
private Button downloadQrisButton;
|
||||
private Button checkStatusButton;
|
||||
private TextView statusTextView;
|
||||
private Button returnMainButton;
|
||||
private ProgressBar progressBar;
|
||||
private String orderId;
|
||||
private String grossAmount;
|
||||
private String referenceId;
|
||||
private String transactionId;
|
||||
private String transactionTime;
|
||||
private String acquirer;
|
||||
private String merchantId;
|
||||
private String backendBase = "https://be-edc.msvc.app";
|
||||
private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans";
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_qris_result);
|
||||
|
||||
qrImageView = findViewById(R.id.qrImageView);
|
||||
amountTextView = findViewById(R.id.amountTextView);
|
||||
referenceTextView = findViewById(R.id.referenceTextView);
|
||||
downloadQrisButton = findViewById(R.id.downloadQrisButton);
|
||||
checkStatusButton = findViewById(R.id.checkStatusButton);
|
||||
statusTextView = findViewById(R.id.statusTextView);
|
||||
returnMainButton = findViewById(R.id.returnMainButton);
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
|
||||
Intent intent = getIntent();
|
||||
String qrImageUrl = intent.getStringExtra("qrImageUrl");
|
||||
int amount = intent.getIntExtra("amount", 0);
|
||||
referenceId = intent.getStringExtra("referenceId");
|
||||
orderId = intent.getStringExtra("orderId");
|
||||
grossAmount = intent.getStringExtra("grossAmount");
|
||||
transactionId = intent.getStringExtra("transactionId");
|
||||
transactionTime = intent.getStringExtra("transactionTime");
|
||||
acquirer = intent.getStringExtra("acquirer");
|
||||
merchantId = intent.getStringExtra("merchantId");
|
||||
|
||||
if (orderId == null || transactionId == null) {
|
||||
Log.e("QrisResultFlow", "orderId or transactionId is null! Intent extras: " + intent.getExtras());
|
||||
android.widget.Toast.makeText(this, "Missing transaction details!", android.widget.Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
// Get the exact amount from the grossAmount string value instead of the integer
|
||||
String amountStr = "Amount: " + grossAmount;
|
||||
amountTextView.setText(amountStr);
|
||||
referenceTextView.setText("Reference ID: " + referenceId);
|
||||
|
||||
// Load QR image
|
||||
new DownloadImageTask(qrImageView).execute(qrImageUrl);
|
||||
|
||||
// Disable check status button initially
|
||||
checkStatusButton.setEnabled(false);
|
||||
// Start polling for pending payment log
|
||||
pollPendingPaymentLog(orderId);
|
||||
|
||||
// Download QRIS button
|
||||
downloadQrisButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
qrImageView.setDrawingCacheEnabled(true);
|
||||
Bitmap bitmap = qrImageView.getDrawingCache();
|
||||
if (bitmap != null) {
|
||||
saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis());
|
||||
}
|
||||
qrImageView.setDrawingCacheEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Check Payment Status button
|
||||
checkStatusButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
simulateWebhook();
|
||||
}
|
||||
});
|
||||
|
||||
// Return to Main Screen button
|
||||
returnMainButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
finishAffinity();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
|
||||
ImageView bmImage;
|
||||
DownloadImageTask(ImageView bmImage) {
|
||||
this.bmImage = bmImage;
|
||||
}
|
||||
protected Bitmap doInBackground(String... urls) {
|
||||
String urlDisplay = urls[0];
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
URL url = new URI(urlDisplay).toURL();
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
InputStream input = connection.getInputStream();
|
||||
bitmap = BitmapFactory.decodeStream(input);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
protected void onPostExecute(Bitmap result) {
|
||||
if (result != null) {
|
||||
bmImage.setImageBitmap(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save bitmap to gallery
|
||||
private void saveImageToGallery(Bitmap bitmap, String fileName) {
|
||||
try {
|
||||
String savedImageURL = android.provider.MediaStore.Images.Media.insertImage(
|
||||
getContentResolver(), bitmap, fileName, "QRIS Payment QR Code");
|
||||
if (savedImageURL != null) {
|
||||
android.widget.Toast.makeText(this, "QRIS saved to gallery", android.widget.Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
android.widget.Toast.makeText(this, "Failed to save QRIS", android.widget.Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
android.widget.Toast.makeText(this, "Error saving QRIS: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void pollPendingPaymentLog(final String orderId) {
|
||||
Log.d("QrisResultFlow", "Polling for orderId (transaction_uuid): " + orderId);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
new Thread(() -> {
|
||||
int maxAttempts = 10;
|
||||
int intervalMs = 1500;
|
||||
int attempt = 0;
|
||||
boolean found = false;
|
||||
while (attempt < maxAttempts && !found) {
|
||||
try {
|
||||
String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}";
|
||||
Log.d("QrisResultFlow", "Polling URL: " + urlStr);
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == 200) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
JSONObject json = new JSONObject(response.toString());
|
||||
JSONArray results = json.optJSONArray("results");
|
||||
if (results != null && results.length() > 0) {
|
||||
for (int i = 0; i < results.length(); i++) {
|
||||
JSONObject log = results.getJSONObject(i);
|
||||
JSONObject reqBody = log.optJSONObject("request_body");
|
||||
if (reqBody != null && "pending".equals(reqBody.optString("transaction_status"))) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("QrisResultFlow", "Polling error: " + e.getMessage(), e);
|
||||
}
|
||||
if (!found) {
|
||||
attempt++;
|
||||
try { Thread.sleep(intervalMs); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
final boolean logFound = found;
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
if (logFound) {
|
||||
checkStatusButton.setEnabled(true);
|
||||
android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log found!", android.widget.Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log NOT found.", android.widget.Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
// Simulate webhook callback
|
||||
private void simulateWebhook() {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("transaction_type", "on-us");
|
||||
payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
|
||||
payload.put("transaction_status", "settlement");
|
||||
payload.put("transaction_id", transactionId); // Use the actual transaction_id
|
||||
payload.put("status_message", "midtrans payment notification");
|
||||
payload.put("status_code", "200");
|
||||
payload.put("signature_key", "dummy_signature");
|
||||
payload.put("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
|
||||
payload.put("payment_type", "qris");
|
||||
payload.put("order_id", orderId); // Use order_id
|
||||
payload.put("merchant_id", merchantId != null ? merchantId : "DUMMY_MERCHANT_ID");
|
||||
payload.put("issuer", acquirer != null ? acquirer : "gopay");
|
||||
payload.put("gross_amount", grossAmount); // Use exact gross amount
|
||||
payload.put("fraud_status", "accept");
|
||||
payload.put("currency", "IDR");
|
||||
payload.put("acquirer", acquirer != null ? acquirer : "gopay");
|
||||
payload.put("shopeepay_reference_number", "");
|
||||
payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID");
|
||||
Log.d("QrisResultFlow", "Webhook payload: " + payload.toString());
|
||||
|
||||
URL url = new URL(webhookUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
OutputStream os = conn.getOutputStream();
|
||||
os.write(payload.toString().getBytes());
|
||||
os.flush();
|
||||
os.close();
|
||||
int responseCode = conn.getResponseCode();
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(
|
||||
responseCode < 400 ? conn.getInputStream() : conn.getErrorStream()));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
Log.d("QrisResultFlow", "Webhook response: " + response.toString());
|
||||
} catch (Exception e) {
|
||||
Log.e("QrisResultFlow", "Webhook error: " + e.getMessage(), e);
|
||||
}
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
// Proceed to show status/result
|
||||
qrImageView.setVisibility(View.GONE);
|
||||
amountTextView.setVisibility(View.GONE);
|
||||
referenceTextView.setVisibility(View.GONE);
|
||||
downloadQrisButton.setVisibility(View.GONE);
|
||||
checkStatusButton.setVisibility(View.GONE);
|
||||
statusTextView.setVisibility(View.VISIBLE);
|
||||
returnMainButton.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
1463
app/src/main/java/com/example/bdkipoc/ReceiptActivity.java
Normal file
106
app/src/main/java/com/example/bdkipoc/StyleHelper.java
Normal file
@ -0,0 +1,106 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class StyleHelper {
|
||||
|
||||
/**
|
||||
* Create rounded rectangle drawable programmatically
|
||||
*/
|
||||
public static GradientDrawable createRoundedDrawable(int color, int strokeColor, int strokeWidth, int radius) {
|
||||
GradientDrawable drawable = new GradientDrawable();
|
||||
drawable.setShape(GradientDrawable.RECTANGLE);
|
||||
drawable.setColor(color);
|
||||
drawable.setStroke(strokeWidth, strokeColor);
|
||||
drawable.setCornerRadius(radius);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply search input styling
|
||||
*/
|
||||
public static void applySearchInputStyle(View view, Context context) {
|
||||
int white = ContextCompat.getColor(context, android.R.color.white);
|
||||
int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray);
|
||||
// ✅ IMPROVED: Larger corner radius and lighter border like in the image
|
||||
GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border
|
||||
view.setBackground(drawable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter button styling
|
||||
*/
|
||||
public static void applyFilterButtonStyle(View view, Context context) {
|
||||
int white = ContextCompat.getColor(context, android.R.color.white);
|
||||
int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray);
|
||||
// ✅ IMPROVED: Larger corner radius like in the image
|
||||
GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border
|
||||
view.setBackground(drawable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply pagination button styling (simple version)
|
||||
*/
|
||||
public static void applyPaginationButtonStyle(View view, Context context, boolean isActive) {
|
||||
int backgroundColor, strokeColor;
|
||||
|
||||
if (isActive) {
|
||||
backgroundColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
|
||||
strokeColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
|
||||
} else {
|
||||
backgroundColor = ContextCompat.getColor(context, android.R.color.white);
|
||||
strokeColor = ContextCompat.getColor(context, android.R.color.transparent);
|
||||
}
|
||||
|
||||
// ✅ IMPROVED: Larger corner radius for modern look (like in the image)
|
||||
GradientDrawable drawable = createRoundedDrawable(backgroundColor, strokeColor, 0, 48); // 16dp radius
|
||||
view.setBackground(drawable);
|
||||
|
||||
// Set text color if it's a TextView
|
||||
if (view instanceof TextView) {
|
||||
int textColor = isActive ?
|
||||
ContextCompat.getColor(context, android.R.color.white) :
|
||||
ContextCompat.getColor(context, android.R.color.black);
|
||||
((TextView) view).setTextColor(textColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply status text color only (no background badge)
|
||||
*/
|
||||
public static void applyStatusTextColor(TextView textView, Context context, String status) {
|
||||
String statusLower = status != null ? status.toLowerCase() : "";
|
||||
int textColor;
|
||||
|
||||
if (statusLower.equals("failed") || statusLower.equals("failure") ||
|
||||
statusLower.equals("error") || statusLower.equals("declined") ||
|
||||
statusLower.equals("expire") || statusLower.equals("cancel")) {
|
||||
// Red text for failed/error statuses
|
||||
textColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
|
||||
} else if (statusLower.equals("success") || statusLower.equals("paid") ||
|
||||
statusLower.equals("settlement") || statusLower.equals("completed") ||
|
||||
statusLower.equals("capture")) {
|
||||
// Green text for successful statuses
|
||||
textColor = ContextCompat.getColor(context, android.R.color.holo_green_dark);
|
||||
} else if (statusLower.equals("pending") || statusLower.equals("processing") ||
|
||||
statusLower.equals("waiting") || statusLower.equals("checking...") ||
|
||||
statusLower.equals("checking")) {
|
||||
// Orange text for pending/processing statuses
|
||||
textColor = ContextCompat.getColor(context, android.R.color.holo_orange_dark);
|
||||
} else if (statusLower.equals("init")) {
|
||||
// Blue text for init status
|
||||
textColor = ContextCompat.getColor(context, android.R.color.holo_blue_dark);
|
||||
} else {
|
||||
// Default gray text for unknown statuses
|
||||
textColor = ContextCompat.getColor(context, android.R.color.darker_gray);
|
||||
}
|
||||
|
||||
textView.setTextColor(textColor);
|
||||
textView.setBackground(null); // Remove any background
|
||||
}
|
||||
}
|
||||
@ -1,204 +0,0 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TransactionActivity extends AppCompatActivity {
|
||||
private RecyclerView recyclerView;
|
||||
private TransactionAdapter adapter;
|
||||
private ProgressBar progressBar;
|
||||
private FloatingActionButton refreshButton;
|
||||
private int page = 0;
|
||||
private final int limit = 10;
|
||||
private boolean isLoading = false;
|
||||
private boolean isLastPage = false;
|
||||
private List<Transaction> transactionList = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_transaction);
|
||||
|
||||
// Set up the toolbar as the action bar
|
||||
androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
// Enable the back button in the action bar
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerView);
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
refreshButton = findViewById(R.id.refreshButton);
|
||||
|
||||
adapter = new TransactionAdapter(transactionList);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
refreshButton.setOnClickListener(v -> refreshTransactions());
|
||||
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (!recyclerView.canScrollVertically(1) && !isLoading && !isLastPage) {
|
||||
loadTransactions(page + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loadTransactions(0);
|
||||
}
|
||||
|
||||
private void refreshTransactions() {
|
||||
page = 0;
|
||||
isLastPage = false;
|
||||
transactionList.clear();
|
||||
adapter.notifyDataSetChanged();
|
||||
loadTransactions(0);
|
||||
}
|
||||
|
||||
private void loadTransactions(int pageToLoad) {
|
||||
isLoading = true;
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
new FetchTransactionsTask(pageToLoad).execute();
|
||||
}
|
||||
|
||||
private class FetchTransactionsTask extends AsyncTask<Void, Void, List<Transaction>> {
|
||||
private int pageToLoad;
|
||||
private boolean error = false;
|
||||
private int total = 0;
|
||||
|
||||
FetchTransactionsTask(int page) {
|
||||
this.pageToLoad = page;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Transaction> doInBackground(Void... voids) {
|
||||
List<Transaction> result = new ArrayList<>();
|
||||
try {
|
||||
String urlString = "https://be-edc.msvc.app/transactions?page=" + pageToLoad + "&limit=" + limit + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=id";
|
||||
URI uri = new URI(urlString);
|
||||
URL url = uri.toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("accept", "*/*");
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == 200) {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
in.close();
|
||||
JSONObject jsonObject = new JSONObject(response.toString());
|
||||
JSONObject results = jsonObject.getJSONObject("results");
|
||||
total = results.getInt("total");
|
||||
JSONArray data = results.getJSONArray("data");
|
||||
for (int i = 0; i < data.length(); i++) {
|
||||
JSONObject t = data.getJSONObject(i);
|
||||
Transaction tx = new Transaction(
|
||||
t.getInt("id"),
|
||||
t.getString("type"),
|
||||
t.getString("channel_category"),
|
||||
t.getString("channel_code"),
|
||||
t.getString("reference_id"),
|
||||
t.getString("amount"),
|
||||
t.getString("cashflow"),
|
||||
t.getString("status"),
|
||||
t.getString("created_at"),
|
||||
t.getString("merchant_name")
|
||||
);
|
||||
result.add(tx);
|
||||
}
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
} catch (IOException | JSONException | URISyntaxException e) {
|
||||
error = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Transaction> transactions) {
|
||||
isLoading = false;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
if (error) {
|
||||
Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (pageToLoad == 0) {
|
||||
transactionList.clear();
|
||||
}
|
||||
transactionList.addAll(transactions);
|
||||
adapter.notifyDataSetChanged();
|
||||
page = pageToLoad;
|
||||
if (transactionList.size() >= total) {
|
||||
isLastPage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(android.view.MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
// Handle the back button click
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
static class Transaction {
|
||||
int id;
|
||||
String type;
|
||||
String channelCategory;
|
||||
String channelCode;
|
||||
String referenceId;
|
||||
String amount;
|
||||
String cashflow;
|
||||
String status;
|
||||
String createdAt;
|
||||
String merchantName;
|
||||
|
||||
Transaction(int id, String type, String channelCategory, String channelCode, String referenceId, String amount, String cashflow, String status, String createdAt, String merchantName) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.channelCategory = channelCategory;
|
||||
this.channelCode = channelCode;
|
||||
this.referenceId = referenceId;
|
||||
this.amount = amount;
|
||||
this.cashflow = cashflow;
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.merchantName = merchantName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
package com.example.bdkipoc;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.TransactionViewHolder> {
|
||||
private List<TransactionActivity.Transaction> transactionList;
|
||||
|
||||
public TransactionAdapter(List<TransactionActivity.Transaction> transactionList) {
|
||||
this.transactionList = transactionList;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
|
||||
return new TransactionViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
|
||||
TransactionActivity.Transaction t = transactionList.get(position);
|
||||
|
||||
// Format the amount as Indonesian Rupiah
|
||||
try {
|
||||
double amountValue = Double.parseDouble(t.amount);
|
||||
NumberFormat rupiahFormat = NumberFormat.getCurrencyInstance(new Locale.Builder().setLanguage("id").setRegion("ID").build());
|
||||
holder.amount.setText(rupiahFormat.format(amountValue));
|
||||
} catch (NumberFormatException e) {
|
||||
holder.amount.setText("Rp " + t.amount);
|
||||
}
|
||||
|
||||
holder.status.setText(t.status);
|
||||
holder.referenceId.setText(t.referenceId);
|
||||
holder.merchantName.setText(t.merchantName);
|
||||
holder.createdAt.setText(t.createdAt.replace("T", " ").substring(0, 19));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return transactionList.size();
|
||||
}
|
||||
|
||||
static class TransactionViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView amount, status, referenceId, merchantName, createdAt;
|
||||
public TransactionViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
amount = itemView.findViewById(R.id.textAmount);
|
||||
status = itemView.findViewById(R.id.textStatus);
|
||||
referenceId = itemView.findViewById(R.id.textReferenceId);
|
||||
merchantName = itemView.findViewById(R.id.textMerchantName);
|
||||
createdAt = itemView.findViewById(R.id.textCreatedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
145
app/src/main/java/com/example/bdkipoc/emv/EmvTTS.java
Normal file
@ -0,0 +1,145 @@
|
||||
package com.example.bdkipoc.emv;
|
||||
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.UtteranceProgressListener;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.bdkipoc.MyApplication;
|
||||
import com.example.bdkipoc.utils.LogUtil;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class EmvTTS extends UtteranceProgressListener {
|
||||
private static final String TAG = "EmvTTS";
|
||||
private TextToSpeech textToSpeech;
|
||||
private boolean supportTTS;
|
||||
private ITTSProgressListener listener;
|
||||
|
||||
private EmvTTS() {
|
||||
|
||||
}
|
||||
|
||||
public static EmvTTS getInstance() {
|
||||
return SingletonHolder.INSTANCE;
|
||||
}
|
||||
|
||||
public void setTTSListener(ITTSProgressListener l) {
|
||||
listener = l;
|
||||
}
|
||||
|
||||
public void removeTTSListener() {
|
||||
listener = null;
|
||||
}
|
||||
|
||||
private static final class SingletonHolder {
|
||||
private static final EmvTTS INSTANCE = new EmvTTS();
|
||||
}
|
||||
|
||||
public void init() {
|
||||
//初始化TTS对象
|
||||
destroy();
|
||||
textToSpeech = new TextToSpeech(MyApplication.app, this::onTTSInit);
|
||||
textToSpeech.setOnUtteranceProgressListener(this);
|
||||
}
|
||||
|
||||
public void play(String text) {
|
||||
play(text, "0");
|
||||
}
|
||||
|
||||
public void play(String text, String utteranceId) {
|
||||
if (!supportTTS) {
|
||||
Log.e(TAG, "PinPadTTS: play TTS failed, TTS not support...");
|
||||
return;
|
||||
}
|
||||
if (textToSpeech == null) {
|
||||
Log.e(TAG, "PinPadTTS: play TTS slipped, textToSpeech not init..");
|
||||
return;
|
||||
}
|
||||
Log.e(TAG, "play() text: [" + text + "]");
|
||||
textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(String utteranceId) {
|
||||
Log.e(TAG, "播放开始,utteranceId:" + utteranceId);
|
||||
if (listener != null) {
|
||||
listener.onStart(utteranceId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDone(String utteranceId) {
|
||||
Log.e(TAG, "播放结束,utteranceId:" + utteranceId);
|
||||
if (listener != null) {
|
||||
listener.onDone(utteranceId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String utteranceId) {
|
||||
Log.e(TAG, "播放出错,utteranceId:" + utteranceId);
|
||||
if (listener != null) {
|
||||
listener.onError(utteranceId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(String utteranceId, boolean interrupted) {
|
||||
Log.e(TAG, "播放停止,utteranceId:" + utteranceId + ",interrupted:" + interrupted);
|
||||
if (listener != null) {
|
||||
listener.onStop(utteranceId, interrupted);
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (textToSpeech != null) {
|
||||
int code = textToSpeech.stop();
|
||||
Log.e(TAG, "tts stop() code:" + code);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSpeaking() {
|
||||
if (textToSpeech != null) {
|
||||
return textToSpeech.isSpeaking();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (textToSpeech != null) {
|
||||
textToSpeech.stop();
|
||||
textToSpeech.shutdown();
|
||||
textToSpeech = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** TTS初始化回调 */
|
||||
private void onTTSInit(int status) {
|
||||
if (status != TextToSpeech.SUCCESS) {
|
||||
LogUtil.e(TAG, "PinPadTTS: init TTS failed, status:" + status);
|
||||
supportTTS = false;
|
||||
return;
|
||||
}
|
||||
updateTtsLanguage();
|
||||
if (supportTTS) {
|
||||
textToSpeech.setPitch(1.0f);
|
||||
textToSpeech.setSpeechRate(1.0f);
|
||||
LogUtil.e(TAG, "onTTSInit() success,locale:" + textToSpeech.getVoice().getLocale());
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新TTS语言 */
|
||||
private void updateTtsLanguage() {
|
||||
Locale locale = Locale.ENGLISH;
|
||||
int result = textToSpeech.setLanguage(locale);
|
||||
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
supportTTS = false; //系统不支持当前Locale对应的语音播报
|
||||
LogUtil.e(TAG, "updateTtsLanguage() failed, TTS not support in locale:" + locale);
|
||||
} else {
|
||||
supportTTS = true;
|
||||
LogUtil.e(TAG, "updateTtsLanguage() success, TTS locale:" + locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
package com.example.bdkipoc.emv;
|
||||
|
||||
import android.speech.tts.TextToSpeech;
|
||||
|
||||
public interface ITTSProgressListener {
|
||||
|
||||
/**
|
||||
* Called when an utterance "starts" as perceived by the caller. This will
|
||||
* be soon before audio is played back in the case of a {@link TextToSpeech#speak}
|
||||
* or before the first bytes of a file are written to the file system in the case
|
||||
* of {@link TextToSpeech#synthesizeToFile}.
|
||||
*
|
||||
* @param utteranceId The utterance ID of the utterance.
|
||||
*/
|
||||
void onStart(String utteranceId);
|
||||
|
||||
/**
|
||||
* Called when an utterance has successfully completed processing.
|
||||
* All audio will have been played back by this point for audible output, and all
|
||||
* output will have been written to disk for file synthesis requests.
|
||||
* <p>
|
||||
* This request is guaranteed to be called after {@link #onStart(String)}.
|
||||
*
|
||||
* @param utteranceId The utterance ID of the utterance.
|
||||
*/
|
||||
void onDone(String utteranceId);
|
||||
|
||||
/**
|
||||
* Called when an error has occurred during processing. This can be called
|
||||
* at any point in the synthesis process. Note that there might be calls
|
||||
* to {@link #onStart(String)} for specified utteranceId but there will never
|
||||
* be a call to both {@link #onDone(String)} and {@link #onError(String)} for
|
||||
* the same utterance.
|
||||
*
|
||||
* @param utteranceId The utterance ID of the utterance.
|
||||
* @deprecated Use {@link #onError(String, int)} instead
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #onError(String, int)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
void onError(String utteranceId);
|
||||
|
||||
/**
|
||||
* Called when an utterance has been stopped while in progress or flushed from the
|
||||
* synthesis queue. This can happen if a client calls {@link TextToSpeech#stop()}
|
||||
* or uses {@link TextToSpeech#QUEUE_FLUSH} as an argument with the
|
||||
* {@link TextToSpeech#speak} or {@link TextToSpeech#synthesizeToFile} methods.
|
||||
*
|
||||
* @param utteranceId The utterance ID of the utterance.
|
||||
* @param interrupted If true, then the utterance was interrupted while being synthesized
|
||||
* and its output is incomplete. If false, then the utterance was flushed
|
||||
* before the synthesis started.
|
||||
*/
|
||||
void onStop(String utteranceId, boolean interrupted);
|
||||
}
|
||||
@ -0,0 +1,564 @@
|
||||
package com.example.bdkipoc.histori;
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
|
||||
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 java.util.TimeZone;
|
||||
|
||||
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() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
String todayDate = dateFormat.format(new Date());
|
||||
|
||||
// Gunakan BuildConfig untuk base URL
|
||||
API_URL = BuildConfig.BACKEND_BASE_URL + "/transactions?page=0&limit=10&sortOrder=DESC&from_date="
|
||||
+ todayDate + "&to_date=" + todayDate + "&location_id=0&merchant_id=0&tid=&mid=&sortColumn=id";
|
||||
|
||||
SUMMARY_API_URL = BuildConfig.BACKEND_BASE_URL + "/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.toUpperCase()); // Ensure uppercase for consistency
|
||||
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");
|
||||
String status = item.getString("status");
|
||||
|
||||
// Only count if status is settlement or success
|
||||
if ("SETTLEMENT".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)) {
|
||||
// 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());
|
||||
inputFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
SimpleDateFormat outputFormat = new SimpleDateFormat("HH.mm.ss", Locale.getDefault());
|
||||
outputFormat.setTimeZone(TimeZone.getDefault());
|
||||
|
||||
Date date = inputFormat.parse(isoDate);
|
||||
return outputFormat.format(date);
|
||||
} catch (ParseException e) {
|
||||
return "00.00.00";
|
||||
}
|
||||
}
|
||||
|
||||
private String formatDate(String isoDate) {
|
||||
try {
|
||||
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
||||
inputFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
|
||||
outputFormat.setTimeZone(TimeZone.getDefault());
|
||||
|
||||
Date date = inputFormat.parse(isoDate);
|
||||
return outputFormat.format(date);
|
||||
} catch (ParseException e) {
|
||||
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(":", ".") + ", " + item.getDate();
|
||||
holder.tvTime.setText(formattedTime);
|
||||
|
||||
holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
|
||||
holder.tvChannel.setText(item.getChannelName());
|
||||
|
||||
// Set status color and text based on API response
|
||||
String status = item.getStatus();
|
||||
if ("SETTLEMENT".equalsIgnoreCase(status)) {
|
||||
holder.tvStatus.setText("Success");
|
||||
holder.tvStatus.setTextColor(Color.parseColor("#4CAF50")); // Green
|
||||
} else if ("SUCCESS".equalsIgnoreCase(status)) {
|
||||
holder.tvStatus.setText("Success");
|
||||
holder.tvStatus.setTextColor(Color.parseColor("#4CAF50")); // Green
|
||||
} else if ("EXPIRE".equalsIgnoreCase(status)) {
|
||||
holder.tvStatus.setText("Expired");
|
||||
holder.tvStatus.setTextColor(Color.parseColor("#F44336")); // Red
|
||||
} else if ("FAILED".equalsIgnoreCase(status)) {
|
||||
holder.tvStatus.setText("Failed");
|
||||
holder.tvStatus.setTextColor(Color.parseColor("#F44336")); // Red
|
||||
} else {
|
||||
holder.tvStatus.setText("Pending");
|
||||
holder.tvStatus.setTextColor(Color.parseColor("#3141FF")); // Blue
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return historyList != null ? historyList.size() : 0;
|
||||
}
|
||||
|
||||
private String formatCurrency(long amount) {
|
||||
try {
|
||||
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||
return formatter.format(amount);
|
||||
} catch (Exception e) {
|
||||
return String.valueOf(amount);
|
||||
}
|
||||
}
|
||||
|
||||
static class HistoryViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvTime;
|
||||
TextView tvAmount;
|
||||
TextView tvChannel;
|
||||
TextView tvStatus;
|
||||
|
||||
public HistoryViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvTime = itemView.findViewById(R.id.tv_time);
|
||||
tvAmount = itemView.findViewById(R.id.tv_amount);
|
||||
tvChannel = itemView.findViewById(R.id.tv_channel);
|
||||
tvStatus = itemView.findViewById(R.id.tv_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,338 @@
|
||||
package com.example.bdkipoc.histori;
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
|
||||
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;
|
||||
import java.util.TimeZone;
|
||||
|
||||
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() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
String currentDate = dateFormat.format(new Date());
|
||||
|
||||
// Gunakan BuildConfig untuk base URL
|
||||
API_URL = BuildConfig.BACKEND_BASE_URL + "/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 with timezone conversion
|
||||
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
||||
inputFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
SimpleDateFormat outputFormat = new SimpleDateFormat("HH:mm.ss, dd MMM yyyy", Locale.getDefault());
|
||||
outputFormat.setTimeZone(TimeZone.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
901
app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java
Normal file
@ -0,0 +1,901 @@
|
||||
package com.example.bdkipoc;
|
||||
import com.example.bdkipoc.qris.view.QrisResultActivity;
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
|
||||
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 transactionDate;
|
||||
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 MIDTRANS_AUTH = BuildConfig.MIDTRANS_SANDBOX_AUTH;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
private String getCurrentDateTime() {
|
||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", new java.util.Locale("id", "ID"));
|
||||
return sdf.format(new java.util.Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 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 (any positive amount)
|
||||
try {
|
||||
int amt = Integer.parseInt(amountStr);
|
||||
initiatePaymentButton.setEnabled(amt > 0); // Changed from >= 1000 to > 0
|
||||
} 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_date", getCurrentDateTime());
|
||||
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(BuildConfig.BACKEND_BASE_URL + "/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: " + BuildConfig.MIDTRANS_CHARGE_URL);
|
||||
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
|
||||
Log.d("MidtransCharge", "X-Override-Notification: " + BuildConfig.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(BuildConfig.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", BuildConfig.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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,289 @@
|
||||
package com.example.bdkipoc.qris.model;
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
|
||||
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", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("shopee", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("airpay shopee", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("gopay", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("dana", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("ovo", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("linkaja", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("link aja", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("jenius", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("qris", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
put("others", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
|
||||
}};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,411 @@
|
||||
package com.example.bdkipoc.qris.network;
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
|
||||
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 = BuildConfig.MIDTRANS_SANDBOX_AUTH;
|
||||
private static final String MIDTRANS_PRODUCTION_AUTH = BuildConfig.MIDTRANS_PRODUCTION_AUTH;
|
||||
private static final String MIDTRANS_AUTH = BuildConfig.MIDTRANS_SANDBOX_AUTH;
|
||||
private static final String MIDTRANS_CHARGE_URL = BuildConfig.MIDTRANS_CHARGE_URL;
|
||||
private static final String MIDTRANS_STATUS_BASE_URL = BuildConfig.MIDTRANS_STATUS_BASE_URL;
|
||||
|
||||
private String backendBase = BuildConfig.BACKEND_BASE_URL;
|
||||
private String webhookUrl = BuildConfig.WEBHOOK_URL;
|
||||
|
||||
/**
|
||||
* 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(1000);
|
||||
conn.setReadTimeout(1000);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,544 @@
|
||||
package com.example.bdkipoc.qris.presenter;
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
|
||||
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 = BuildConfig.MAX_REFRESH_ATTEMPTS;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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(1000);
|
||||
connection.setReadTimeout(1000);
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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("yyyy-MM-dd HH:mm:ss", 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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,381 @@
|
||||
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 android.widget.LinearLayout;
|
||||
import android.widget.Button;
|
||||
import android.content.Intent;
|
||||
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.SimpleDateFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
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 LinearLayout backNavigation;
|
||||
private Button btnContinue;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settlement);
|
||||
|
||||
initViews();
|
||||
setupRecyclerView();
|
||||
fetchApiData();
|
||||
setupClickListeners();
|
||||
}
|
||||
|
||||
private void fetchApiData() {
|
||||
// Get current date in yyyy-MM-dd format
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
String currentDate = sdf.format(new Date());
|
||||
|
||||
// Build API URL with current date and credentials from BuildConfig
|
||||
String apiUrl = BuildConfig.BACKEND_BASE_URL + "/transactions/performa-chanel-pembayaran" +
|
||||
"?from_date=" + currentDate +
|
||||
"&to_date=" + currentDate +
|
||||
"&location_id=0&merchant_id=0";
|
||||
|
||||
// Execute network call in background thread
|
||||
new ApiTask().execute(apiUrl);
|
||||
}
|
||||
|
||||
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);
|
||||
backNavigation = findViewById(R.id.back_navigation);
|
||||
btnContinue = findViewById(R.id.btn_continue);
|
||||
|
||||
settlementList = new ArrayList<>();
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SettlementAdapter(settlementList);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void setupClickListeners() {
|
||||
// Updated to use backNavigation instead of btnBack
|
||||
backNavigation.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
btnContinue.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(SettlementActivity.this, SettlementDetailActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Add authorization header if needed
|
||||
// connection.setRequestProperty("Authorization", BuildConfig.MIDTRANS_SANDBOX_AUTH);
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
reader.close();
|
||||
return response.toString();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
if (result != null) {
|
||||
try {
|
||||
JSONObject jsonResponse = new JSONObject(result);
|
||||
if (jsonResponse.getInt("status") == 200) {
|
||||
JSONArray dataArray = jsonResponse.getJSONArray("data");
|
||||
processApiData(dataArray);
|
||||
} else {
|
||||
Toast.makeText(SettlementActivity.this, "API Error", Toast.LENGTH_SHORT).show();
|
||||
loadSampleData();
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(SettlementActivity.this, "JSON Parse Error", Toast.LENGTH_SHORT).show();
|
||||
loadSampleData();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(SettlementActivity.this, "Network Error", Toast.LENGTH_SHORT).show();
|
||||
loadSampleData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SettlementItem class - combined in same file
|
||||
class SettlementItem {
|
||||
private String channelName;
|
||||
private long amount;
|
||||
private int transactionCount;
|
||||
private int iconResource;
|
||||
|
||||
public SettlementItem() {}
|
||||
|
||||
public SettlementItem(String channelName, long amount, int transactionCount, int iconResource) {
|
||||
this.channelName = channelName;
|
||||
this.amount = amount;
|
||||
this.transactionCount = transactionCount;
|
||||
this.iconResource = iconResource;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getChannelName() { return channelName; }
|
||||
public void setChannelName(String channelName) { this.channelName = channelName; }
|
||||
public long getAmount() { return amount; }
|
||||
public void setAmount(long amount) { this.amount = amount; }
|
||||
public int getTransactionCount() { return transactionCount; }
|
||||
public void setTransactionCount(int transactionCount) { this.transactionCount = transactionCount; }
|
||||
public int getIconResource() { return iconResource; }
|
||||
public void setIconResource(int iconResource) { this.iconResource = iconResource; }
|
||||
}
|
||||
|
||||
// SettlementAdapter class - combined in same file
|
||||
class SettlementAdapter extends RecyclerView.Adapter<SettlementAdapter.SettlementViewHolder> {
|
||||
|
||||
private List<SettlementItem> settlementList;
|
||||
|
||||
public SettlementAdapter(List<SettlementItem> settlementList) {
|
||||
this.settlementList = settlementList;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SettlementViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_settlement, parent, false);
|
||||
return new SettlementViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SettlementViewHolder holder, int position) {
|
||||
SettlementItem item = settlementList.get(position);
|
||||
|
||||
holder.ivIcon.setImageResource(item.getIconResource());
|
||||
holder.tvChannelName.setText(item.getChannelName());
|
||||
holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
|
||||
holder.tvTransactionCount.setText(item.getTransactionCount() + " Transaksi");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return settlementList.size();
|
||||
}
|
||||
|
||||
private String formatCurrency(long amount) {
|
||||
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
static class SettlementViewHolder extends RecyclerView.ViewHolder {
|
||||
ImageView ivIcon;
|
||||
TextView tvChannelName;
|
||||
TextView tvAmount;
|
||||
TextView tvTransactionCount;
|
||||
|
||||
public SettlementViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
ivIcon = itemView.findViewById(R.id.iv_icon);
|
||||
tvChannelName = itemView.findViewById(R.id.tv_channel_name);
|
||||
tvAmount = itemView.findViewById(R.id.tv_amount);
|
||||
tvTransactionCount = itemView.findViewById(R.id.tv_transaction_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,403 @@
|
||||
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.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.example.bdkipoc.BuildConfig;
|
||||
import com.example.bdkipoc.LoginActivity;
|
||||
import com.example.bdkipoc.R;
|
||||
|
||||
public class SettlementDetailActivity extends AppCompatActivity {
|
||||
|
||||
private TextView tvStoreName, tvStoreLocation, tvMid, tvTid;
|
||||
private TextView tvSettlementDate, tvSettlementTime;
|
||||
private TextView tvTotalMasuk, tvTotalKeluar, tvBiayaAdmin, tvGrandTotal;
|
||||
private RecyclerView recyclerView;
|
||||
private SettlementDetailAdapter adapter;
|
||||
private List<SettlementDetailItem> settlementDetailList;
|
||||
private LinearLayout backNavigation;
|
||||
private Button btnSendSettlement;
|
||||
|
||||
// Summary totals
|
||||
private long totalMasuk = 0; // Set to 0
|
||||
private long totalKeluar = 0;
|
||||
private long biayaAdmin = 0; // Set to 0
|
||||
private long grandTotal = 0;
|
||||
|
||||
// User data
|
||||
private JSONObject userData;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settlement_detail);
|
||||
|
||||
// Load user data from login session
|
||||
loadUserData();
|
||||
|
||||
initViews();
|
||||
setupRecyclerView();
|
||||
fetchApiData();
|
||||
setupClickListeners();
|
||||
updateDateTime();
|
||||
}
|
||||
|
||||
private void loadUserData() {
|
||||
userData = LoginActivity.getUserDataAsJson(this);
|
||||
}
|
||||
|
||||
private void fetchApiData() {
|
||||
// Get current date in yyyy-MM-dd format
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
String currentDate = sdf.format(new Date());
|
||||
|
||||
// Build API URL with current date
|
||||
String apiUrl = BuildConfig.BACKEND_BASE_URL + "/transactions/performa-chanel-pembayaran" +
|
||||
"?from_date=" + currentDate +
|
||||
"&to_date=" + currentDate +
|
||||
"&location_id=0&merchant_id=0";
|
||||
|
||||
new ApiTask().execute(apiUrl);
|
||||
}
|
||||
|
||||
private void processApiData(JSONArray dataArray) {
|
||||
try {
|
||||
settlementDetailList.clear();
|
||||
|
||||
long totalAmount = 0;
|
||||
|
||||
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");
|
||||
|
||||
String displayName = formatChannelName(channelCode);
|
||||
|
||||
settlementDetailList.add(new SettlementDetailItem(
|
||||
displayName,
|
||||
maxAmount,
|
||||
transactions
|
||||
));
|
||||
|
||||
totalAmount += maxAmount;
|
||||
}
|
||||
|
||||
calculateTotalsFromApiData(totalAmount);
|
||||
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.notifyDataSetChanged();
|
||||
updateSummary();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(SettlementDetailActivity.this, "Error parsing data", Toast.LENGTH_SHORT).show();
|
||||
loadSampleData();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateTotalsFromApiData(long totalAmount) {
|
||||
totalMasuk = 0; // Set to 0 temporarily
|
||||
totalKeluar = totalAmount;
|
||||
biayaAdmin = 0; // Set to 0 temporarily
|
||||
grandTotal = totalKeluar - totalMasuk - biayaAdmin;
|
||||
}
|
||||
|
||||
private String formatChannelName(String channelCode) {
|
||||
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";
|
||||
case "CREDIT": return "Kartu Kredit";
|
||||
case "TRANSFER": return "Transfer";
|
||||
case "E_MONEY": return "Uang Elektronik";
|
||||
case "CASH_DEPOSIT": return "Setoran Tunai";
|
||||
case "BILL_PAYMENT": return "Pembayaran Tagihan";
|
||||
case "CASH_WITHDRAWAL": return "Penarikan Tunai";
|
||||
case "TOP_UP": return "Top-up Saldo";
|
||||
case "REFUND": return "Refund (void)";
|
||||
default:
|
||||
return channelCode.substring(0, 1).toUpperCase() +
|
||||
channelCode.substring(1).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
tvStoreName = findViewById(R.id.tv_store_name);
|
||||
tvStoreLocation = findViewById(R.id.tv_store_location);
|
||||
tvMid = findViewById(R.id.tv_mid);
|
||||
tvTid = findViewById(R.id.tv_tid);
|
||||
|
||||
tvSettlementDate = findViewById(R.id.tv_settlement_date);
|
||||
tvSettlementTime = findViewById(R.id.tv_settlement_time);
|
||||
|
||||
tvTotalMasuk = findViewById(R.id.tv_total_masuk);
|
||||
tvTotalKeluar = findViewById(R.id.tv_total_keluar);
|
||||
tvBiayaAdmin = findViewById(R.id.tv_biaya_admin);
|
||||
tvGrandTotal = findViewById(R.id.tv_grand_total);
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_settlement_details);
|
||||
backNavigation = findViewById(R.id.back_navigation);
|
||||
btnSendSettlement = findViewById(R.id.btn_send_settlement);
|
||||
|
||||
settlementDetailList = new ArrayList<>();
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SettlementDetailAdapter(settlementDetailList);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void loadSampleData() {
|
||||
settlementDetailList.clear();
|
||||
|
||||
settlementDetailList.add(new SettlementDetailItem("Kartu Kredit", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Kartu Debit", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("QRIS", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Transfer", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Uang Elektronik", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Setoran Tunai", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Pembayaran Tagihan", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Penarikan Tunai", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Top-up Saldo", 200000, 14));
|
||||
settlementDetailList.add(new SettlementDetailItem("Refund (void)", 200000, 14));
|
||||
|
||||
totalMasuk = 0; // Set to 0 temporarily
|
||||
totalKeluar = 2000000; // Total from sample data
|
||||
biayaAdmin = 0; // Set to 0 temporarily
|
||||
grandTotal = totalKeluar - totalMasuk - biayaAdmin;
|
||||
|
||||
adapter.notifyDataSetChanged();
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
private void updateSummary() {
|
||||
tvTotalMasuk.setText("Rp " + formatCurrency(totalMasuk));
|
||||
tvTotalKeluar.setText("Rp " + formatCurrency(totalKeluar));
|
||||
tvBiayaAdmin.setText("Rp " + formatCurrency(biayaAdmin));
|
||||
tvGrandTotal.setText(formatCurrency(grandTotal));
|
||||
}
|
||||
|
||||
private void updateDateTime() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", new Locale("id", "ID"));
|
||||
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||
|
||||
Date now = new Date();
|
||||
tvSettlementDate.setText(dateFormat.format(now));
|
||||
tvSettlementTime.setText(timeFormat.format(now));
|
||||
|
||||
// Set store info from user data
|
||||
if (userData != null) {
|
||||
String storeName = userData.optString("store_name", "TOKO KLONTONG PAK EKO");
|
||||
String storeAddress = userData.optString("store_address", "Ciputat Baru, Tangsel");
|
||||
String mid = userData.optString("mid", "12345678901");
|
||||
String tid = userData.optString("tid", "12345678901");
|
||||
|
||||
tvStoreName.setText(storeName);
|
||||
tvStoreLocation.setText(storeAddress);
|
||||
tvMid.setText(mid);
|
||||
tvTid.setText(tid);
|
||||
} else {
|
||||
// Fallback to default values
|
||||
tvStoreName.setText("TOKO KLONTONG PAK EKO");
|
||||
tvStoreLocation.setText("Ciputat Baru, Tangsel");
|
||||
tvMid.setText("12345678901");
|
||||
tvTid.setText("12345678901");
|
||||
}
|
||||
}
|
||||
|
||||
private void setupClickListeners() {
|
||||
backNavigation.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
btnSendSettlement.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendSettlement();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendSettlement() {
|
||||
Toast.makeText(this, "Mengirim settlement...", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// TODO: Implement actual settlement sending logic
|
||||
|
||||
Toast.makeText(this, "Settlement berhasil dikirim!", Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
private String formatCurrency(long amount) {
|
||||
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
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(SettlementDetailActivity.this, "API Error", Toast.LENGTH_SHORT).show();
|
||||
loadSampleData();
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(SettlementDetailActivity.this, "JSON Parse Error", Toast.LENGTH_SHORT).show();
|
||||
loadSampleData();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(SettlementDetailActivity.this, "Network Error", Toast.LENGTH_SHORT).show();
|
||||
loadSampleData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SettlementDetailItem {
|
||||
private String paymentMethod;
|
||||
private long totalNominal;
|
||||
private int jumlahTransaksi;
|
||||
|
||||
public SettlementDetailItem(String paymentMethod, long totalNominal, int jumlahTransaksi) {
|
||||
this.paymentMethod = paymentMethod;
|
||||
this.totalNominal = totalNominal;
|
||||
this.jumlahTransaksi = jumlahTransaksi;
|
||||
}
|
||||
|
||||
public String getPaymentMethod() { return paymentMethod; }
|
||||
public long getTotalNominal() { return totalNominal; }
|
||||
public int getJumlahTransaksi() { return jumlahTransaksi; }
|
||||
|
||||
public void setPaymentMethod(String paymentMethod) { this.paymentMethod = paymentMethod; }
|
||||
public void setTotalNominal(long totalNominal) { this.totalNominal = totalNominal; }
|
||||
public void setJumlahTransaksi(int jumlahTransaksi) { this.jumlahTransaksi = jumlahTransaksi; }
|
||||
}
|
||||
|
||||
class SettlementDetailAdapter extends RecyclerView.Adapter<SettlementDetailAdapter.SettlementDetailViewHolder> {
|
||||
|
||||
private List<SettlementDetailItem> settlementDetailList;
|
||||
|
||||
public SettlementDetailAdapter(List<SettlementDetailItem> settlementDetailList) {
|
||||
this.settlementDetailList = settlementDetailList;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SettlementDetailViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_settlement_detail, parent, false);
|
||||
return new SettlementDetailViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SettlementDetailViewHolder holder, int position) {
|
||||
SettlementDetailItem item = settlementDetailList.get(position);
|
||||
|
||||
holder.tvPaymentMethod.setText(item.getPaymentMethod());
|
||||
holder.tvTotalNominal.setText(formatCurrency(item.getTotalNominal()));
|
||||
holder.tvJumlahTransaksi.setText(String.valueOf(item.getJumlahTransaksi()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return settlementDetailList.size();
|
||||
}
|
||||
|
||||
private String formatCurrency(long amount) {
|
||||
NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
static class SettlementDetailViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvPaymentMethod;
|
||||
TextView tvTotalNominal;
|
||||
TextView tvJumlahTransaksi;
|
||||
|
||||
public SettlementDetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvPaymentMethod = itemView.findViewById(R.id.tv_payment_method);
|
||||
tvTotalNominal = itemView.findViewById(R.id.tv_total_nominal);
|
||||
tvJumlahTransaksi = itemView.findViewById(R.id.tv_jumlah_transaksi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,258 @@
|
||||
package com.example.bdkipoc.transaction.managers;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.bdkipoc.MyApplication;
|
||||
import com.sunmi.pay.hardware.aidl.AidlConstants.CardType;
|
||||
import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2;
|
||||
|
||||
/**
|
||||
* CardScannerManager - Handles card detection for both EMV and Simple modes
|
||||
*/
|
||||
public class CardScannerManager {
|
||||
private static final String TAG = "CardScannerManager";
|
||||
|
||||
private CardScannerCallback callback;
|
||||
private boolean isProcessing = false;
|
||||
|
||||
public interface CardScannerCallback {
|
||||
void onCardDetected(String cardType, Bundle cardData);
|
||||
void onEMVCardDetected(int cardType);
|
||||
void onScanError(String errorMessage);
|
||||
void onScanProgress(String message);
|
||||
}
|
||||
|
||||
public CardScannerManager(CardScannerCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void startScanning(boolean isEMVMode) {
|
||||
if (isProcessing) {
|
||||
Log.d(TAG, "Card check already in progress - ignoring call");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Starting card check - setting isProcessing = true");
|
||||
isProcessing = true;
|
||||
|
||||
try {
|
||||
// Small delay to ensure everything is ready
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
if (isProcessing) {
|
||||
if (isEMVMode) {
|
||||
startEMVCardCheck();
|
||||
} else {
|
||||
startSimpleCardCheck();
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error in startScanning: " + e.getMessage(), e);
|
||||
handleScanError("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void stopScanning() {
|
||||
try {
|
||||
if (MyApplication.app != null && MyApplication.app.readCardOptV2 != null) {
|
||||
MyApplication.app.readCardOptV2.cancelCheckCard();
|
||||
}
|
||||
isProcessing = false;
|
||||
Log.d(TAG, "Card scanning stopped");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error stopping card check: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isScanning() {
|
||||
return isProcessing;
|
||||
}
|
||||
|
||||
private void startEMVCardCheck() {
|
||||
try {
|
||||
if (callback != null) {
|
||||
callback.onScanProgress("EMV Mode: Starting card scan...");
|
||||
}
|
||||
|
||||
int cardType = AidlConstantsV2.CardType.NFC.getValue() | AidlConstantsV2.CardType.IC.getValue();
|
||||
Log.d(TAG, "Starting EMV checkCard with cardType: " + cardType);
|
||||
|
||||
MyApplication.app.readCardOptV2.checkCard(cardType, mEMVCheckCardCallback, 60);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error in startEMVCardCheck: " + e.getMessage());
|
||||
handleScanError("Error starting EMV card scan: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void startSimpleCardCheck() {
|
||||
try {
|
||||
if (!MyApplication.app.isConnectPaySDK()) {
|
||||
if (callback != null) {
|
||||
callback.onScanProgress("Connecting to PaySDK...");
|
||||
}
|
||||
MyApplication.app.bindPaySDKService();
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback.onScanProgress("Simple Mode: Starting card scan...");
|
||||
}
|
||||
|
||||
int cardType = CardType.MAGNETIC.getValue() | CardType.IC.getValue() | CardType.NFC.getValue();
|
||||
|
||||
MyApplication.app.readCardOptV2.checkCard(cardType, mSimpleCheckCardCallback, 60);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
handleScanError("Error starting card scan: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleScanError(String errorMessage) {
|
||||
Log.e(TAG, "Scan error: " + errorMessage);
|
||||
isProcessing = false;
|
||||
|
||||
if (callback != null) {
|
||||
callback.onScanError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public void resetScanning() {
|
||||
Log.d(TAG, "Resetting scanning state");
|
||||
isProcessing = false;
|
||||
}
|
||||
|
||||
// Simple Card Detection Callback
|
||||
private final CheckCardCallbackV2 mSimpleCheckCardCallback = new CheckCardCallbackV2.Stub() {
|
||||
@Override
|
||||
public void findMagCard(Bundle info) throws RemoteException {
|
||||
Log.d(TAG, "Simple Mode: findMagCard callback triggered");
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onCardDetected("MAGNETIC", info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findICCard(String atr) throws RemoteException {
|
||||
Bundle info = new Bundle();
|
||||
info.putString("atr", atr);
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onCardDetected("IC", info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findRFCard(String uuid) throws RemoteException {
|
||||
Bundle info = new Bundle();
|
||||
info.putString("uuid", uuid);
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onCardDetected("NFC", info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int code, String message) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onScanError("Card error: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findICCardEx(Bundle info) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onCardDetected("IC", info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findRFCardEx(Bundle info) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onCardDetected("NFC", info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onErrorEx(Bundle info) throws RemoteException {
|
||||
isProcessing = false;
|
||||
String msg = info.getString("message", "Unknown error");
|
||||
if (callback != null) {
|
||||
callback.onScanError("Card error: " + msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// EMV Card Detection Callback
|
||||
private final CheckCardCallbackV2 mEMVCheckCardCallback = new CheckCardCallbackV2.Stub() {
|
||||
@Override
|
||||
public void findMagCard(Bundle info) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onCardDetected("MAGNETIC", info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findICCard(String atr) throws RemoteException {
|
||||
MyApplication.app.basicOptV2.buzzerOnDevice(1, 2750, 200, 0);
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onEMVCardDetected(AidlConstantsV2.CardType.IC.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findRFCard(String uuid) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onEMVCardDetected(AidlConstantsV2.CardType.NFC.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int code, String message) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onScanError("EMV Error: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findICCardEx(Bundle info) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onEMVCardDetected(AidlConstantsV2.CardType.IC.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findRFCardEx(Bundle info) throws RemoteException {
|
||||
isProcessing = false;
|
||||
if (callback != null) {
|
||||
callback.onEMVCardDetected(AidlConstantsV2.CardType.NFC.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onErrorEx(Bundle info) throws RemoteException {
|
||||
isProcessing = false;
|
||||
String msg = info.getString("message", "Unknown error");
|
||||
if (callback != null) {
|
||||
callback.onScanError("EMV Error: " + msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
package com.example.bdkipoc.transaction.managers;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.bdkipoc.R;
|
||||
|
||||
/**
|
||||
* ModalManager - Handles modal UI operations
|
||||
*/
|
||||
public class ModalManager {
|
||||
private static final String TAG = "ModalManager";
|
||||
|
||||
private FrameLayout modalOverlay;
|
||||
private TextView modalText;
|
||||
private ImageView modalIcon;
|
||||
private Animation fadeIn;
|
||||
private Animation fadeOut;
|
||||
private boolean isModalShowing = false;
|
||||
|
||||
public ModalManager(FrameLayout modalOverlay, TextView modalText, ImageView modalIcon) {
|
||||
this.modalOverlay = modalOverlay;
|
||||
this.modalText = modalText;
|
||||
this.modalIcon = modalIcon;
|
||||
initAnimations();
|
||||
}
|
||||
|
||||
private void initAnimations() {
|
||||
fadeIn = AnimationUtils.loadAnimation(modalOverlay.getContext(), android.R.anim.fade_in);
|
||||
fadeOut = AnimationUtils.loadAnimation(modalOverlay.getContext(), android.R.anim.fade_out);
|
||||
|
||||
fadeIn.setDuration(300);
|
||||
fadeOut.setDuration(300);
|
||||
}
|
||||
|
||||
public void showScanCardModal() {
|
||||
if (isModalShowing) return;
|
||||
|
||||
modalOverlay.post(() -> {
|
||||
modalText.setText("Silakan Tempelkan / Gesekkan / Masukkan Kartu ke Perangkat");
|
||||
modalIcon.setImageResource(R.drawable.ic_card_insert);
|
||||
|
||||
modalOverlay.setVisibility(View.VISIBLE);
|
||||
modalOverlay.startAnimation(fadeIn);
|
||||
|
||||
isModalShowing = true;
|
||||
Log.d(TAG, "Modal scan card shown");
|
||||
});
|
||||
}
|
||||
|
||||
public void showProcessingModal(String message) {
|
||||
if (!isModalShowing) {
|
||||
modalOverlay.post(() -> {
|
||||
modalText.setText(message);
|
||||
modalIcon.setImageResource(R.drawable.ic_card_insert);
|
||||
|
||||
modalOverlay.setVisibility(View.VISIBLE);
|
||||
modalOverlay.startAnimation(fadeIn);
|
||||
|
||||
isModalShowing = true;
|
||||
Log.d(TAG, "Modal processing shown: " + message);
|
||||
});
|
||||
} else {
|
||||
// Just update text if modal already showing
|
||||
modalOverlay.post(() -> {
|
||||
modalText.setText(message);
|
||||
Log.d(TAG, "Modal text updated: " + message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void hideModal() {
|
||||
if (!isModalShowing) return;
|
||||
|
||||
modalOverlay.post(() -> {
|
||||
fadeOut.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
modalOverlay.setVisibility(View.GONE);
|
||||
isModalShowing = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
|
||||
modalOverlay.startAnimation(fadeOut);
|
||||
Log.d(TAG, "Modal hidden");
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return isModalShowing;
|
||||
}
|
||||
|
||||
public void updateText(String text) {
|
||||
if (isModalShowing) {
|
||||
modalOverlay.post(() -> {
|
||||
modalText.setText(text);
|
||||
Log.d(TAG, "Modal text updated: " + text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void updateIcon(int iconResource) {
|
||||
if (isModalShowing) {
|
||||
modalOverlay.post(() -> {
|
||||
modalIcon.setImageResource(iconResource);
|
||||
Log.d(TAG, "Modal icon updated");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
package com.example.bdkipoc.transaction.managers;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.bdkipoc.MyApplication;
|
||||
import com.example.bdkipoc.utils.ByteUtil;
|
||||
import com.sunmi.pay.hardware.aidlv2.AidlErrorCodeV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.bean.PinPadConfigV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadListenerV2;
|
||||
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2;
|
||||
|
||||
/**
|
||||
* PinPadManager - Handles PIN pad operations
|
||||
*/
|
||||
public class PinPadManager {
|
||||
private static final String TAG = "PinPadManager";
|
||||
|
||||
private PinPadOptV2 mPinPadOptV2;
|
||||
private PinPadManagerCallback callback;
|
||||
|
||||
public interface PinPadManagerCallback {
|
||||
void onPinInputLength(int length);
|
||||
void onPinInputConfirmed(byte[] pinBlock);
|
||||
void onPinInputCancelled();
|
||||
void onPinInputError(int code, String message);
|
||||
}
|
||||
|
||||
public PinPadManager(PinPadManagerCallback callback) {
|
||||
this.callback = callback;
|
||||
initPinPadComponents();
|
||||
}
|
||||
|
||||
private void initPinPadComponents() {
|
||||
if (MyApplication.app != null) {
|
||||
mPinPadOptV2 = MyApplication.app.pinPadOptV2;
|
||||
Log.d(TAG, "PIN Pad components initialized");
|
||||
} else {
|
||||
Log.e(TAG, "MyApplication.app is null");
|
||||
}
|
||||
}
|
||||
|
||||
public void initPinPad(String cardNo, int pinType) {
|
||||
Log.d(TAG, "========== PIN PAD INITIALIZATION ==========");
|
||||
try {
|
||||
if (mPinPadOptV2 == null) {
|
||||
throw new IllegalStateException("PIN Pad service not available");
|
||||
}
|
||||
|
||||
if (cardNo == null || cardNo.length() < 13) {
|
||||
throw new IllegalArgumentException("Invalid card number for PIN");
|
||||
}
|
||||
|
||||
PinPadConfigV2 pinPadConfig = new PinPadConfigV2();
|
||||
pinPadConfig.setPinPadType(0);
|
||||
pinPadConfig.setPinType(pinType);
|
||||
pinPadConfig.setOrderNumKey(true); // Set to true for normal order, false for random
|
||||
|
||||
String panForPin = cardNo.substring(cardNo.length() - 13, cardNo.length() - 1);
|
||||
byte[] panBytes = panForPin.getBytes("US-ASCII");
|
||||
pinPadConfig.setPan(panBytes);
|
||||
|
||||
pinPadConfig.setTimeout(60 * 1000);
|
||||
pinPadConfig.setPinKeyIndex(12);
|
||||
pinPadConfig.setMaxInput(12);
|
||||
pinPadConfig.setMinInput(0);
|
||||
pinPadConfig.setKeySystem(0);
|
||||
pinPadConfig.setAlgorithmType(0);
|
||||
|
||||
Log.d(TAG, "Initializing PIN pad with config");
|
||||
mPinPadOptV2.initPinPad(pinPadConfig, mPinPadListener);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "PIN pad initialization failed: " + e.getMessage());
|
||||
if (callback != null) {
|
||||
callback.onPinInputError(-1, "PIN Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelPinInput() {
|
||||
try {
|
||||
if (mPinPadOptV2 != null) {
|
||||
// Cancel PIN input if needed
|
||||
Log.d(TAG, "PIN input cancelled");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error cancelling PIN input: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// PIN Pad Listener
|
||||
private final PinPadListenerV2 mPinPadListener = new PinPadListenerV2.Stub() {
|
||||
@Override
|
||||
public void onPinLength(int len) throws RemoteException {
|
||||
Log.d(TAG, "PIN input length: " + len);
|
||||
if (callback != null) {
|
||||
callback.onPinInputLength(len);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirm(int i, byte[] pinBlock) throws RemoteException {
|
||||
Log.d(TAG, "PIN input confirmed");
|
||||
|
||||
if (pinBlock != null) {
|
||||
String hexStr = ByteUtil.bytes2HexStr(pinBlock);
|
||||
Log.d(TAG, "PIN block received: " + hexStr);
|
||||
if (callback != null) {
|
||||
callback.onPinInputConfirmed(pinBlock);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "PIN bypass confirmed");
|
||||
if (callback != null) {
|
||||
callback.onPinInputConfirmed(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() throws RemoteException {
|
||||
Log.d(TAG, "PIN input cancelled by user");
|
||||
if (callback != null) {
|
||||
callback.onPinInputCancelled();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int code) throws RemoteException {
|
||||
Log.e(TAG, "PIN pad error: " + code);
|
||||
String msg = AidlErrorCodeV2.valueOf(code).getMsg();
|
||||
if (callback != null) {
|
||||
callback.onPinInputError(code, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHover(int event, byte[] data) throws RemoteException {
|
||||
Log.d(TAG, "PIN pad hover event: " + event);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,358 @@
|
||||
package com.example.bdkipoc.transaction.managers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* PostTransactionBackendManager - Handles backend transaction posting
|
||||
*
|
||||
* This manager handles the communication with the backend service for transaction posting
|
||||
* and provides the transaction_uuid needed for Midtrans integration.
|
||||
*/
|
||||
public class PostTransactionBackendManager {
|
||||
private static final String TAG = "PostTransactionBackend";
|
||||
|
||||
// Backend Configuration
|
||||
private static final String BACKEND_BASE_URL = "https://be-edc.msvc.app";
|
||||
private static final String TRANSACTIONS_ENDPOINT = BACKEND_BASE_URL + "/transactions";
|
||||
|
||||
// Default values
|
||||
private static final String DEFAULT_DEVICE_CODE = "PB4K252T00021";
|
||||
private static final int DEFAULT_DEVICE_ID = 1;
|
||||
private static final String DEFAULT_CASHFLOW = "MONEY_IN";
|
||||
private static final String DEFAULT_CHANNEL_CATEGORY = "RETAIL_OUTLET";
|
||||
|
||||
// ✅ NEW: Static merchant data
|
||||
private static final String DEFAULT_MERCHANT_NAME = "BUDIAJAIB123";
|
||||
private static final String DEFAULT_MID = "542531513";
|
||||
private static final String DEFAULT_TID = "535151521";
|
||||
|
||||
private Context context;
|
||||
private PostTransactionCallback callback;
|
||||
|
||||
public interface PostTransactionCallback {
|
||||
void onPostTransactionSuccess(JSONObject response, String transactionUuid);
|
||||
void onPostTransactionError(String errorMessage);
|
||||
void onPostTransactionProgress(String message);
|
||||
}
|
||||
|
||||
public PostTransactionBackendManager(Context context, PostTransactionCallback callback) {
|
||||
this.context = context;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post transaction to backend service
|
||||
*/
|
||||
public void postTransaction(String paymentType, String referenceId, long amount, String status) {
|
||||
String channelCode = mapPaymentTypeToChannelCode(paymentType);
|
||||
String transactionUuid = generateUUID();
|
||||
|
||||
Log.d(TAG, "=== POSTING TRANSACTION TO BACKEND ===");
|
||||
Log.d(TAG, "Payment Type: " + paymentType);
|
||||
Log.d(TAG, "Channel Code: " + channelCode);
|
||||
Log.d(TAG, "Reference ID: " + referenceId);
|
||||
Log.d(TAG, "Amount: " + amount);
|
||||
Log.d(TAG, "Status: " + status);
|
||||
Log.d(TAG, "Transaction UUID: " + transactionUuid);
|
||||
Log.d(TAG, "=====================================");
|
||||
|
||||
if (callback != null) {
|
||||
callback.onPostTransactionProgress("Posting transaction to backend...");
|
||||
}
|
||||
|
||||
new PostTransactionTask(paymentType, channelCode, referenceId, amount, status, transactionUuid).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Post transaction with INIT status (for pre-authorization)
|
||||
*/
|
||||
public void postInitTransaction(String paymentType, String referenceId, long amount) {
|
||||
postTransaction(paymentType, referenceId, amount, "INIT");
|
||||
}
|
||||
|
||||
/**
|
||||
* Post transaction with SUCCESS status (for completed transactions)
|
||||
*/
|
||||
public void postSuccessTransaction(String paymentType, String referenceId, long amount) {
|
||||
postTransaction(paymentType, referenceId, amount, "SUCCESS");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing transaction status
|
||||
*/
|
||||
public void updateTransactionStatus(String transactionUuid, String newStatus) {
|
||||
Log.d(TAG, "Updating transaction " + transactionUuid + " to status: " + newStatus);
|
||||
// TODO: Implement update endpoint if available
|
||||
// For now, we'll create a new transaction with updated status
|
||||
}
|
||||
|
||||
/**
|
||||
* Map payment type to channel code for backend
|
||||
*/
|
||||
private String mapPaymentTypeToChannelCode(String paymentType) {
|
||||
if (paymentType == null) {
|
||||
return "CREDIT_CARD"; // Default
|
||||
}
|
||||
|
||||
switch (paymentType.toLowerCase()) {
|
||||
case "credit_card":
|
||||
return "CREDIT_CARD";
|
||||
case "debit_card":
|
||||
return "DEBIT_CARD";
|
||||
case "e_money":
|
||||
return "E_MONEY";
|
||||
case "qris":
|
||||
return "QRIS";
|
||||
default:
|
||||
Log.w(TAG, "Unknown payment type: " + paymentType + ", using CREDIT_CARD");
|
||||
return "CREDIT_CARD";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUID v4 for transaction
|
||||
*/
|
||||
private String generateUUID() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device serial number (Sunmi device code)
|
||||
*/
|
||||
private String getDeviceCode() {
|
||||
try {
|
||||
// Try to get actual device serial number
|
||||
// For Sunmi devices, this might be available through system properties
|
||||
String serialNumber = android.os.Build.SERIAL;
|
||||
if (serialNumber != null && !serialNumber.equals("unknown") && !serialNumber.equals(android.os.Build.UNKNOWN)) {
|
||||
return serialNumber;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Could not get device serial number: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Fallback to default device code
|
||||
return DEFAULT_DEVICE_CODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* AsyncTask for posting transaction to backend
|
||||
*/
|
||||
private class PostTransactionTask extends AsyncTask<Void, Void, Boolean> {
|
||||
private String paymentType;
|
||||
private String channelCode;
|
||||
private String referenceId;
|
||||
private long amount;
|
||||
private String status;
|
||||
private String transactionUuid;
|
||||
private String errorMessage;
|
||||
private JSONObject responseData;
|
||||
|
||||
public PostTransactionTask(String paymentType, String channelCode, String referenceId,
|
||||
long amount, String status, String transactionUuid) {
|
||||
this.paymentType = paymentType;
|
||||
this.channelCode = channelCode;
|
||||
this.referenceId = referenceId;
|
||||
this.amount = amount;
|
||||
this.status = status;
|
||||
this.transactionUuid = transactionUuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
// Build transaction payload
|
||||
JSONObject payload = buildTransactionPayload();
|
||||
|
||||
Log.d(TAG, "Backend payload: " + payload.toString());
|
||||
|
||||
// Make HTTP request
|
||||
return makeBackendRequest(payload);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Backend transaction exception: " + e.getMessage(), e);
|
||||
errorMessage = "Backend transaction error: " + e.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean success) {
|
||||
if (success && responseData != null && callback != null) {
|
||||
try {
|
||||
// Extract transaction_uuid from response
|
||||
JSONObject data = responseData.optJSONObject("data");
|
||||
String returnedUuid = null;
|
||||
|
||||
if (data != null) {
|
||||
returnedUuid = data.optString("transaction_uuid", transactionUuid);
|
||||
}
|
||||
|
||||
Log.d(TAG, "✅ Backend transaction successful!");
|
||||
Log.d(TAG, "Original UUID: " + transactionUuid);
|
||||
Log.d(TAG, "Returned UUID: " + returnedUuid);
|
||||
|
||||
callback.onPostTransactionSuccess(responseData, returnedUuid != null ? returnedUuid : transactionUuid);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error processing backend response: " + e.getMessage());
|
||||
if (callback != null) {
|
||||
callback.onPostTransactionError("Error processing backend response: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
} else if (callback != null) {
|
||||
callback.onPostTransactionError(errorMessage != null ? errorMessage : "Unknown backend error");
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject buildTransactionPayload() throws JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
|
||||
// Required fields
|
||||
payload.put("type", "PAYMENT");
|
||||
payload.put("channel_category", DEFAULT_CHANNEL_CATEGORY);
|
||||
payload.put("channel_code", channelCode);
|
||||
payload.put("reference_id", referenceId);
|
||||
payload.put("amount", amount);
|
||||
payload.put("cashflow", DEFAULT_CASHFLOW);
|
||||
payload.put("status", status);
|
||||
payload.put("device_id", DEFAULT_DEVICE_ID);
|
||||
payload.put("transaction_uuid", transactionUuid);
|
||||
payload.put("transaction_time_seconds", 2.2); // Default value as mentioned
|
||||
payload.put("device_code", getDeviceCode());
|
||||
|
||||
// ✅ NEW: Static merchant data (no longer null)
|
||||
payload.put("merchant_name", DEFAULT_MERCHANT_NAME);
|
||||
payload.put("mid", DEFAULT_MID);
|
||||
payload.put("tid", DEFAULT_TID);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private Boolean makeBackendRequest(JSONObject payload) {
|
||||
try {
|
||||
URL url = new URI(TRANSACTIONS_ENDPOINT).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
|
||||
// Set request properties
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Accept", "*/*");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(30000);
|
||||
|
||||
// Send payload
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input = payload.toString().getBytes("utf-8");
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
|
||||
// Get response
|
||||
int responseCode = conn.getResponseCode();
|
||||
Log.d(TAG, "Backend response code: " + responseCode);
|
||||
|
||||
BufferedReader br;
|
||||
StringBuilder response = new StringBuilder();
|
||||
String responseLine;
|
||||
|
||||
if (responseCode >= 200 && responseCode < 300) {
|
||||
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
||||
} else {
|
||||
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
||||
}
|
||||
|
||||
while ((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
|
||||
String responseString = response.toString();
|
||||
Log.d(TAG, "Backend response: " + responseString);
|
||||
|
||||
// Parse response
|
||||
try {
|
||||
responseData = new JSONObject(responseString);
|
||||
|
||||
// Check if response indicates success
|
||||
int status = responseData.optInt("status", 0);
|
||||
String message = responseData.optString("message", "");
|
||||
|
||||
if (status == 200 && "Successfully".equals(message)) {
|
||||
return true;
|
||||
} else {
|
||||
errorMessage = "Backend error: " + message + " (Status: " + status + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Error parsing backend response: " + e.getMessage());
|
||||
errorMessage = "Invalid backend response format";
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Backend request exception: " + e.getMessage(), e);
|
||||
errorMessage = "Network error: " + e.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate reference ID
|
||||
*/
|
||||
public static String generateReferenceId() {
|
||||
return "ref" + System.currentTimeMillis() + (int)(Math.random() * 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to map card menu ID to payment type
|
||||
*/
|
||||
public static String mapCardMenuToPaymentType(int cardMenuId) {
|
||||
// Based on MainActivity.java card IDs
|
||||
switch (cardMenuId) {
|
||||
case 2131296346: // R.id.card_kartu_kredit
|
||||
return "credit_card";
|
||||
case 2131296344: // R.id.card_kartu_debit
|
||||
return "debit_card";
|
||||
case 2131296360: // R.id.card_uang_elektronik
|
||||
return "e_money";
|
||||
case 2131296352: // R.id.card_qris
|
||||
return "qris";
|
||||
default:
|
||||
Log.w(TAG, "Unknown card menu ID: " + cardMenuId + ", defaulting to credit_card");
|
||||
return "credit_card";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug method to log transaction details
|
||||
*/
|
||||
public void debugTransactionData(String paymentType, String referenceId, long amount, String status) {
|
||||
Log.d(TAG, "=== TRANSACTION DEBUG INFO ===");
|
||||
Log.d(TAG, "Payment Type: " + paymentType);
|
||||
Log.d(TAG, "Channel Code: " + mapPaymentTypeToChannelCode(paymentType));
|
||||
Log.d(TAG, "Reference ID: " + referenceId);
|
||||
Log.d(TAG, "Amount: " + amount);
|
||||
Log.d(TAG, "Status: " + status);
|
||||
Log.d(TAG, "Device Code: " + getDeviceCode());
|
||||
Log.d(TAG, "Device ID: " + DEFAULT_DEVICE_ID);
|
||||
Log.d(TAG, "Merchant Name: " + DEFAULT_MERCHANT_NAME);
|
||||
Log.d(TAG, "MID: " + DEFAULT_MID);
|
||||
Log.d(TAG, "TID: " + DEFAULT_TID);
|
||||
Log.d(TAG, "==============================");
|
||||
}
|
||||
}
|
||||
265
app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java
Normal file
@ -0,0 +1,265 @@
|
||||
package com.example.bdkipoc.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ByteUtil {
|
||||
|
||||
/** 打印内容 */
|
||||
public static String byte2PrintHex(byte[] raw, int offset, int count) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
if (offset < 0 || offset > raw.length) {
|
||||
offset = 0;
|
||||
}
|
||||
int end = offset + count;
|
||||
if (end > raw.length) {
|
||||
end = raw.length;
|
||||
}
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (int i = offset; i < end; i++) {
|
||||
int v = raw[i] & 0xFF;
|
||||
String hv = Integer.toHexString(v);
|
||||
if (hv.length() < 2) {
|
||||
hex.append(0);
|
||||
}
|
||||
hex.append(hv);
|
||||
hex.append(" ");
|
||||
}
|
||||
if (hex.length() > 0) {
|
||||
hex.deleteCharAt(hex.length() - 1);
|
||||
}
|
||||
return hex.toString().toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组转换成16进制字符串
|
||||
*
|
||||
* @param bytes 源字节数组
|
||||
* @return 转换后的16进制字符串
|
||||
*/
|
||||
public static String bytes2HexStr(byte... bytes) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return "";
|
||||
}
|
||||
return bytes2HexStr(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组转换成16进制字符串
|
||||
*
|
||||
* @param src 源字节数组
|
||||
* @param offset 偏移量
|
||||
* @param len 数据长度
|
||||
* @return 转换后的16进制字符串
|
||||
*/
|
||||
public static String bytes2HexStr(byte[] src, int offset, int len) {
|
||||
int end = offset + len;
|
||||
if (src == null || src.length == 0 || offset < 0 || len < 0 || end > src.length) {
|
||||
return "";
|
||||
}
|
||||
byte[] buffer = new byte[len * 2];
|
||||
int h = 0, l = 0;
|
||||
for (int i = offset, j = 0; i < end; i++) {
|
||||
h = src[i] >> 4 & 0x0f;
|
||||
l = src[i] & 0x0f;
|
||||
buffer[j++] = (byte) (h > 9 ? h - 10 + 'A' : h + '0');
|
||||
buffer[j++] = (byte) (l > 9 ? l - 10 + 'A' : l + '0');
|
||||
}
|
||||
return new String(buffer);
|
||||
}
|
||||
|
||||
public static byte[] hexStr2Bytes(String hexStr) {
|
||||
if (TextUtils.isEmpty(hexStr)) {
|
||||
return new byte[0];
|
||||
}
|
||||
int length = hexStr.length() / 2;
|
||||
char[] chars = hexStr.toCharArray();
|
||||
byte[] b = new byte[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
b[i] = (byte) (char2Byte(chars[i * 2]) << 4 | char2Byte(chars[i * 2 + 1]));
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
public static byte hexStr2Byte(String hexStr) {
|
||||
return (byte) Integer.parseInt(hexStr, 16);
|
||||
}
|
||||
|
||||
public static String hexStr2Str(String hexStr) {
|
||||
String vi = "0123456789ABC DEF".trim();
|
||||
char[] array = hexStr.toCharArray();
|
||||
byte[] bytes = new byte[hexStr.length() / 2];
|
||||
int temp;
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
char c = array[2 * i];
|
||||
temp = vi.indexOf(c) * 16;
|
||||
c = array[2 * i + 1];
|
||||
temp += vi.indexOf(c);
|
||||
bytes[i] = (byte) (temp & 0xFF);
|
||||
}
|
||||
return new String(bytes);
|
||||
}
|
||||
|
||||
public static String hexStr2AsciiStr(String hexStr) {
|
||||
String vi = "0123456789ABC DEF".trim();
|
||||
hexStr = hexStr.trim().replace(" ", "").toUpperCase(Locale.US);
|
||||
char[] array = hexStr.toCharArray();
|
||||
byte[] bytes = new byte[hexStr.length() / 2];
|
||||
int temp = 0x00;
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
char c = array[2 * i];
|
||||
temp = vi.indexOf(c) << 4;
|
||||
c = array[2 * i + 1];
|
||||
temp |= vi.indexOf(c);
|
||||
bytes[i] = (byte) (temp & 0xFF);
|
||||
}
|
||||
return new String(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将无符号short转换成int,大端模式(高位在前)
|
||||
*/
|
||||
public static int unsignedShort2IntBE(byte[] src, int offset) {
|
||||
return (src[offset] & 0xff) << 8 | (src[offset + 1] & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将无符号short转换成int,小端模式(低位在前)
|
||||
*/
|
||||
public static int unsignedShort2IntLE(byte[] src, int offset) {
|
||||
return (src[offset] & 0xff) | (src[offset + 1] & 0xff) << 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将无符号byte转换成int
|
||||
*/
|
||||
public static int unsignedByte2Int(byte[] src, int offset) {
|
||||
return src[offset] & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组转换成int,大端模式(高位在前)
|
||||
*/
|
||||
public static int unsignedInt2IntBE(byte[] src, int offset) {
|
||||
int result = 0;
|
||||
for (int i = offset; i < offset + 4; i++) {
|
||||
result |= (src[i] & 0xff) << (offset + 3 - i) * 8;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组转换成int,小端模式(低位在前)
|
||||
*/
|
||||
public static int unsignedInt2IntLE(byte[] src, int offset) {
|
||||
int value = 0;
|
||||
for (int i = offset; i < offset + 4; i++) {
|
||||
value |= (src[i] & 0xff) << (i - offset) * 8;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将int转换成byte数组,大端模式(高位在前)
|
||||
*/
|
||||
public static byte[] int2BytesBE(int src) {
|
||||
byte[] result = new byte[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
result[i] = (byte) (src >> (3 - i) * 8);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将int转换成byte数组,小端模式(低位在前)
|
||||
*/
|
||||
public static byte[] int2BytesLE(int src) {
|
||||
byte[] result = new byte[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
result[i] = (byte) (src >> i * 8);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将short转换成byte数组,大端模式(高位在前)
|
||||
*/
|
||||
public static byte[] short2BytesBE(short src) {
|
||||
byte[] result = new byte[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
result[i] = (byte) (src >> (1 - i) * 8);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将short转换成byte数组,小端模式(低位在前)
|
||||
*/
|
||||
public static byte[] short2BytesLE(short src) {
|
||||
byte[] result = new byte[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
result[i] = (byte) (src >> i * 8);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组列表合并成单个字节数组
|
||||
*/
|
||||
public static byte[] concatByteArrays(byte[]... list) {
|
||||
if (list == null || list.length == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
return concatByteArrays(Arrays.asList(list));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组列表合并成单个字节数组
|
||||
*/
|
||||
public static byte[] concatByteArrays(List<byte[]> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return new byte[0];
|
||||
}
|
||||
int totalLen = 0;
|
||||
for (byte[] b : list) {
|
||||
if (b == null || b.length == 0) {
|
||||
continue;
|
||||
}
|
||||
totalLen += b.length;
|
||||
}
|
||||
byte[] result = new byte[totalLen];
|
||||
int index = 0;
|
||||
for (byte[] b : list) {
|
||||
if (b == null || b.length == 0) {
|
||||
continue;
|
||||
}
|
||||
System.arraycopy(b, 0, result, index, b.length);
|
||||
index += b.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert char to byte
|
||||
*
|
||||
* @param c char
|
||||
* @return byte
|
||||
*/
|
||||
private static int char2Byte(char c) {
|
||||
if (c >= 'a') {
|
||||
return (c - 'a' + 10) & 0x0f;
|
||||
}
|
||||
if (c >= 'A') {
|
||||
return (c - 'A' + 10) & 0x0f;
|
||||
}
|
||||
return (c - '0') & 0x0f;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
89
app/src/main/java/com/example/bdkipoc/utils/LogUtil.java
Normal file
@ -0,0 +1,89 @@
|
||||
package com.example.bdkipoc.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public class LogUtil {
|
||||
|
||||
public static final int VERBOSE = 1;
|
||||
public static final int DEBUG = 2;
|
||||
public static final int INFO = 3;
|
||||
public static final int WARN = 4;
|
||||
public static final int ERROR = 5;
|
||||
public static final int NOTHING = 6;
|
||||
public static int LEVEL = VERBOSE;
|
||||
|
||||
public static void setLevel(int Level) {
|
||||
LEVEL = Level;
|
||||
}
|
||||
|
||||
public static void v(String TAG, String msg) {
|
||||
if (LEVEL <= VERBOSE && !TextUtils.isEmpty(msg)) {
|
||||
MyLog(VERBOSE, TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void d(String TAG, String msg) {
|
||||
if (LEVEL <= DEBUG && !TextUtils.isEmpty(msg)) {
|
||||
MyLog(DEBUG, TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void i(String TAG, String msg) {
|
||||
if (LEVEL <= INFO && !TextUtils.isEmpty(msg)) {
|
||||
MyLog(INFO, TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void w(String TAG, String msg) {
|
||||
if (LEVEL <= WARN && !TextUtils.isEmpty(msg)) {
|
||||
MyLog(WARN, TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void e(String TAG, String msg) {
|
||||
if (LEVEL <= ERROR && !TextUtils.isEmpty(msg)) {
|
||||
MyLog(ERROR, TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void MyLog(int type, String TAG, String msg) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
int index = 4;
|
||||
String className = stackTrace[index].getFileName();
|
||||
String methodName = stackTrace[index].getMethodName();
|
||||
int lineNumber = stackTrace[index].getLineNumber();
|
||||
methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.append("[ (")
|
||||
.append(className)
|
||||
.append(":")
|
||||
.append(lineNumber)
|
||||
.append(")#")
|
||||
.append(methodName)
|
||||
.append(" ] ");
|
||||
stringBuilder.append(msg);
|
||||
String logStr = stringBuilder.toString();
|
||||
switch (type) {
|
||||
case VERBOSE:
|
||||
Log.v(TAG, logStr);
|
||||
break;
|
||||
case DEBUG:
|
||||
Log.d(TAG, logStr);
|
||||
break;
|
||||
case INFO:
|
||||
Log.i(TAG, logStr);
|
||||
break;
|
||||
case WARN:
|
||||
Log.w(TAG, logStr);
|
||||
break;
|
||||
case ERROR:
|
||||
Log.e(TAG, logStr);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
107
app/src/main/java/com/example/bdkipoc/utils/Utility.java
Normal file
@ -0,0 +1,107 @@
|
||||
package com.example.bdkipoc.utils;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.bdkipoc.MyApplication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class Utility {
|
||||
private Utility() {
|
||||
throw new AssertionError("Create instance of Utility is forbidden.");
|
||||
}
|
||||
|
||||
/** Bundle对象转换成字符串 */
|
||||
public static String bundle2String(Bundle bundle) {
|
||||
return bundle2String(bundle, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据key排序后将Bundle内容拼接成字符串
|
||||
*
|
||||
* @param bundle 要处理的bundle
|
||||
* @param order 排序规则,0-不排序,1-升序,2-降序
|
||||
* @return 拼接后的字符串
|
||||
*/
|
||||
public static String bundle2String(Bundle bundle, int order) {
|
||||
if (bundle == null || bundle.keySet().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<String> list = new ArrayList<>(bundle.keySet());
|
||||
if (order == 1) { //升序
|
||||
Collections.sort(list, String::compareTo);
|
||||
} else if (order == 2) {//降序
|
||||
Collections.sort(list, Collections.reverseOrder());
|
||||
}
|
||||
for (String key : list) {
|
||||
sb.append(key);
|
||||
sb.append(":");
|
||||
Object value = bundle.get(key);
|
||||
if (value instanceof byte[]) {
|
||||
sb.append(ByteUtil.bytes2HexStr((byte[]) value));
|
||||
} else {
|
||||
sb.append(value);
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 将null转换成空串 */
|
||||
public static String null2String(String str) {
|
||||
return str == null ? "" : str;
|
||||
}
|
||||
|
||||
public static String formatStr(String format, Object... params) {
|
||||
return String.format(Locale.ENGLISH, format, params);
|
||||
}
|
||||
|
||||
/** check whether src is hex format */
|
||||
public static boolean checkHexValue(String src) {
|
||||
return Pattern.matches("[0-9a-fA-F]+", src);
|
||||
}
|
||||
|
||||
/** 显示Toast */
|
||||
public static void showToast(final String msg) {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.post(() -> Toast.makeText(MyApplication.app, msg, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
/** 显示Toast */
|
||||
public static void showToast(int resId) {
|
||||
showToast(MyApplication.app.getString(resId));
|
||||
}
|
||||
|
||||
/** 根据结果码获取成功失败信息 */
|
||||
public static String getStateString(int code) {
|
||||
return code == 0 ? "success" : "failed, code:" + code;
|
||||
}
|
||||
|
||||
/** 根据结果状态获取成功失败信息 */
|
||||
public static String getStateString(boolean state) {
|
||||
return state ? "success" : "failed";
|
||||
}
|
||||
|
||||
/** 将dp转成px */
|
||||
public static int dp2px(int dp) {
|
||||
float density = MyApplication.app.getResources().getDisplayMetrics().density;
|
||||
return Math.round(dp * density);
|
||||
}
|
||||
|
||||
/** 将px转成dp */
|
||||
public static int px2dp(int px) {
|
||||
float density = MyApplication.app.getResources().getDisplayMetrics().density;
|
||||
return Math.round(px / density);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.example.bdkipoc.wrapper;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2;
|
||||
|
||||
|
||||
public class CheckCardCallbackV2Wrapper extends CheckCardCallbackV2.Stub {
|
||||
@Override
|
||||
public void findMagCard(Bundle info) throws RemoteException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findICCard(String atr) throws RemoteException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findRFCard(String uuid) throws RemoteException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int code, String message) throws RemoteException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findICCardEx(Bundle info) throws RemoteException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findRFCardEx(Bundle info) throws RemoteException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onErrorEx(Bundle info) throws RemoteException {
|
||||
|
||||
}
|
||||
}
|
||||
4
app/src/main/res/anim/fade_in.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
4
app/src/main/res/anim/fade_out.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
14
app/src/main/res/anim/scale_in.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<scale
|
||||
android:duration="200"
|
||||
android:fromXScale="0.8"
|
||||
android:fromYScale="0.8"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="1.0"
|
||||
android:toYScale="1.0" />
|
||||
<alpha
|
||||
android:duration="200"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
</set>
|
||||
14
app/src/main/res/anim/scale_out.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<scale
|
||||
android:duration="200"
|
||||
android:fromXScale="1.0"
|
||||
android:fromYScale="1.0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="0.8"
|
||||
android:toYScale="0.8" />
|
||||
<alpha
|
||||
android:duration="200"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
</set>
|
||||
10
app/src/main/res/anim/slide_down.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromYDelta="0%p"
|
||||
android:toYDelta="50%p" />
|
||||
<alpha
|
||||
android:duration="300"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
</set>
|
||||
10
app/src/main/res/anim/slide_in_left.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromXDelta="-100%p"
|
||||
android:toXDelta="0" />
|
||||
<alpha
|
||||
android:duration="300"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
</set>
|
||||
10
app/src/main/res/anim/slide_in_right.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromXDelta="100%p"
|
||||
android:toXDelta="0" />
|
||||
<alpha
|
||||
android:duration="300"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
</set>
|
||||
10
app/src/main/res/anim/slide_out_left.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="-100%p" />
|
||||
<alpha
|
||||
android:duration="300"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
</set>
|
||||
10
app/src/main/res/anim/slide_out_right.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="100%p" />
|
||||
<alpha
|
||||
android:duration="300"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
</set>
|
||||
10
app/src/main/res/anim/slide_up.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromYDelta="50%p"
|
||||
android:toYDelta="0%p" />
|
||||
<alpha
|
||||
android:duration="300"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
</set>
|
||||
BIN
app/src/main/res/drawable/banner.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
6
app/src/main/res/drawable/bg_status.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#F0F0F0"/>
|
||||
<corners android:radius="8dp"/>
|
||||
<stroke android:width="1dp" android:color="#E0E0E0"/>
|
||||
</shape>
|
||||
6
app/src/main/res/drawable/border_button_red.xml
Normal 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>
|
||||
6
app/src/main/res/drawable/button_active_background.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#DE0701" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/button_cancel_background.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:width="2dp" android:color="#E31937"/>
|
||||
<corners android:radius="8dp"/>
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
</shape>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/button_active_background" android:state_enabled="true"/>
|
||||
<item android:drawable="@drawable/button_inactive_background" android:state_enabled="false"/>
|
||||
</selector>
|
||||
9
app/src/main/res/drawable/button_finish_background.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#3498DB" />
|
||||
|
||||
<corners android:radius="8dp" />
|
||||
|
||||
</shape>
|
||||
6
app/src/main/res/drawable/button_inactive_background.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#ECEFF0" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
13
app/src/main/res/drawable/button_secondary_background.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#F5F5F5" />
|
||||
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#E0E0E0" />
|
||||
|
||||
<corners android:radius="8dp" />
|
||||
|
||||
</shape>
|
||||
12
app/src/main/res/drawable/card_background.xml
Normal 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>
|
||||
6
app/src/main/res/drawable/copyable_text_background.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#F0F0F0" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke android:width="1dp" android:color="#DDDDDD" />
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/ic_arrow_back.png
Normal file
|
After Width: | Height: | Size: 220 B |
10
app/src/main/res/drawable/ic_backspace.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- res/drawable/icons/ic_backspace.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#333333"
|
||||
android:pathData="M22,3H7c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12 19,15.59z"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/ic_card_insert.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
10
app/src/main/res/drawable/ic_check_circle.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/ic_credit_card.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
app/src/main/res/drawable/ic_debit_card.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable/ic_e_money.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable/ic_email.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable/ic_help.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable/ic_history.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |