From ef756b97a11480598ac85271f918cf4976d0d657 Mon Sep 17 00:00:00 2001 From: wirabasalamah Date: Wed, 22 Apr 2026 22:31:52 +0700 Subject: [PATCH] Initial import --- .gitignore | 24 + README.md | 38 + app/build.gradle.kts | 74 ++ app/proguard-rules.pro | 1 + app/src/main/AndroidManifest.xml | 53 ++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 8862 bytes .../java/com/iiyh/emoneyinfo/MainActivity.kt | 83 ++ .../com/iiyh/emoneyinfo/ads/AdMobConfig.kt | 13 + .../java/com/iiyh/emoneyinfo/data/FaqData.kt | 48 + .../java/com/iiyh/emoneyinfo/data/Models.kt | 78 ++ .../com/iiyh/emoneyinfo/nfc/AndroidStrings.kt | 8 + .../com/iiyh/emoneyinfo/nfc/BrizziCrypto.kt | 85 ++ .../com/iiyh/emoneyinfo/nfc/CardReaders.kt | 861 ++++++++++++++++++ .../java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt | 192 ++++ .../iiyh/emoneyinfo/nfc/UnifiedNfcReader.kt | 125 +++ .../iiyh/emoneyinfo/pdf/HistoryPdfExporter.kt | 243 +++++ .../com/iiyh/emoneyinfo/ui/EmoneyInfoApp.kt | 139 +++ .../emoneyinfo/ui/components/AdMobBanner.kt | 64 ++ .../emoneyinfo/ui/components/ScreenTopBar.kt | 30 + .../iiyh/emoneyinfo/ui/screens/AboutScreen.kt | 156 ++++ .../iiyh/emoneyinfo/ui/screens/FaqScreen.kt | 194 ++++ .../emoneyinfo/ui/screens/HistoryScreen.kt | 228 +++++ .../iiyh/emoneyinfo/ui/screens/HomeScreen.kt | 334 +++++++ .../ui/screens/PrivacyPolicyScreen.kt | 45 + .../emoneyinfo/ui/screens/SettingsScreen.kt | 181 ++++ .../iiyh/emoneyinfo/ui/screens/TermsScreen.kt | 45 + .../com/iiyh/emoneyinfo/ui/theme/Color.kt | 13 + .../com/iiyh/emoneyinfo/ui/theme/Theme.kt | 31 + .../java/com/iiyh/emoneyinfo/ui/theme/Type.kt | 5 + .../java/com/iiyh/emoneyinfo/util/AppLog.kt | 23 + app/src/main/res/drawable-nodpi/app_logo.png | Bin 0 -> 16273 bytes .../main/res/drawable-nodpi/home_header.png | Bin 0 -> 12244 bytes .../drawable-nodpi/ic_activity_outline.png | Bin 0 -> 7666 bytes .../res/drawable-nodpi/ic_card_outline.png | Bin 0 -> 7976 bytes .../drawable-nodpi/ic_settings_outline.png | Bin 0 -> 10632 bytes .../main/res/drawable-nodpi/ic_simcard.png | Bin 0 -> 11505 bytes .../res/drawable/ic_launcher_background.xml | 3 + .../res/drawable/ic_launcher_foreground.xml | 18 + .../main/res/drawable/launch_background.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 844 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 438 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2016 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 648 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 322 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1296 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1088 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 550 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 2802 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 1384 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 752 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 4384 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 1886 bytes .../ic_launcher_foreground.webp | Bin 0 -> 978 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 6326 bytes app/src/main/res/values-id/strings.xml | 150 +++ app/src/main/res/values/colors.xml | 9 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 150 +++ app/src/main/res/values/themes.xml | 14 + app/src/main/res/xml/file_paths.xml | 6 + app/src/main/res/xml/nfc_tech_filter.xml | 9 + build.gradle.kts | 5 + docs/CODE_DOCUMENTATION.md | 414 +++++++++ gradle.properties | 4 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 ++++ gradlew.bat | 89 ++ settings.gradle.kts | 18 + 71 files changed, 4518 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/MainActivity.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ads/AdMobConfig.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/data/FaqData.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/data/Models.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/nfc/AndroidStrings.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/nfc/BrizziCrypto.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/nfc/CardReaders.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/nfc/UnifiedNfcReader.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/pdf/HistoryPdfExporter.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/EmoneyInfoApp.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/components/AdMobBanner.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/components/ScreenTopBar.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/screens/AboutScreen.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/screens/FaqScreen.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HistoryScreen.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HomeScreen.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/screens/PrivacyPolicyScreen.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/screens/SettingsScreen.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/screens/TermsScreen.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Color.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Type.kt create mode 100644 app/src/main/java/com/iiyh/emoneyinfo/util/AppLog.kt create mode 100755 app/src/main/res/drawable-nodpi/app_logo.png create mode 100755 app/src/main/res/drawable-nodpi/home_header.png create mode 100644 app/src/main/res/drawable-nodpi/ic_activity_outline.png create mode 100644 app/src/main/res/drawable-nodpi/ic_card_outline.png create mode 100644 app/src/main/res/drawable-nodpi/ic_settings_outline.png create mode 100644 app/src/main/res/drawable-nodpi/ic_simcard.png create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/launch_background.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-id/strings.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/file_paths.xml create mode 100644 app/src/main/res/xml/nfc_tech_filter.xml create mode 100644 build.gradle.kts create mode 100644 docs/CODE_DOCUMENTATION.md create mode 100644 gradle.properties create mode 100755 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100755 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58ee6e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.DS_Store + +# Gradle / Kotlin +.gradle/ +.kotlin/ +build/ +*/build/ + +# Local SDK / machine-specific config +local.properties + +# IDE +.idea/ +*.iml + +# Signing keys +*.jks +*.keystore + +# Generated artifacts +app/release/ +*.apk +*.aab + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a3a9a1 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Emoney Info Android + +Android version of the iOS `Emoney Info` app. + +## Documentation +- Main code documentation: [docs/CODE_DOCUMENTATION.md](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/docs/CODE_DOCUMENTATION.md) + +## Current state +- Kotlin + Jetpack Compose application +- Home, History, Settings, FAQ, About, Terms, and Privacy screens +- NFC readers validated on real devices for Brizzi, Flazz, TapCash, KMT, JackCard, and Mandiri e-Money +- AdMob banner and interstitial units configured +- PDF export flow available from transaction history +- English and Indonesian localization enabled + +## Environment +- Package: `com.korancrew.emoneyinfo` +- minSdk: `28` +- compileSdk: `34` +- Android SDK root expected at `/opt/android-sdk` +- JDK: `17` or `21` + +## Build +Use Temurin 21 on this machine: + +```bash +JAVA_HOME=/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home \ +ANDROID_SDK_ROOT=/opt/android-sdk \ +./gradlew assembleDebug +``` + +For a production verification build: + +```bash +JAVA_HOME=/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home \ +ANDROID_SDK_ROOT=/opt/android-sdk \ +./gradlew assembleRelease +``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..523557e --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + namespace = "com.iiyh.emoneyinfo" + compileSdk = 36 + compileSdkMinor = 1 + + defaultConfig { + applicationId = "com.iiyh.emoneyinfo" + minSdk = 28 + targetSdk = 36 + versionCode = 2 + versionName = "1.0.0" + + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + buildConfig = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2024.06.00") + + implementation("androidx.core:core-ktx:1.18.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") + implementation("androidx.activity:activity-compose:1.13.0") + implementation("androidx.navigation:navigation-compose:2.9.7") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("com.google.android.material:material:1.13.0") + implementation(composeBom) + implementation("com.google.android.gms:play-services-ads:25.2.0") + + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..eb4aef7 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ +# Intentionally empty for now. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b6c4bc5 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..664f0ad1f22a7761bf363dbe81872530c038446e GIT binary patch literal 8862 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 + initializationStatus.adapterStatusMap.forEach { (name, status) -> + AppLog.d( + "EmoneyInfoAds", + "Adapter=$name state=${status.initializationState} latency=${status.latency} desc=${status.description}" + ) + } + adsEnabled.value = true + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ads/AdMobConfig.kt b/app/src/main/java/com/iiyh/emoneyinfo/ads/AdMobConfig.kt new file mode 100644 index 0000000..6739be9 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ads/AdMobConfig.kt @@ -0,0 +1,13 @@ +package com.iiyh.emoneyinfo.ads + +object AdMobConfig { + const val APP_ID = "ca-app-pub-3389368171983845~3596282656" + const val BANNER_HOME = "ca-app-pub-3389368171983845/3971687176" + const val BANNER_SETTINGS = "ca-app-pub-3389368171983845/1794140038" + const val BANNER_HISTORY = "ca-app-pub-3389368171983845/7102307034" + const val INTERSTITIAL_PDF = "ca-app-pub-3389368171983845/7236992223" + val TEST_DEVICE_IDS = listOf( + "33BE2250B43518CCDA7DE426D04EE231", + "463419B65276BB5AEDB52AC2A947CA1C" + ) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/data/FaqData.kt b/app/src/main/java/com/iiyh/emoneyinfo/data/FaqData.kt new file mode 100644 index 0000000..66d8b64 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/data/FaqData.kt @@ -0,0 +1,48 @@ +package com.iiyh.emoneyinfo.data + +import androidx.annotation.StringRes +import com.iiyh.emoneyinfo.R + +data class FaqItem( + @param:StringRes val questionRes: Int, + @param:StringRes val answerRes: Int +) + +data class FaqCategory( + @param:StringRes val titleRes: Int, + val items: List +) + +object FaqData { + val all = listOf( + FaqCategory( + titleRes = R.string.faq_category_cards, + items = listOf( + FaqItem(R.string.faq_q_supported_cards, R.string.faq_a_supported_cards), + FaqItem(R.string.faq_q_card_not_detected, R.string.faq_a_card_not_detected), + FaqItem(R.string.faq_q_card_read_failed, R.string.faq_a_card_read_failed) + ) + ), + FaqCategory( + titleRes = R.string.faq_category_transactions, + items = listOf( + FaqItem(R.string.faq_q_transactions_not_shown, R.string.faq_a_transactions_not_shown), + FaqItem(R.string.faq_q_export_pdf, R.string.faq_a_export_pdf) + ) + ), + FaqCategory( + titleRes = R.string.faq_category_balance, + items = listOf( + FaqItem(R.string.faq_q_balance_wrong, R.string.faq_a_balance_wrong), + FaqItem(R.string.faq_q_balance_topup, R.string.faq_a_balance_topup) + ) + ), + FaqCategory( + titleRes = R.string.faq_category_app, + items = listOf( + FaqItem(R.string.faq_q_app_language, R.string.faq_a_app_language), + FaqItem(R.string.faq_q_hide_card_number, R.string.faq_a_hide_card_number) + ) + ) + ) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/data/Models.kt b/app/src/main/java/com/iiyh/emoneyinfo/data/Models.kt new file mode 100644 index 0000000..749a871 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/data/Models.kt @@ -0,0 +1,78 @@ +package com.iiyh.emoneyinfo.data + +import androidx.annotation.StringRes +import com.iiyh.emoneyinfo.R +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.math.abs + +enum class CardType(@param:StringRes val labelRes: Int) { + UNKNOWN(R.string.card_unknown), + MANDIRI(R.string.card_mandiri), + FLAZZ(R.string.card_flazz), + BRIZZI(R.string.card_brizzi), + TAPCASH(R.string.card_tapcash), + JACKCARD(R.string.card_jackcard), + MEGACASH(R.string.card_megacash), + KMT(R.string.card_kmt) +} + +data class TransactionItem( + val title: String, + val date: Date, + val amount: Long, + val isCredit: Boolean, + val locationName: String = "" +) { + fun formattedAmount(): String { + val formatter = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("id-ID")).apply { + maximumFractionDigits = 0 + currency = java.util.Currency.getInstance("IDR") + } + val raw = formatter.format(abs(amount)).replace("IDR", "Rp") + return if (isCredit) "+$raw" else "-$raw" + } + + fun formattedDate(): String = SimpleDateFormat("dd MMM yyyy · HH:mm", Locale.forLanguageTag("id-ID")).format(date) + + fun subtitle(): String = buildString { + append(formattedDate()) + if (locationName.isNotBlank()) { + append(" · ") + append(locationName) + } + } +} + +data class EmoneyUiState( + val cardType: CardType = CardType.UNKNOWN, + val balance: Long = 0, + val cardNumber: String = "", + val transactions: List = emptyList(), + val scanMessage: String = "", + val isNfcSupported: Boolean = true +) { + fun formattedBalance(): String { + val formatter = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("id-ID")).apply { + maximumFractionDigits = 0 + currency = java.util.Currency.getInstance("IDR") + } + return formatter.format(balance).replace("IDR", "Rp") + } + + fun hasCardData(): Boolean = cardType != CardType.UNKNOWN || cardNumber.isNotBlank() || balance > 0 || transactions.isNotEmpty() +} + +fun String.formatCardNumber(): String { + val digits = filter { it.isDigit() } + if (digits.isEmpty()) return this + return digits.chunked(4).joinToString(" ") +} + +fun String.maskFirst12(): String { + val digits = filter { it.isDigit() } + if (digits.length != 16) return formatCardNumber() + return "**** **** **** ${digits.takeLast(4)}" +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/nfc/AndroidStrings.kt b/app/src/main/java/com/iiyh/emoneyinfo/nfc/AndroidStrings.kt new file mode 100644 index 0000000..e02b208 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/nfc/AndroidStrings.kt @@ -0,0 +1,8 @@ +package com.iiyh.emoneyinfo.nfc + +import android.content.Context +import com.iiyh.emoneyinfo.R + +internal class AndroidStrings(private val context: Context) { + fun get(id: Int, vararg args: Any): String = context.getString(id, *args) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/nfc/BrizziCrypto.kt b/app/src/main/java/com/iiyh/emoneyinfo/nfc/BrizziCrypto.kt new file mode 100644 index 0000000..1ae48f8 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/nfc/BrizziCrypto.kt @@ -0,0 +1,85 @@ +package com.iiyh.emoneyinfo.nfc + +import java.security.GeneralSecurityException +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +internal object BrizziCrypto { + private const val AUTH_KEY = "0000030080000000" + private const val MASTER_3DES_KEY = "C152153D5807784C721A433B5B59636DC152153D5807784C" + + fun decryptDeSeDe(data: ByteArray): ByteArray = + tripleDesCbc(data, MASTER_3DES_KEY.hexToBytes(), ByteArray(8), Cipher.DECRYPT_MODE) + + fun encryptDeSeDe(inputHex: String, keyHex: String, ivHex: String): ByteArray { + val normalizedKey = when (keyHex.length) { + 48 -> keyHex + 32 -> keyHex + keyHex.take(16) + 16 -> keyHex + keyHex + keyHex + else -> "00000000000000000000000000000000" + } + return tripleDesCbc( + inputHex.hexToBytes(), + normalizedKey.hexToBytes(), + ivHex.hexToBytes(), + Cipher.ENCRYPT_MODE + ) + } + + fun encrypt(hex: String, keyHex: String): ByteArray { + val left = keyHex.take(16) + val right = keyHex.drop(16).take(16) + val firstDecrypt = decrypt(hex, left).toHex() + val secondEncrypt = desCbc(firstDecrypt.hexToBytes(), right.hexToBytes(), Cipher.ENCRYPT_MODE).toHex() + return decrypt(secondEncrypt, left) + } + + fun decrypt(hex: String, keyHex: String): ByteArray = + desCbc(hex.hexToBytes(), keyHex.hexToBytes(), Cipher.DECRYPT_MODE) + + fun mix(left: ByteArray, right: ByteArray): ByteArray { + require(right.isNotEmpty()) { "empty security key" } + return ByteArray(left.size) { index -> + (left[index].toInt() xor right[index % right.size].toInt()).toByte() + } + } + + fun generateSamRandom(keyCardHex: String, randomHex: String): String { + val mixed = mix(encrypt(keyCardHex, randomHex), "0000000000000000".hexToBytes()) + val sam = mixed.toHex().take(16) + val rotated = sam.hexToBytes().rotateLeftBytes(1).toHex() + val result = encrypt( + mix("1122334455667788".hexToBytes(), keyCardHex.hexToBytes()).toHex(), + randomHex + ).toHex().take(16) + + val tail = encrypt( + mix(rotated.hexToBytes(), result.hexToBytes()).toHex(), + randomHex + ).toHex() + + return result + tail + } + + fun authKey(): String = AUTH_KEY + + @Throws(GeneralSecurityException::class) + private fun tripleDesCbc( + input: ByteArray, + key: ByteArray, + iv: ByteArray, + mode: Int + ): ByteArray { + val cipher = Cipher.getInstance("DESede/CBC/NoPadding") + cipher.init(mode, SecretKeySpec(key, "DESede"), IvParameterSpec(iv)) + return cipher.doFinal(input) + } + + @Throws(GeneralSecurityException::class) + private fun desCbc(input: ByteArray, key: ByteArray, mode: Int): ByteArray { + val cipher = Cipher.getInstance("DES/CBC/NoPadding") + cipher.init(mode, SecretKeySpec(key, "DES"), IvParameterSpec(ByteArray(8))) + return cipher.doFinal(input) + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/nfc/CardReaders.kt b/app/src/main/java/com/iiyh/emoneyinfo/nfc/CardReaders.kt new file mode 100644 index 0000000..6ad1342 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/nfc/CardReaders.kt @@ -0,0 +1,861 @@ +package com.iiyh.emoneyinfo.nfc + +import android.nfc.Tag +import android.nfc.tech.IsoDep +import android.nfc.tech.NfcF +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.data.CardType +import com.iiyh.emoneyinfo.data.EmoneyUiState +import com.iiyh.emoneyinfo.data.TransactionItem +import com.iiyh.emoneyinfo.util.AppLog +import java.nio.charset.Charset +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +internal interface CardReader { + fun canHandle(tag: Tag): Boolean + fun read(tag: Tag, strings: AndroidStrings): EmoneyUiState +} + +internal class IsoDepCardRouter : CardReader { + override fun canHandle(tag: Tag): Boolean = IsoDep.get(tag) != null + + override fun read(tag: Tag, strings: AndroidStrings): EmoneyUiState { + val isoDep = IsoDep.get(tag) ?: error("IsoDep not available") + isoDep.connect() + isoDep.timeout = 5000 + return try { + BrizziReader.read(isoDep, strings) + ?: MandiriReader.read(isoDep, strings) + ?: FlazzReader.read(isoDep, strings) + ?: TapCashReader.read(isoDep, strings) + ?: JackCardReader.read(isoDep, strings) + ?: MegaCashReader.read(isoDep, strings) + ?: error(strings.get(R.string.card_not_supported)) + } finally { + runCatching { isoDep.close() } + } + } +} + +internal class FelicaCardReader : CardReader { + override fun canHandle(tag: Tag): Boolean = NfcF.get(tag) != null + + override fun read(tag: Tag, strings: AndroidStrings): EmoneyUiState { + val nfcF = NfcF.get(tag) ?: error("NfcF not available") + nfcF.connect() + nfcF.timeout = 5000 + return try { + KmtReader.read(nfcF, strings) + } finally { + runCatching { nfcF.close() } + } + } +} + +private object BrizziReader { + fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? { + AppLog.d("EmoneyInfoBrizzi", "Starting Brizzi detection") + val init = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x5A, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x01, 0x00, 0x00), + le = 0x00 + ) + AppLog.d("EmoneyInfoBrizzi", "Init status=${"%02X%02X".format(init.sw1, init.sw2)}") + if (!(init.hasStatus(0x91, 0xAF) || init.hasStatus(0x91, 0x00))) return null + + val uid = readBrizziUid(isoDep) + AppLog.d("EmoneyInfoBrizzi", "UID read complete") + val cardNumberResp = listOf( + byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00), + byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00) + ).asSequence().map { payload -> + isoDep.transceiveApdu( + cla = 0x90, + ins = 0xBD, + p1 = 0x00, + p2 = 0x00, + data = payload, + le = 0x00 + ) + }.firstOrNull { it.hasStatus(0x91, 0x00) } ?: isoDep.transceiveApdu( + cla = 0x90, + ins = 0xBD, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00), + le = 0x00 + ) + AppLog.d("EmoneyInfoBrizzi", "Card number status=${"%02X%02X".format(cardNumberResp.sw1, cardNumberResp.sw2)}") + require(cardNumberResp.hasStatus(0x91, 0x00)) { strings.get(R.string.error_brizzi_card_number) } + val cardNumber = cardNumberResp.data.toHex().safeSlice(6, 22).formatCardNumber() + AppLog.d("EmoneyInfoBrizzi", "Card number parsed") + + val process01 = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x5A, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x03, 0x00, 0x00), + le = 0x00 + ) + AppLog.d("EmoneyInfoBrizzi", "Process01 status=${"%02X%02X".format(process01.sw1, process01.sw2)}") + require(process01.hasStatus(0x91, 0x00)) { strings.get(R.string.error_brizzi_step1) } + + val process02 = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x0A, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x00), + le = 0x00 + ) + AppLog.d("EmoneyInfoBrizzi", "Process02 status=${"%02X%02X".format(process02.sw1, process02.sw2)}") + require(process02.hasStatus(0x91, 0x00) || process02.hasStatus(0x91, 0xAF)) { + strings.get(R.string.error_brizzi_step2) + } + + val keyCard = process02.data.toHex() + val decrypted = BrizziCrypto.decryptDeSeDe("8DC0DC40FE1DC582CF7099E2AACFBC10".hexToBytes()).toHex().take(32) + val encryptedKey = BrizziCrypto.encryptDeSeDe( + inputHex = cardNumber.replace(" ", "") + uid + "FF", + keyHex = decrypted, + ivHex = "0000000000000000" + ).toHex().take(32) + + val random = BrizziCrypto.encryptDeSeDe( + inputHex = encryptedKey, + keyHex = BrizziCrypto.decryptDeSeDe("3C37029CA595FE4E7E62FCB2F7909B2C".hexToBytes()).toHex().take(32), + ivHex = BrizziCrypto.authKey() + ).toHex().take(32) + + val samChallenge = BrizziCrypto.generateSamRandom(keyCard, random).take(32) + AppLog.d("EmoneyInfoBrizzi", "Generated Brizzi challenge") + val process03 = isoDep.transceiveApdu( + cla = 0x90, + ins = 0xAF, + p1 = 0x00, + p2 = 0x00, + data = samChallenge.hexToBytes(), + le = 0x00 + ) + AppLog.d("EmoneyInfoBrizzi", "Process03 status=${"%02X%02X".format(process03.sw1, process03.sw2)}") + require(process03.hasStatus(0x91, 0x00) || process03.hasStatus(0x91, 0xAF)) { + strings.get(R.string.error_brizzi_step3) + } + + val balanceResp = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x6C, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x00), + le = 0x00 + ) + AppLog.d("EmoneyInfoBrizzi", "Balance status=${"%02X%02X".format(balanceResp.sw1, balanceResp.sw2)}") + require(balanceResp.hasStatus(0x91, 0x00) || balanceResp.hasStatus(0x91, 0xAF)) { + strings.get(R.string.error_brizzi_balance) + } + val balance = balanceResp.data.toHex().safeSlice(0, 8).reverseByteOrderHex().hexToLong() + AppLog.d("EmoneyInfoBrizzi", "Balance parsed") + + val logStart = isoDep.transceiveApdu( + cla = 0x90, + ins = 0xBB, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + le = 0x00 + ) + AppLog.d("EmoneyInfoBrizzi", "Log start status=${"%02X%02X".format(logStart.sw1, logStart.sw2)}") + + val rawLog = StringBuilder() + if (logStart.hasStatus(0x91, 0x00) || logStart.hasStatus(0x91, 0xAF)) { + rawLog.append(logStart.data.toHex()) + while (true) { + val more = isoDep.transceiveApdu(cla = 0x90, ins = 0xAF, p1 = 0x00, p2 = 0x00, le = 0x00) + rawLog.append(more.data.toHex()) + if (!more.hasStatus(0x91, 0xAF)) break + } + } + + val transactions = parseBrizziLogs(rawLog.toString(), strings) + AppLog.d("EmoneyInfoBrizzi", "Parsed transactions=${transactions.size}") + return EmoneyUiState( + cardType = CardType.BRIZZI, + cardNumber = cardNumber, + balance = balance, + transactions = transactions.sortedByDescending { it.date }, + scanMessage = if (transactions.isEmpty()) { + strings.get(R.string.scan_brizzi_success) + } else { + strings.get(R.string.scan_brizzi_history_success) + } + ) + } + + private fun readBrizziUid(isoDep: IsoDep): String { + val first = isoDep.transceiveApdu(cla = 0x90, ins = 0x60, p1 = 0x00, p2 = 0x00, le = 0x00) + AppLog.d("EmoneyInfoBrizzi", "UID step1 status=${"%02X%02X".format(first.sw1, first.sw2)}") + require(first.hasStatus(0x91, 0xAF)) { "Failed to start Brizzi UID read" } + while (true) { + val next = isoDep.transceiveApdu(cla = 0x90, ins = 0xAF, p1 = 0x00, p2 = 0x00, le = 0x00) + AppLog.d("EmoneyInfoBrizzi", "UID next step status=${"%02X%02X".format(next.sw1, next.sw2)}") + if (next.hasStatus(0x91, 0xAF)) continue + return next.data.toHex().take(14) + } + } + + private fun parseBrizziLogs(logs: String, strings: AndroidStrings): List { + if (logs.isBlank() || logs.length % 64 != 0) return emptyList() + return buildList { + for (offset in logs.indices step 64) { + val chunk = logs.substring(offset, offset + 64) + val date = parseDdmmyyHhmmss( + datePart = chunk.substring(32, 38), + timePart = chunk.substring(38, 44) + ) ?: continue + val code = chunk.substring(44, 46).uppercase(Locale.US) + val title = when (code) { + "5F" -> strings.get(R.string.tx_reactivation) + "EB" -> strings.get(R.string.payment) + "EC" -> strings.get(R.string.topup) + "ED" -> strings.get(R.string.tx_void) + "EF" -> strings.get(R.string.tx_update_balance) + else -> strings.get(R.string.tx_transaction) + } + val isCredit = code == "EC" || code == "ED" + add( + TransactionItem( + title = title, + date = date, + amount = chunk.substring(46, 52).reverseByteOrderHex().hexToLong(), + isCredit = isCredit + ) + ) + } + } + } +} + +private object FlazzReader { + private val selectDfAid = "A0000000180F0000018001".hexToBytes() + + fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? { + AppLog.d("EmoneyInfoFlazz", "Starting IsoDep Flazz detection") + val select = isoDep.transceiveSelect(selectDfAid) + AppLog.d("EmoneyInfoFlazz", "Select DF status=${"%02X%02X".format(select.sw1, select.sw2)}") + val fallbackSelect = if (!select.isSuccess()) { + isoDep.transceiveApdu( + cla = 0x00, + ins = 0xA4, + p1 = 0x01, + p2 = 0x00, + data = byteArrayOf(0x02, 0x00), + le = 0x00 + ) + } else { + select + } + AppLog.d("EmoneyInfoFlazz", "Fallback select status=${"%02X%02X".format(fallbackSelect.sw1, fallbackSelect.sw2)}") + if (!fallbackSelect.isSuccess()) return null + + val cardInfo = isoDep.transceiveApdu( + cla = 0x00, + ins = 0xB0, + p1 = 0x81, + p2 = 0x00, + le = 0x8E + ) + AppLog.d("EmoneyInfoFlazz", "Card info status=${"%02X%02X".format(cardInfo.sw1, cardInfo.sw2)}") + val cardNumber = if (cardInfo.isSuccess()) { + cardInfo.data.toString(Charset.forName("ISO-8859-1")) + .substringAfter(';', "") + .substringBefore('=', "") + .trim() + } else { + "" + } + + val balanceResp = isoDep.transceiveApdu( + cla = 0x80, + ins = 0x32, + p1 = 0x00, + p2 = 0x03, + data = byteArrayOf(0x00, 0x00, 0x00, 0x00), + le = 0x00 + ) + AppLog.d("EmoneyInfoFlazz", "Balance status=${"%02X%02X".format(balanceResp.sw1, balanceResp.sw2)}") + val balance = if (balanceResp.isSuccess() && balanceResp.data.size >= 4) { + balanceResp.data.toHex().substring(2, 8).hexToLong() + } else { + 0L + } + + val transactions = if (cardInfo.isSuccess()) { + readFlazzHistory(isoDep, strings) + } else { + emptyList() + } + + return EmoneyUiState( + cardType = CardType.FLAZZ, + cardNumber = cardNumber.formatCardNumber(), + balance = balance, + transactions = transactions.sortedByDescending { it.date }, + scanMessage = if (transactions.isEmpty()) { + strings.get(R.string.scan_flazz_success) + } else { + strings.get(R.string.scan_flazz_history_success) + } + ) + } + + private fun readFlazzHistory(isoDep: IsoDep, strings: AndroidStrings): List { + val logCheck = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x81, p2 = 0x00, le = 0x00) + return if (logCheck.isSuccess()) { + readV2History(isoDep, strings) + } else { + readV1History(isoDep, strings) + } + } + + private fun readV1History(isoDep: IsoDep, strings: AndroidStrings): List { + val part1 = StringBuilder() + + for (index in 0 until 16) { + val offset = index * 15 + val first = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x84, p2 = offset, le = 60) + if (!first.isSuccess()) break + part1.append(first.data.toHex()) + } + for (index in 0 until 16) { + val offset = index * 15 + val second = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x85, p2 = offset, le = 60) + if (!second.isSuccess()) break + part1.append(second.data.toHex()) + } + return parseFlazz120Logs(part1.toString(), strings) + } + + private fun readV2History(isoDep: IsoDep, strings: AndroidStrings): List { + val mapV1 = StringBuilder() + val mapV2 = StringBuilder() + + for (index in 0 until 5) { + val offset = index * 60 + val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x85, p2 = offset, le = 120) + if (!resp.isSuccess()) break + mapV1.append(resp.data.toHex()) + } + for (index in 0 until 5) { + val offset = index * 60 + val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x84, p2 = offset, le = 240) + if (!resp.isSuccess()) break + mapV1.append(resp.data.toHex()) + } + + val random = isoDep.transceiveApdu(cla = 0x00, ins = 0x84, p1 = 0x00, p2 = 0x00, le = 8) + if (random.isSuccess()) { + val auth = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x32, + p1 = 0x03, + p2 = 0x00, + data = ("0801" + random.data.toHex()).hexToBytes(), + le = 41 + ) + if (auth.isSuccess()) { + for (index in 0 until 256) { + val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x89, p2 = index, le = 64) + if (!resp.isSuccess()) break + mapV2.append(resp.data.toHex()) + } + for (index in 0 until 256) { + val resp = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x32, + p1 = 0x03, + p2 = 0x00, + data = byteArrayOf(index.toByte()), + le = 32 + ) + if (!resp.isSuccess()) break + mapV2.append(resp.data.toHex()) + } + } + } + + return (parseFlazz120Logs(mapV1.toString(), strings) + parseFlazz64Logs(mapV2.toString(), strings)) + } + + private fun parseFlazz120Logs(logs: String, strings: AndroidStrings): List { + if (logs.isBlank() || logs.length % 120 != 0) return emptyList() + return buildList { + for (offset in logs.indices step 120) { + val chunk = logs.substring(offset, offset + 120) + val transactionTime = chunk.substring(76, 84).hexToLong() + if (transactionTime <= 0) continue + val amount = chunk.substring(12, 18).hexToLong() + val type = chunk.substring(0, 4).hexToLong() + add( + TransactionItem( + title = if (type == 1024L) strings.get(R.string.payment) else strings.get(R.string.topup), + date = flazzSecondsFrom1980(transactionTime), + amount = amount, + isCredit = type != 1024L + ) + ) + } + } + } + + private fun parseFlazz64Logs(logs: String, strings: AndroidStrings): List { + if (logs.isBlank() || logs.length % 64 != 0) return emptyList() + return buildList { + for (offset in logs.indices step 64) { + val chunk = logs.substring(offset, offset + 64) + val transactionTime = chunk.substring(8, 16).hexToLong() + if (transactionTime <= 0) continue + val type = chunk.substring(0, 2).hexToLong() + val rawAmount = chunk.substring(2, 8).hexToLong() + val amount = if (type == 4L) 16777216L - rawAmount else rawAmount + add( + TransactionItem( + title = if (type == 4L) strings.get(R.string.payment) else strings.get(R.string.topup), + date = flazzSecondsFrom1980(transactionTime), + amount = amount, + isCredit = type != 4L + ) + ) + } + } + } +} + +private object TapCashReader { + private val aid = "A000424E49100001".hexToBytes() + + fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? { + val select = isoDep.transceiveSelect(aid) + if (!select.isSuccess()) return null + + val purse = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x32, + p1 = 0x03, + p2 = 0x00, + le = 0x00 + ) + if (!purse.isSuccess() || purse.data.size < 64) { + return EmoneyUiState( + cardType = CardType.TAPCASH, + scanMessage = strings.get(R.string.scan_tapcash_detected_partial) + ) + } + + val balance = purse.data.copyOfRange(2, 5).toHex().hexToLong() + val cardNumber = purse.data.copyOfRange(8, 16).toHex().formatCardNumber() + val totalRecords = purse.data.getOrNull(40)?.toInt()?.and(0xFF)?.coerceAtMost(10) ?: 0 + val history = mutableListOf() + + for (index in 0 until totalRecords) { + val resp = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x32, + p1 = 0x03, + p2 = 0x00, + data = byteArrayOf(index.toByte()), + le = 0x10 + ) + if (!resp.isSuccess() || resp.data.size < 8) continue + parseTapCashRecord(resp.data, strings)?.let(history::add) + } + + return EmoneyUiState( + cardType = CardType.TAPCASH, + cardNumber = cardNumber, + balance = balance, + transactions = history.sortedByDescending { it.date }, + scanMessage = if (history.isEmpty()) { + strings.get(R.string.scan_tapcash_success) + } else { + strings.get(R.string.scan_tapcash_history_success) + } + ) + } + + private fun parseTapCashRecord(data: ByteArray, strings: AndroidStrings): TransactionItem? { + if (data.size < 8) return null + val header = data.copyOfRange(0, 1).toHex().uppercase(Locale.US) + val amountBytes = data.copyOfRange(1, 4).toHex() + val amount = when (header) { + "01", "05", "07", "10", "20" -> amountBytes.twosComplementHexToLong() + else -> amountBytes.hexToLong() + } + val title = when (header) { + "01" -> strings.get(R.string.payment) + "02" -> strings.get(R.string.tx_black_list_card) + "03", "04" -> strings.get(R.string.topup) + "05" -> strings.get(R.string.tx_statement_fee) + "06" -> strings.get(R.string.tx_update_balance) + "07" -> strings.get(R.string.tx_grace_period) + "10", "20" -> strings.get(R.string.tx_refund) + "22" -> strings.get(R.string.tx_close) + "F0" -> strings.get(R.string.tx_atu) + else -> strings.get(R.string.tx_transaction) + } + val processType = when (header) { + "03", "04" -> true + else -> false + } + val date = julianSecondsFrom1995(data.copyOfRange(4, 8).toHex().hexToLong()) + + return TransactionItem( + title = title, + date = date, + amount = amount, + isCredit = processType + ) + } +} + +private object MandiriReader { + private val aid = "0000000000000001".hexToBytes() + + fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? { + AppLog.d("EmoneyInfoMandiri", "Starting Mandiri detection") + val select = isoDep.transceiveSelect(aid) + AppLog.d("EmoneyInfoMandiri", "Select status=${"%02X%02X".format(select.sw1, select.sw2)}") + if (!select.isSuccess()) return null + + val cardResp = isoDep.transceiveApdu(0x00, 0xB3, 0x00, 0x00, le = 0x3F) + AppLog.d("EmoneyInfoMandiri", "Card response status=${"%02X%02X".format(cardResp.sw1, cardResp.sw2)}") + if (!cardResp.isSuccess()) error(strings.get(R.string.error_mandiri_card_number)) + val cardHex = cardResp.data.toHex() + val cardNumber = cardHex.safeSlice(0, 16).formatCardNumber() + val cardType = cardHex.safeSlice(36, 38).hexToIntOrZero() + AppLog.d("EmoneyInfoMandiri", "Card data parsed type=$cardType") + + val balanceResp = isoDep.transceiveApdu(0x00, 0xB5, 0x00, 0x00, le = 0x0A) + AppLog.d("EmoneyInfoMandiri", "Balance status=${"%02X%02X".format(balanceResp.sw1, balanceResp.sw2)}") + if (!balanceResp.isSuccess()) error(strings.get(R.string.error_mandiri_balance)) + val balance = balanceResp.data.toHex().safeSlice(0, 8).reverseByteOrderHex().hexToLong() + + val transactions = if (cardType == 131) { + readNewLogs(isoDep, strings) + } else { + readOldLogs(isoDep, strings) + } + AppLog.d("EmoneyInfoMandiri", "Transactions=${transactions.size}") + + return EmoneyUiState( + cardType = CardType.MANDIRI, + cardNumber = cardNumber, + balance = balance, + transactions = transactions.sortedByDescending { it.date }, + scanMessage = if (cardType == 131) { + strings.get(R.string.scan_mandiri_history_success) + } else if (transactions.isNotEmpty()) { + strings.get(R.string.scan_mandiri_history_success) + } else { + strings.get(R.string.scan_mandiri_success) + } + ) + } + + private fun readNewLogs(isoDep: IsoDep, strings: AndroidStrings): List { + val raw = StringBuilder() + for (index in 0 until 256) { + val resp = isoDep.transceiveApdu( + cla = 0x00, + ins = 0xD1, + p1 = index and 0xFF, + p2 = 0x00, + le = 0x00 + ) + if (!resp.isSuccess()) break + raw.append(resp.data.toHex()) + } + + val logs = raw.toString() + if (logs.isEmpty() || logs.length % 48 != 0) return emptyList() + + return buildList { + for (offset in logs.indices step 48) { + val chunk = logs.substring(offset, offset + 48) + val date = parseDdmmyyHhmmss( + datePart = chunk.substring(0, 6), + timePart = chunk.substring(6, 12) + ) ?: continue + val processType = chunk.substring(28, 32).toIntOrNull() ?: 0 + val amount = chunk.substring(32, 40).reverseByteOrderHex().hexToLong() + add( + TransactionItem( + title = if (processType == 100) strings.get(R.string.topup) else strings.get(R.string.payment), + date = date, + amount = amount, + isCredit = processType == 100 + ) + ) + } + } + } + + private fun readOldLogs(isoDep: IsoDep, strings: AndroidStrings): List { + val raw = StringBuilder() + for (index in 0 until 10) { + val resp = isoDep.transceiveApdu( + cla = 0x00, + ins = 0xB2, + p1 = index and 0xFF, + p2 = 0x00, + le = 0x1E + ) + AppLog.d( + "EmoneyInfoMandiri", + "Old log index=$index status=${"%02X%02X".format(resp.sw1, resp.sw2)}" + ) + if (!resp.isSuccess()) break + raw.append(resp.data.toHex()) + } + + val logs = raw.toString() + if (logs.isEmpty() || logs.length % 60 != 0) return emptyList() + + return buildList { + for (offset in logs.indices step 60) { + val chunk = logs.substring(offset, offset + 60) + parseOldMandiriLog(chunk, strings)?.let(::add) + } + } + } + + private fun parseOldMandiriLog(chunk: String, strings: AndroidStrings): TransactionItem? { + if (chunk.length < 60) return null + val bytes = chunk.hexToBytes() + if (bytes.size < 30) return null + + val timestampBytes = bytes.copyOfRange(0, 6) + val amountBytes = bytes.copyOfRange(10, 14) + val detailBytes = bytes.copyOfRange(18, 30) + + val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Jakarta")).apply { + set( + 2000 + (timestampBytes[0].toInt() and 0xFF), + ((timestampBytes[1].toInt() and 0xFF) - 1).coerceAtLeast(0), + (timestampBytes[2].toInt() and 0xFF).coerceAtLeast(1), + timestampBytes[3].toInt() and 0xFF, + timestampBytes[4].toInt() and 0xFF, + timestampBytes[5].toInt() and 0xFF + ) + set(Calendar.MILLISECOND, 0) + } + + val amount = amountBytes.littleEndianLong() + val descriptor = detailBytes.toHex() + val isCredit = descriptor.contains("64") + val title = if (isCredit) strings.get(R.string.topup) else strings.get(R.string.payment) + + return TransactionItem( + title = title, + date = calendar.time, + amount = amount, + isCredit = isCredit + ) + } +} + +private object JackCardReader { + private val aid = "A0000005714E4A43".hexToBytes() + + fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? { + val select = isoDep.transceiveSelect(aid) + if (!select.isSuccess()) return null + + val cardNumber = select.data.toHex().safeSlice(16, 32).formatCardNumber() + val balanceResp = isoDep.transceiveApdu(0x90, 0x4C, 0x00, 0x00, le = 0x04) + val balance = if (balanceResp.isSuccess()) balanceResp.data.toHex().hexToLong() else 0L + + return EmoneyUiState( + cardType = CardType.JACKCARD, + cardNumber = cardNumber, + balance = balance, + scanMessage = strings.get(R.string.scan_jackcard_success) + ) + } +} + +private object MegaCashReader { + fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? { + val init = isoDep.transceiveApdu( + cla = 0x90, + ins = 0xBD, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00), + le = 0x00 + ) + if (!init.isSuccess()) return null + + val cardNumber = init.data.toHex().drop(4).formatCardNumber() + val balanceResp = isoDep.transceiveApdu( + cla = 0x90, + ins = 0x6C, + p1 = 0x00, + p2 = 0x00, + data = byteArrayOf(0x02), + le = 0x00 + ) + val balance = if (balanceResp.isSuccess()) { + balanceResp.data.toHex().reverseByteOrderHex().hexToLong() + } else { + 0L + } + + return EmoneyUiState( + cardType = CardType.MEGACASH, + cardNumber = cardNumber, + balance = balance, + scanMessage = strings.get(R.string.scan_megacash_success) + ) + } +} + +private object KmtReader { + private val stationMap = mapOf( + 0 to "PARKIR RESKA", + 1 to "Tanah Abang", + 67 to "C-Access", + 257 to "Bogor", + 258 to "Cilebut", + 259 to "Bojonggede", + 260 to "Citayam", + 261 to "Depok", + 262 to "Depok Baru", + 263 to "Univ. Indonesia", + 264 to "Univ. Indonesia", + 265 to "Univ. Pancasila", + 272 to "Lenteng Agung", + 273 to "Tanjung Barat", + 274 to "Pasar Minggu", + 275 to "Pasar Minggu Baru", + 276 to "Duren Kalibata", + 277 to "Cawang", + 278 to "Tebet", + 279 to "Manggarai", + 280 to "Cikini", + 281 to "Gondangdia", + 288 to "Juanda", + 289 to "Sawah Besar", + 290 to "Mangga Besar", + 291 to "Jayakarta", + 292 to "Jakarta Kota", + 293 to "Bekasi", + 294 to "Kranji", + 295 to "Cakung", + 296 to "Klender Baru", + 297 to "Buaran", + 304 to "Klender", + 305 to "Jatinegara", + 313 to "Tangerang", + 327 to "Karet", + 328 to "Sudirman", + 329 to "Tanah Abang", + 336 to "Palmerah", + 337 to "Kebayoran", + 338 to "Pondok Ranji", + 339 to "Jurang Mangu", + 340 to "Sudimara", + 341 to "Rawabuntu", + 342 to "Serpong", + 343 to "Cisauk", + 344 to "Cicayur", + 345 to "Parung Panjang", + 352 to "Cilejit", + 353 to "Daru", + 354 to "Tenjo", + 355 to "Tigaraksa", + 356 to "Maja", + 357 to "Citeras", + 358 to "Rangkasbitung", + 374 to "Bekasi Timur", + 376 to "Cikarang" + ) + + fun read(nfcF: NfcF, strings: AndroidStrings): EmoneyUiState { + val cardBlock = nfcF.readWithoutEncryption( + serviceCode = byteArrayOf(0x0B, 0x30), + blockNumbers = listOf(0) + ).firstOrNull() ?: error("Failed to read KMT card number") + val cardNumber = cardBlock.toString(Charsets.UTF_8).trim('\u0000', ' ').ifBlank { "KMT" } + + val balanceBlock = nfcF.readWithoutEncryption( + serviceCode = byteArrayOf(0x17, 0x10), + blockNumbers = listOf(0) + ).firstOrNull() ?: error("Failed to read KMT balance") + val balance = balanceBlock.copyOfRange(0, 4).littleEndianLong() + + val historyBlocks = nfcF.readWithoutEncryption( + serviceCode = byteArrayOf(0x0F, 0x20), + blockNumbers = (0 until 15).toList() + ) + val transactions = historyBlocks.mapNotNull { parseHistoryBlock(it, strings) }.sortedByDescending { it.date } + + return EmoneyUiState( + cardType = CardType.KMT, + cardNumber = cardNumber, + balance = balance, + transactions = transactions, + scanMessage = strings.get(R.string.scan_kmt_history_success) + ) + } + + private fun parseHistoryBlock(data: ByteArray, strings: AndroidStrings): TransactionItem? { + if (data.size < 16) return null + val stationId = data.copyOfRange(8, 10).bigEndianLong().toInt() + val type = data[10].toInt() and 0xFF + val isParking = stationId == 0 + var isCredit = type == 0x00 + val title = when (type) { + 0x00, 0x03 -> strings.get(R.string.topup) + 0x01 -> strings.get(R.string.payment) + else -> strings.get(R.string.payment) + } + if (type == 0x03){ + isCredit = true + } + val amount = if (isParking) { + data.copyOfRange(8, 12).bigEndianLong() + } else { + data.copyOfRange(4, 8).bigEndianLong() + } + val date = if (isParking) { + parseReskaDate(data) + } else { + kmtSecondsFrom2000(data.copyOfRange(0, 4).bigEndianLong()) + } + val location = stationMap[stationId]?.uppercase(Locale.getDefault()).orEmpty() + + return TransactionItem( + title = title, + date = date, + amount = amount, + isCredit = isCredit, + locationName = location + ) + } + + private fun parseReskaDate(data: ByteArray): Date { + val first16 = data.toHex().take(16) + return runCatching { + SimpleDateFormat("ddMMyyyyHHmmssSS", Locale.US).parse(first16) + }.getOrNull() ?: Date() + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt b/app/src/main/java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt new file mode 100644 index 0000000..b65025b --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt @@ -0,0 +1,192 @@ +package com.iiyh.emoneyinfo.nfc + +import android.nfc.tech.IsoDep +import android.nfc.tech.NfcF +import com.iiyh.emoneyinfo.util.AppLog +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +private const val NFC_LOG_TAG = "EmoneyInfoNfc" + +internal data class ApduResponse( + val data: ByteArray, + val sw1: Int, + val sw2: Int +) { + fun isSuccess(): Boolean = sw1 == 0x90 && sw2 == 0x00 + fun hasStatus(sw1: Int, sw2: Int): Boolean = this.sw1 == sw1 && this.sw2 == sw2 +} + +internal fun IsoDep.transceiveSelect(aid: ByteArray): ApduResponse = + transceiveApdu(cla = 0x00, ins = 0xA4, p1 = 0x04, p2 = 0x00, data = aid) + +internal fun IsoDep.transceiveApdu( + cla: Int, + ins: Int, + p1: Int, + p2: Int, + data: ByteArray? = null, + le: Int? = null +): ApduResponse { + val apdu = mutableListOf( + cla.toByte(), + ins.toByte(), + p1.toByte(), + p2.toByte() + ) + if (data != null) { + apdu += data.size.toByte() + apdu += data.toList() + } + if (le != null) { + apdu += if (le == 256) 0x00 else (le and 0xFF).toByte() + } + + val requestBytes = apdu.toByteArray() + AppLog.d( + NFC_LOG_TAG, + "APDU -> cla=%02X ins=%02X p1=%02X p2=%02X lc=%d le=%s data=%s".format( + cla and 0xFF, + ins and 0xFF, + p1 and 0xFF, + p2 and 0xFF, + data?.size ?: 0, + le?.toString() ?: "-", + data?.toHex() ?: "" + ) + ) + val response = try { + transceive(requestBytes) + } catch (error: Throwable) { + AppLog.e( + NFC_LOG_TAG, + "APDU !! cla=%02X ins=%02X failed: %s".format( + cla and 0xFF, + ins and 0xFF, + error.message ?: error.javaClass.simpleName + ), + error + ) + throw error + } + require(response.size >= 2) { "Invalid APDU response" } + val parsed = ApduResponse( + data = response.copyOf(response.size - 2), + sw1 = response[response.size - 2].toInt() and 0xFF, + sw2 = response[response.size - 1].toInt() and 0xFF + ) + AppLog.d( + NFC_LOG_TAG, + "APDU <- sw=%02X%02X data=%s".format(parsed.sw1, parsed.sw2, parsed.data.toHex()) + ) + return parsed +} + +internal fun NfcF.readWithoutEncryption( + serviceCode: ByteArray, + blockNumbers: List +): List { + val packet = mutableListOf() + packet += 0x00 + packet += 0x06 + packet += tag.id.toList() + packet += 0x01 + packet += serviceCode.toList() + packet += blockNumbers.size.toByte() + blockNumbers.forEach { blockNo -> + packet += 0x80.toByte() + packet += blockNo.toByte() + } + packet[0] = packet.size.toByte() + + val response = transceive(packet.toByteArray()) + require(response.size >= 13) { "Invalid FeliCa response" } + val status1 = response[10].toInt() and 0xFF + val status2 = response[11].toInt() and 0xFF + require(status1 == 0x00 && status2 == 0x00) { + "FeliCa status error: $status1/$status2" + } + val blockCount = response[12].toInt() and 0xFF + var offset = 13 + return buildList { + repeat(blockCount) { + add(response.copyOfRange(offset, offset + 16)) + offset += 16 + } + } +} + +internal fun ByteArray.toHex(): String = joinToString("") { "%02X".format(it) } + +internal fun String.hexToBytes(): ByteArray { + require(length % 2 == 0) { "Hex string must have even length" } + return chunked(2).map { it.toInt(16).toByte() }.toByteArray() +} + +internal fun String.hexToLong(): Long = if (isBlank()) 0 else toLong(16) + +internal fun String.hexToIntOrZero(): Int = toIntOrNull(16) ?: 0 + +internal fun String.reverseByteOrderHex(): String = + chunked(2).reversed().joinToString("") + +internal fun String.safeSlice(start: Int, end: Int): String { + if (length <= start) return "" + return substring(start, minOf(end, length)) +} + +internal fun String.formatCardNumber(): String = + chunked(4).joinToString(" ").trim() + +internal fun ByteArray.rotateLeftBytes(count: Int): ByteArray { + if (isEmpty()) return this + val shift = count % size + return copyOfRange(shift, size) + copyOfRange(0, shift) +} + +internal fun String.twosComplementHexToLong(): Long { + val bits = length * 4 + val value = hexToLong() + val signBit = 1L shl (bits - 1) + return if ((value and signBit) == 0L) value else value - (1L shl bits) +} + +internal fun ByteArray.bigEndianLong(): Long = + fold(0L) { acc, byte -> (acc shl 8) or (byte.toInt() and 0xFF).toLong() } + +internal fun ByteArray.littleEndianLong(): Long = + reversedArray().bigEndianLong() + +internal fun parseDdmmyyHhmmss(datePart: String, timePart: String): Date? { + return runCatching { + val raw = datePart + timePart + java.text.SimpleDateFormat("ddMMyyHHmmss", Locale.US).parse(raw) + }.getOrNull() +} + +internal fun julianSecondsFrom1995(seconds: Long): Date { + val calendar = Calendar.getInstance().apply { + timeZone = TimeZone.getTimeZone("UTC") + set(1995, Calendar.JANUARY, 1, 0, 0, 0) + set(Calendar.MILLISECOND, 0) + } + return Date(calendar.timeInMillis + (seconds * 1000)) +} + +internal fun kmtSecondsFrom2000(seconds: Long): Date { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Jakarta")).apply { + set(2000, Calendar.JANUARY, 1, 7, 0, 0) + set(Calendar.MILLISECOND, 0) + } + return Date(calendar.timeInMillis + (seconds * 1000)) +} + +internal fun flazzSecondsFrom1980(seconds: Long): Date { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Jakarta")).apply { + set(1980, Calendar.JANUARY, 1, 0, 0, 0) + set(Calendar.MILLISECOND, 0) + } + return Date(calendar.timeInMillis + (seconds * 1000)) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/nfc/UnifiedNfcReader.kt b/app/src/main/java/com/iiyh/emoneyinfo/nfc/UnifiedNfcReader.kt new file mode 100644 index 0000000..9c0ad7f --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/nfc/UnifiedNfcReader.kt @@ -0,0 +1,125 @@ +package com.iiyh.emoneyinfo.nfc + +import android.app.Activity +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.IsoDep +import android.nfc.tech.NfcF +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.data.EmoneyUiState +import com.iiyh.emoneyinfo.util.AppLog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class UnifiedNfcReader(private val context: Context) { + private val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(context) + private val strings = AndroidStrings(context) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private val readers: List = listOf(IsoDepCardRouter(), FelicaCardReader()) + private var resetMessageJob: Job? = null + private val _uiState = MutableStateFlow( + EmoneyUiState( + isNfcSupported = adapter != null, + scanMessage = currentScanMessage() + ) + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun refreshStatus() { + resetMessageJob?.cancel() + _uiState.value = _uiState.value.copy( + isNfcSupported = adapter != null, + scanMessage = when { + adapter == null -> context.getString(R.string.nfc_not_supported) + !adapter.isEnabled -> context.getString(R.string.nfc_disabled) + _uiState.value.hasCardData() -> _uiState.value.scanMessage + else -> currentScanMessage() + } + ) + } + + fun startScan() { + refreshStatus() + } + + fun onNewIntent(intent: Intent) { + resetMessageJob?.cancel() + val tag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) + } ?: return + AppLog.d("EmoneyInfoNfc", "onNewIntent techs=${tag.techList.joinToString()}") + val reader = readers.firstOrNull { it.canHandle(tag) } ?: return + runCatching { + _uiState.value = reader.read(tag, strings) + scheduleResetToRescanHint() + }.onFailure { + AppLog.e("EmoneyInfoNfc", "Scan failed: ${it.message}", it) + _uiState.value = _uiState.value.copy( + scanMessage = context.getString( + R.string.scan_failed_message, + it.message ?: context.getString(R.string.unknown_error) + ) + ) + } + } + + fun enableForegroundDispatch(activity: Activity) { + val adapter = adapter ?: return + if (!adapter.isEnabled) { + refreshStatus() + return + } + val intent = Intent(activity, activity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + val pendingIntent = PendingIntent.getActivity( + activity, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + val filters = arrayOf(IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)) + val techLists = arrayOf( + arrayOf(IsoDep::class.java.name), + arrayOf(NfcF::class.java.name) + ) + adapter.enableForegroundDispatch(activity, pendingIntent, filters, techLists) + } + + fun disableForegroundDispatch(activity: Activity) { + adapter?.disableForegroundDispatch(activity) + } + + private fun scheduleResetToRescanHint() { + resetMessageJob?.cancel() + resetMessageJob = scope.launch { + delay(5_000) + _uiState.value = _uiState.value.copy( + scanMessage = if (adapter?.isEnabled == true) { + context.getString(R.string.tap_again_hint) + } else { + currentScanMessage() + } + ) + } + } + + private fun currentScanMessage(): String = when { + adapter == null -> context.getString(R.string.nfc_not_supported) + !adapter.isEnabled -> context.getString(R.string.nfc_disabled) + else -> context.getString(R.string.tap_card_hint) + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/pdf/HistoryPdfExporter.kt b/app/src/main/java/com/iiyh/emoneyinfo/pdf/HistoryPdfExporter.kt new file mode 100644 index 0000000..9b92cb1 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/pdf/HistoryPdfExporter.kt @@ -0,0 +1,243 @@ +package com.iiyh.emoneyinfo.pdf + +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Typeface +import android.graphics.pdf.PdfDocument +import android.net.Uri +import androidx.core.content.FileProvider +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.data.EmoneyUiState +import com.iiyh.emoneyinfo.data.TransactionItem +import com.iiyh.emoneyinfo.data.formatCardNumber +import com.iiyh.emoneyinfo.data.maskFirst12 +import java.io.File +import java.io.FileOutputStream +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +object HistoryPdfExporter { + private const val PAGE_WIDTH = 595 + private const val PAGE_HEIGHT = 842 + private const val MARGIN = 40f + + fun export(context: Context, state: EmoneyUiState): File { + val document = PdfDocument() + var page = document.startPage(PdfDocument.PageInfo.Builder(PAGE_WIDTH, PAGE_HEIGHT, 1).create()) + var canvas = page.canvas + var y = MARGIN + var pageNumber = 1 + + val bodyPaint = paint(10f) + val boldPaint = paint(10f, Typeface.BOLD) + val titlePaint = paint(13f, Typeface.BOLD) + val headerPaint = paint(10f, Typeface.BOLD, color = pdfGreen()) + val smallPaint = paint(9f) + val footerPaint = paint(9f, color = Color.LTGRAY, align = Paint.Align.CENTER) + val amountPositivePaint = paint(9f, color = Color.rgb(33, 140, 33), align = Paint.Align.RIGHT) + val amountDefaultPaint = paint(9f, color = Color.BLACK, align = Paint.Align.RIGHT) + val rightHeaderPaint = paint(10f, Typeface.BOLD, color = pdfGreen(), align = Paint.Align.RIGHT) + val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.LTGRAY + strokeWidth = 1f + } + val altRowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.rgb(247, 247, 247) + } + + val subtitle = context.getString(R.string.pdf_subtitle) + val cardLabel = context.getString(state.cardType.labelRes) + val balanceText = state.formattedBalance() + val cardNumber = state.cardNumber.maskFirst12() + val list = state.transactions + val hasLocation = list.any { it.locationName.isNotBlank() } + val indonesianLocale = Locale.forLanguageTag("id-ID") + val dateFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm", indonesianLocale) + val numFormatter = NumberFormat.getCurrencyInstance(indonesianLocale).apply { + currency = java.util.Currency.getInstance("IDR") + maximumFractionDigits = 0 + } + + fun newPage() { + document.finishPage(page) + pageNumber += 1 + page = document.startPage(PdfDocument.PageInfo.Builder(PAGE_WIDTH, PAGE_HEIGHT, pageNumber).create()) + canvas = page.canvas + y = MARGIN + } + + val logo = BitmapFactory.decodeResource(context.resources, R.drawable.app_logo) + if (logo != null) { + val imgH = 30f + val imgW = logo.width * (imgH / logo.height.toFloat()) + canvas.drawBitmap(logo, null, android.graphics.RectF(MARGIN, y, MARGIN + imgW, y + imgH), null) + y += imgH + 12f + } else { + val fallbackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = pdfGreen() } + canvas.drawRoundRect(MARGIN, y, MARGIN + 48f, y + 48f, 10f, 10f, fallbackPaint) + val letterPaint = paint(20f, Typeface.BOLD, color = Color.WHITE, align = Paint.Align.CENTER) + canvas.drawText("E", MARGIN + 24f, y + 31f, letterPaint) + y += 60f + } + + y += drawWrappedText(canvas, subtitle, MARGIN, y, PAGE_WIDTH - MARGIN * 2, bodyPaint) + 16f + canvas.drawLine(MARGIN, y, PAGE_WIDTH - MARGIN, y, linePaint) + y += 16f + + y = drawInfoRow(canvas, context.getString(R.string.pdf_card_label), cardLabel, y, boldPaint, bodyPaint) + y = drawInfoRow(canvas, context.getString(R.string.pdf_balance_label), balanceText, y, boldPaint, bodyPaint) + y = drawInfoRow(canvas, context.getString(R.string.card_number_label), cardNumber, y, boldPaint, bodyPaint) + y += 8f + canvas.drawLine(MARGIN, y, PAGE_WIDTH - MARGIN, y, linePaint) + y += 16f + + val dateColW = 130f + val typeColW = 70f + val locationColW = if (hasLocation) 120f else 0f + val amountX = MARGIN + dateColW + typeColW + locationColW + val amountColW = PAGE_WIDTH - MARGIN - amountX + + canvas.drawText(context.getString(R.string.pdf_date_label), MARGIN, y, headerPaint) + canvas.drawText(context.getString(R.string.pdf_transaction_label), MARGIN + dateColW, y, headerPaint) + if (hasLocation) { + canvas.drawText(context.getString(R.string.pdf_location_label), MARGIN + dateColW + typeColW, y, headerPaint) + } + canvas.drawText(context.getString(R.string.pdf_amount_label), amountX + amountColW, y, rightHeaderPaint) + y += 14f + + val headerUnderline = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.argb(102, 92, 125, 122) + strokeWidth = 1f + } + canvas.drawLine(MARGIN, y, PAGE_WIDTH - MARGIN, y, headerUnderline) + y += 12f + + list.forEachIndexed { index, item -> + val rowHeight = 16f + if (y > PAGE_HEIGHT - MARGIN - 40f) { + newPage() + } + + if (index % 2 == 0) { + canvas.drawRect(MARGIN - 4f, y - 11f, PAGE_WIDTH - MARGIN + 4f, y + 5f, altRowPaint) + } + + val dateText = dateFormatter.format(item.date) + val typeText = item.title + val amountText = numFormatter.format(item.amount).replace("IDR", "Rp") + val amountPaint = if (item.isCredit) amountPositivePaint else amountDefaultPaint + + canvas.drawText(dateText, MARGIN, y, smallPaint) + canvas.drawText(typeText, MARGIN + dateColW, y, smallPaint) + if (hasLocation) { + canvas.drawText(item.locationName.ifBlank { "–" }, MARGIN + dateColW + typeColW, y, smallPaint) + } + canvas.drawText(amountText, amountX + amountColW, y, amountPaint) + y += rowHeight + } + + y += 10f + canvas.drawLine(MARGIN, y, PAGE_WIDTH - MARGIN, y, linePaint) + y += 14f + canvas.drawText( + "emoneyInfo © ${Calendar.getInstance().get(Calendar.YEAR)}", + PAGE_WIDTH / 2f, + y, + footerPaint + ) + + document.finishPage(page) + + val file = File(context.cacheDir, "emoney_history_${System.currentTimeMillis()}.pdf") + FileOutputStream(file).use { output -> + document.writeTo(output) + } + document.close() + return file + } + + fun openOrShare(context: Context, file: File) { + val uri = FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + file + ) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "application/pdf" + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + val viewIntent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "application/pdf") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + val packageManager = context.packageManager + val openIntents = packageManager.queryIntentActivities(viewIntent, 0).map { resolveInfo -> + Intent(viewIntent).setPackage(resolveInfo.activityInfo.packageName) + }.toTypedArray() + + val chooser = Intent.createChooser(shareIntent, context.getString(R.string.pdf_open_or_share)) + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, openIntents) + chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(chooser) + } + + private fun drawInfoRow( + canvas: Canvas, + label: String, + value: String, + y: Float, + labelPaint: Paint, + valuePaint: Paint + ): Float { + val labelW = 90f + val colonX = MARGIN + labelW + val valueX = colonX + 14f + canvas.drawText(label, MARGIN, y, labelPaint) + canvas.drawText(":", colonX, y, valuePaint) + canvas.drawText(value, valueX, y, valuePaint) + return y + 16f + } + + private fun drawWrappedText( + canvas: Canvas, + text: String, + x: Float, + y: Float, + width: Float, + paint: Paint + ): Float { + val textPaint = android.text.TextPaint(paint) + val layout = android.text.StaticLayout.Builder + .obtain(text, 0, text.length, textPaint, width.toInt()) + .build() + canvas.save() + canvas.translate(x, y) + layout.draw(canvas) + canvas.restore() + return layout.height.toFloat() + } + + private fun paint( + size: Float, + typefaceStyle: Int = Typeface.NORMAL, + color: Int = Color.BLACK, + align: Paint.Align = Paint.Align.LEFT + ) = Paint(Paint.ANTI_ALIAS_FLAG).apply { + textSize = size + typeface = Typeface.create(Typeface.DEFAULT, typefaceStyle) + this.color = color + textAlign = align + } + + private fun pdfGreen(): Int = Color.rgb(92, 125, 122) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/EmoneyInfoApp.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/EmoneyInfoApp.kt new file mode 100644 index 0000000..4feb8fb --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/EmoneyInfoApp.kt @@ -0,0 +1,139 @@ +package com.iiyh.emoneyinfo.ui + +import android.content.Context +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CreditCard +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.iiyh.emoneyinfo.R +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.iiyh.emoneyinfo.nfc.UnifiedNfcReader +import com.iiyh.emoneyinfo.ui.screens.AboutScreen +import com.iiyh.emoneyinfo.ui.screens.FaqScreen +import com.iiyh.emoneyinfo.ui.screens.HomeScreen +import com.iiyh.emoneyinfo.ui.screens.HistoryScreen +import com.iiyh.emoneyinfo.ui.screens.PrivacyPolicyScreen +import com.iiyh.emoneyinfo.ui.screens.SettingsScreen +import com.iiyh.emoneyinfo.ui.screens.TermsScreen + +private data class BottomDestination(val route: String, val label: String) + +@Composable +fun EmoneyInfoApp( + nfcReader: UnifiedNfcReader, + adsEnabled: Boolean +) { + val context = LocalContext.current + val navController = rememberNavController() + val uiState by nfcReader.uiState.collectAsState() + val preferences = remember(context) { + context.getSharedPreferences("emoney_info_prefs", Context.MODE_PRIVATE) + } + var showCardNumber by remember(preferences) { + mutableStateOf(preferences.getBoolean("masked", true)) + } + + val bottomDestinations = listOf( + BottomDestination("home", stringResource(R.string.tab_home)), + BottomDestination("settings", stringResource(R.string.tab_settings)) + ) + + Scaffold( + bottomBar = { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val visible = currentDestination?.route in setOf("home", "settings") + if (visible) { + NavigationBar { + bottomDestinations.forEach { destination -> + NavigationBarItem( + selected = currentDestination?.hierarchy?.any { it.route == destination.route } == true, + onClick = { + if (destination.route == "home") { + navController.popBackStack("home", false) + } else { + navController.navigate(destination.route) { + launchSingleTop = true + } + } + }, + icon = { + Icon( + imageVector = if (destination.route == "home") Icons.Default.CreditCard else Icons.Default.Settings, + contentDescription = destination.label + ) + }, + label = { Text(destination.label) } + ) + } + } + } + } + ) { innerPadding -> + NavHost(navController = navController, startDestination = "home", modifier = Modifier.padding(innerPadding)) { + composable("home") { + HomeScreen( + state = uiState, + adsEnabled = adsEnabled, + showCardNumber = showCardNumber, + onScanTapped = { nfcReader.startScan() }, + onViewHistoryTapped = { navController.navigate("history") }, + onSettingsTapped = { navController.navigate("settings") } + ) + } + composable("settings") { + SettingsScreen( + adsEnabled = adsEnabled, + showCardNumber = showCardNumber, + onShowCardNumberChanged = { + showCardNumber = it + preferences.edit().putBoolean("masked", it).apply() + }, + onHelpCenterTapped = { navController.navigate("faq") }, + onAboutTapped = { navController.navigate("about") } + ) + } + composable("history") { + HistoryScreen( + state = uiState, + adsEnabled = adsEnabled, + onBack = { navController.popBackStack() } + ) + } + composable("faq") { + FaqScreen(onBack = { navController.popBackStack() }) + } + composable("about") { + AboutScreen( + onBack = { navController.popBackStack() }, + onTermsTapped = { navController.navigate("terms") }, + onPrivacyTapped = { navController.navigate("privacy") } + ) + } + composable("terms") { + TermsScreen(onBack = { navController.popBackStack() }) + } + composable("privacy") { + PrivacyPolicyScreen(onBack = { navController.popBackStack() }) + } + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/components/AdMobBanner.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/components/AdMobBanner.kt new file mode 100644 index 0000000..b080ac2 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/components/AdMobBanner.kt @@ -0,0 +1,64 @@ +package com.iiyh.emoneyinfo.ui.components + +import android.view.ViewGroup +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.google.android.gms.ads.AdListener +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.AdSize +import com.google.android.gms.ads.AdView +import com.google.android.gms.ads.LoadAdError +import com.iiyh.emoneyinfo.util.AppLog + +@Composable +fun AdMobBanner(adUnitId: String, modifier: Modifier = Modifier) { + val context = LocalContext.current + val configuration = LocalConfiguration.current + val density = LocalDensity.current + val adWidthPx = with(density) { configuration.screenWidthDp.dp.roundToPx() } + val adWidthDp = with(density) { adWidthPx.toDp().value.toInt() } + val adSize = remember(adWidthDp) { + AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, adWidthDp) + } + val adView = remember(adUnitId, adSize) { + AdView(context).apply { + setAdSize(adSize) + this.adUnitId = adUnitId + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + adListener = object : AdListener() { + override fun onAdLoaded() { + AppLog.d("EmoneyInfoAds", "Banner loaded: $adUnitId") + } + + override fun onAdFailedToLoad(error: LoadAdError) { + AppLog.w( + "EmoneyInfoAds", + "Banner failed: $adUnitId code=${error.code} message=${error.message}" + ) + } + } + loadAd(AdRequest.Builder().build()) + } + } + + DisposableEffect(adView) { + onDispose { + adView.destroy() + } + } + + AndroidView( + modifier = modifier, + factory = { adView } + ) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/components/ScreenTopBar.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/components/ScreenTopBar.kt new file mode 100644 index 0000000..87598f3 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/components/ScreenTopBar.kt @@ -0,0 +1,30 @@ +package com.iiyh.emoneyinfo.ui.components + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.iiyh.emoneyinfo.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScreenTopBar(title: String, onBack: () -> Unit) { + TopAppBar( + windowInsets = WindowInsets(0, 0, 0, 0), + title = { Text(text = title) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } + ) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/AboutScreen.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/AboutScreen.kt new file mode 100644 index 0000000..23d3195 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/AboutScreen.kt @@ -0,0 +1,156 @@ +package com.iiyh.emoneyinfo.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CreditCard +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.ui.components.ScreenTopBar +import com.iiyh.emoneyinfo.ui.theme.Card +import com.iiyh.emoneyinfo.ui.theme.Primary +import com.iiyh.emoneyinfo.ui.theme.Secondary +import com.iiyh.emoneyinfo.ui.theme.TextSecondary + +@Composable +fun AboutScreen(onBack: () -> Unit, onTermsTapped: () -> Unit, onPrivacyTapped: () -> Unit) { + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + ScreenTopBar( + title = stringResource(R.string.about_app), + onBack = onBack + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Card( + colors = CardDefaults.cardColors(containerColor = Card), + shape = RoundedCornerShape(24.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Image( + painter = painterResource(R.drawable.app_logo), + contentDescription = null, + modifier = Modifier.size(84.dp), + contentScale = ContentScale.Fit + ) + Text(stringResource(R.string.app_name), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.settings_about_subtitle), color = TextSecondary) + Text( + stringResource(R.string.about_description), + color = TextSecondary + ) + } + } + LegalRow( + title = stringResource(R.string.terms_conditions), + icon = Icons.Default.Description, + onClick = onTermsTapped + ) + LegalRow( + title = stringResource(R.string.privacy_policy), + icon = Icons.Default.Lock, + onClick = onPrivacyTapped + ) + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(Brush.linearGradient(listOf(Primary, Secondary))) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon(Icons.Default.CreditCard, contentDescription = null, tint = Color.White) + Text(stringResource(R.string.about_architecture_title), color = Color.White, fontWeight = FontWeight.Bold) + Text( + stringResource(R.string.about_architecture_desc), + color = Color.White + ) + } + } + } +} + +@Composable +private fun LegalRow(title: String, icon: ImageVector, onClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ), + colors = CardDefaults.cardColors(containerColor = Card), + shape = RoundedCornerShape(18.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + colors = CardDefaults.cardColors(containerColor = Primary.copy(alpha = 0.16f)), + shape = RoundedCornerShape(14.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = Secondary, + modifier = Modifier.padding(12.dp) + ) + } + Text( + text = title, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(vertical = 12.dp) + ) + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/FaqScreen.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/FaqScreen.kt new file mode 100644 index 0000000..5b64ce2 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/FaqScreen.kt @@ -0,0 +1,194 @@ +package com.iiyh.emoneyinfo.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.QuestionAnswer +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.data.FaqData +import com.iiyh.emoneyinfo.data.FaqItem +import com.iiyh.emoneyinfo.ui.components.ScreenTopBar +import com.iiyh.emoneyinfo.ui.theme.Card +import com.iiyh.emoneyinfo.ui.theme.Primary +import com.iiyh.emoneyinfo.ui.theme.Secondary +import com.iiyh.emoneyinfo.ui.theme.TextSecondary + +@Composable +fun FaqScreen(onBack: () -> Unit) { + var query by remember { mutableStateOf("") } + val uriHandler = LocalUriHandler.current + val filteredCategories = FaqData.all.mapNotNull { category -> + val filteredItems = category.items.filter { + val question = stringResource(it.questionRes) + val answer = stringResource(it.answerRes) + query.isBlank() || question.contains(query, true) || answer.contains(query, true) + } + if (filteredItems.isEmpty()) null else category.copy(items = filteredItems) + } + + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + ScreenTopBar( + title = stringResource(R.string.help_center), + onBack = onBack + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(Brush.linearGradient(listOf(Primary, Secondary))) + .padding(horizontal = 20.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + stringResource(R.string.faq_header), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + OutlinedTextField( + value = query, + onValueChange = { query = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.faq_search)) } + ) + LazyColumn( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (filteredCategories.isEmpty()) { + item { + Text( + text = stringResource(R.string.faq_no_results), + color = TextSecondary, + style = MaterialTheme.typography.bodyMedium + ) + } + } else { + filteredCategories.forEach { category -> + item { + Text( + text = stringResource(category.titleRes), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + color = TextSecondary, + modifier = Modifier.padding(top = 8.dp, bottom = 2.dp) + ) + } + items(category.items) { item -> + FaqCard(item = item) + } + } + } + item { + Spacer(modifier = Modifier.height(8.dp)) + Card( + colors = CardDefaults.cardColors(containerColor = Primary), + shape = RoundedCornerShape(20.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.faq_help_card_title), + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(R.string.faq_help_card_desc), + color = Color.White.copy(alpha = 0.88f), + style = MaterialTheme.typography.bodyMedium + ) + Button( + onClick = { + uriHandler.openUri("mailto:support@iptek.co?subject=Ask%20Support") + } + ) { + Icon(Icons.Default.Email, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.faq_email_support)) + } + } + } + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + } +} + +@Composable +private fun FaqCard(item: FaqItem) { + Card( + modifier = Modifier.padding(vertical = 2.dp), + colors = CardDefaults.cardColors(containerColor = Card), + shape = RoundedCornerShape(18.dp) + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Card( + colors = CardDefaults.cardColors(containerColor = Primary.copy(alpha = 0.16f)), + shape = RoundedCornerShape(14.dp) + ) { + Icon( + imageVector = Icons.Default.QuestionAnswer, + contentDescription = null, + tint = Secondary, + modifier = Modifier.padding(10.dp) + ) + } + } + Text(stringResource(item.questionRes), fontWeight = FontWeight.SemiBold) + Text(stringResource(item.answerRes), color = TextSecondary) + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HistoryScreen.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HistoryScreen.kt new file mode 100644 index 0000000..1ca66cd --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HistoryScreen.kt @@ -0,0 +1,228 @@ +package com.iiyh.emoneyinfo.ui.screens + +import android.app.Activity +import android.content.ContextWrapper +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.Image +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PictureAsPdf +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.FullScreenContentCallback +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.interstitial.InterstitialAd +import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.data.EmoneyUiState +import com.iiyh.emoneyinfo.data.TransactionItem +import com.iiyh.emoneyinfo.ui.theme.Danger +import com.iiyh.emoneyinfo.ads.AdMobConfig +import com.iiyh.emoneyinfo.pdf.HistoryPdfExporter +import com.iiyh.emoneyinfo.ui.components.AdMobBanner +import com.iiyh.emoneyinfo.ui.components.ScreenTopBar +import com.iiyh.emoneyinfo.ui.theme.Card +import com.iiyh.emoneyinfo.ui.theme.Primary +import com.iiyh.emoneyinfo.ui.theme.Success +import com.iiyh.emoneyinfo.ui.theme.TextSecondary + +@Composable +fun HistoryScreen(state: EmoneyUiState, adsEnabled: Boolean, onBack: () -> Unit) { + val exportButtonHeight = 56.dp + val context = LocalContext.current + val activity = context.findActivity() + var interstitialAd by remember { mutableStateOf(null) } + + fun loadInterstitial() { + InterstitialAd.load( + context, + AdMobConfig.INTERSTITIAL_PDF, + AdRequest.Builder().build(), + object : InterstitialAdLoadCallback() { + override fun onAdLoaded(ad: InterstitialAd) { + interstitialAd = ad + } + + override fun onAdFailedToLoad(error: LoadAdError) { + interstitialAd = null + } + } + ) + } + + LaunchedEffect(adsEnabled) { + if (adsEnabled) { + loadInterstitial() + } else { + interstitialAd = null + } + } + + fun exportPdfNow() { + runCatching { + val file = HistoryPdfExporter.export(context, state) + HistoryPdfExporter.openOrShare(context, file) + }.onFailure { + Toast.makeText(context, context.getString(R.string.pdf_export_failed), Toast.LENGTH_SHORT).show() + } + } + + fun exportWithAd() { + val ad = interstitialAd + if (adsEnabled && ad != null && activity != null) { + interstitialAd = null + ad.fullScreenContentCallback = object : FullScreenContentCallback() { + override fun onAdDismissedFullScreenContent() { + loadInterstitial() + exportPdfNow() + } + + override fun onAdFailedToShowFullScreenContent(adError: com.google.android.gms.ads.AdError) { + loadInterstitial() + exportPdfNow() + } + } + ad.show(activity) + } else { + exportPdfNow() + } + } + + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + ScreenTopBar( + title = stringResource(R.string.history_title), + onBack = onBack + ) + } + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (adsEnabled) { + AdMobBanner(adUnitId = AdMobConfig.BANNER_HISTORY, modifier = Modifier.fillMaxWidth()) + } + + if (state.transactions.isEmpty()) { + Text(stringResource(R.string.no_history), color = TextSecondary) + } else { + LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) { + items(state.transactions) { item -> + HistoryTransactionCard(item = item) + } + item { Spacer(modifier = Modifier.padding(bottom = exportButtonHeight + 64.dp)) } + } + } + } + + Button( + onClick = { exportWithAd() }, + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(24.dp) + ) { + Icon(Icons.Default.PictureAsPdf, contentDescription = null) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.export_pdf)) + } + } + } +} + +private fun android.content.Context.findActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null +} + +@Composable +private fun HistoryTransactionCard(item: TransactionItem) { + val primaryLabel = item.locationName.ifBlank { item.title } + val secondaryLabel = item.formattedDate() + val trailingSubtitle = item.title.takeIf { item.locationName.isNotBlank() } + + Card( + colors = CardDefaults.cardColors(containerColor = Card), + shape = RoundedCornerShape(18.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Card( + colors = CardDefaults.cardColors( + containerColor = if (item.isCredit) Primary.copy(alpha = 0.22f) else Danger.copy(alpha = 0.12f) + ), + shape = RoundedCornerShape(14.dp) + ) { + Image( + painter = painterResource(if (item.isCredit) R.drawable.ic_activity_outline else R.drawable.ic_card_outline), + contentDescription = null, + modifier = Modifier + .padding(12.dp) + .size(24.dp), + contentScale = ContentScale.Fit + ) + } + Spacer(modifier = Modifier.padding(horizontal = 8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(primaryLabel, fontWeight = FontWeight.SemiBold) + Text(secondaryLabel, style = MaterialTheme.typography.bodySmall, color = TextSecondary) + } + Column(horizontalAlignment = Alignment.End) { + Text(item.formattedAmount(), color = if (item.isCredit) Success else Danger) + trailingSubtitle?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = TextSecondary + ) + } + } + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HomeScreen.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HomeScreen.kt new file mode 100644 index 0000000..ea6c908 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/HomeScreen.kt @@ -0,0 +1,334 @@ +package com.iiyh.emoneyinfo.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.CreditCard +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import android.widget.Toast +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.data.EmoneyUiState +import com.iiyh.emoneyinfo.data.formatCardNumber +import com.iiyh.emoneyinfo.data.maskFirst12 +import com.iiyh.emoneyinfo.ui.theme.Primary +import com.iiyh.emoneyinfo.ads.AdMobConfig +import com.iiyh.emoneyinfo.ui.components.AdMobBanner +import com.iiyh.emoneyinfo.ui.theme.Danger +import com.iiyh.emoneyinfo.ui.theme.Secondary +import com.iiyh.emoneyinfo.ui.theme.Success +import com.iiyh.emoneyinfo.ui.theme.TextSecondary + +@Composable +fun HomeScreen( + state: EmoneyUiState, + adsEnabled: Boolean, + showCardNumber: Boolean, + onScanTapped: () -> Unit, + onViewHistoryTapped: () -> Unit, + onSettingsTapped: () -> Unit +) { + @Suppress("DEPRECATION") + val clipboard = LocalClipboardManager.current + val context = LocalContext.current + val latestTransaction = state.transactions.firstOrNull() + val hasCardData = state.hasCardData() + val displayCardNumber = when { + state.cardNumber.isBlank() -> "" + showCardNumber -> state.cardNumber.formatCardNumber() + else -> state.cardNumber.maskFirst12() + } + val latestTitle = latestTransaction?.locationName?.ifBlank { latestTransaction.title } + ?: stringResource(R.string.placeholder_transaction_title) + val latestSubtitle = latestTransaction?.formattedDate() ?: stringResource(R.string.placeholder_transaction_date) + val latestTrailingSubtitle = latestTransaction?.title?.takeIf { latestTransaction.locationName.isNotBlank() } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .verticalScroll(rememberScrollState()) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(R.drawable.app_logo), + contentDescription = null, + modifier = Modifier.size(42.dp), + contentScale = ContentScale.Fit + ) + Spacer(Modifier.size(10.dp)) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = onSettingsTapped) { + Icon(Icons.Default.Settings, contentDescription = "Settings") + } + } + + Column { + Text(stringResource(R.string.available_balance), color = TextSecondary, style = MaterialTheme.typography.labelMedium) + Spacer(Modifier.height(4.dp)) + Text(state.formattedBalance(), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(28.dp)) + .background(Brush.linearGradient(listOf(Primary, Secondary))) + ) { + Image( + painter = painterResource(R.drawable.home_header), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(220.dp), + contentScale = ContentScale.Crop, + alpha = 0.18f + ) + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Surface( + color = Color.White.copy(alpha = 0.16f), + shape = RoundedCornerShape(999.dp) + ) { + Text( + text = if (hasCardData) stringResource(R.string.scan_result_title) else stringResource(R.string.scan_ready_title), + color = Color.White, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + style = MaterialTheme.typography.labelMedium + ) + } + Text( + stringResource(state.cardType.labelRes), + color = Color.White.copy(alpha = 0.95f), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + if (state.cardNumber.isNotBlank()) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + stringResource(R.string.card_number_label), + color = Color.White.copy(alpha = 0.72f), + style = MaterialTheme.typography.labelMedium + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = displayCardNumber, + color = Color.White.copy(alpha = 0.92f), + style = MaterialTheme.typography.bodyMedium + ) + IconButton( + onClick = { + clipboard.setText(AnnotatedString(state.cardNumber)) + Toast.makeText( + context, + context.getString(R.string.copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + }, + modifier = Modifier.size(20.dp) + ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = stringResource(R.string.copy_card_number), + tint = Color.White.copy(alpha = 0.78f), + modifier = Modifier.size(14.dp) + ) + } + } + } + } + if (hasCardData) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + stringResource(R.string.latest_scan_message), + color = Color.White.copy(alpha = 0.72f), + style = MaterialTheme.typography.labelMedium + ) + Text(state.scanMessage, color = Color.White, style = MaterialTheme.typography.bodyMedium) + } + } else { + Text(stringResource(R.string.tap_card_hint), color = Color.White, style = MaterialTheme.typography.bodyMedium) + } + } + } + + if (adsEnabled) { + AdMobBanner(adUnitId = AdMobConfig.BANNER_HOME, modifier = Modifier.fillMaxWidth()) + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringResource(R.string.last_activity_label), + color = TextSecondary, + style = MaterialTheme.typography.labelMedium + ) + if (latestTransaction == null) { + Text( + text = stringResource(R.string.history_summary_empty), + color = TextSecondary, + style = MaterialTheme.typography.bodySmall + ) + } else { + Text( + text = stringResource(R.string.history_summary_count, state.transactions.size), + color = TextSecondary, + style = MaterialTheme.typography.bodySmall + ) + } + } + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + InfoChip( + modifier = Modifier.weight(1f), + title = stringResource(R.string.card_type_label), + value = stringResource(state.cardType.labelRes), + iconRes = R.drawable.ic_card_outline + ) + InfoChip( + modifier = Modifier.weight(1f), + title = stringResource(R.string.history_summary_card), + value = if (state.transactions.isEmpty()) "0" else state.transactions.size.toString(), + iconRes = R.drawable.ic_activity_outline + ) + } + + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_activity_outline), + contentDescription = null, + modifier = Modifier.size(28.dp), + contentScale = ContentScale.Fit + ) + Spacer(Modifier.size(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = latestTitle, + fontWeight = FontWeight.SemiBold + ) + Text( + text = latestSubtitle, + color = TextSecondary, + style = MaterialTheme.typography.bodySmall + ) + } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = latestTransaction?.formattedAmount() ?: stringResource(R.string.placeholder_transaction_amount), + color = latestTransaction?.let { if (it.isCredit) Success else Danger } + ?: MaterialTheme.colorScheme.onSurface + ) + latestTrailingSubtitle?.let { + Text( + text = it, + color = TextSecondary, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onViewHistoryTapped + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon(Icons.Default.History, contentDescription = null) + Text(stringResource(R.string.view_full_history), fontWeight = FontWeight.SemiBold) + } + } +} + +@Composable +private fun InfoChip( + modifier: Modifier = Modifier, + title: String, + value: String, + iconRes: Int +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(24.dp), + contentScale = ContentScale.Fit + ) + Column { + Text(title, color = TextSecondary, style = MaterialTheme.typography.labelSmall) + Text(value, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.bodyMedium) + } + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/PrivacyPolicyScreen.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/PrivacyPolicyScreen.kt new file mode 100644 index 0000000..f503835 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/PrivacyPolicyScreen.kt @@ -0,0 +1,45 @@ +package com.iiyh.emoneyinfo.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.ui.components.ScreenTopBar +import com.iiyh.emoneyinfo.ui.theme.TextSecondary + +@Composable +fun PrivacyPolicyScreen(onBack: () -> Unit) { + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + ScreenTopBar( + title = stringResource(R.string.privacy_policy), + onBack = onBack + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text(stringResource(R.string.privacy_intro), color = TextSecondary) + Text(stringResource(R.string.privacy_point_1)) + Text(stringResource(R.string.privacy_point_2)) + Text(stringResource(R.string.privacy_point_3)) + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/SettingsScreen.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/SettingsScreen.kt new file mode 100644 index 0000000..15f83f9 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/SettingsScreen.kt @@ -0,0 +1,181 @@ +package com.iiyh.emoneyinfo.ui.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.ui.theme.Card +import com.iiyh.emoneyinfo.ui.theme.Primary +import com.iiyh.emoneyinfo.ui.theme.TextSecondary +import com.iiyh.emoneyinfo.ads.AdMobConfig +import com.iiyh.emoneyinfo.ui.components.AdMobBanner + +@Composable +fun SettingsScreen( + adsEnabled: Boolean, + showCardNumber: Boolean, + onShowCardNumberChanged: (Boolean) -> Unit, + onHelpCenterTapped: () -> Unit, + onAboutTapped: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(18.dp) + ) { + Text(stringResource(R.string.settings_title), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) + + if (adsEnabled) { + AdMobBanner(adUnitId = AdMobConfig.BANNER_SETTINGS, modifier = Modifier.fillMaxWidth()) + } + + Card( + colors = CardDefaults.cardColors(containerColor = Card), + shape = RoundedCornerShape(20.dp) + ) { + Column { + SettingRow( + title = stringResource(R.string.language), + subtitle = stringResource(R.string.language_value), + iconRes = R.drawable.ic_settings_outline + ) {} + SettingsDivider() + ToggleSettingRow( + title = stringResource(R.string.show_card_number), + subtitle = stringResource(R.string.show_card_number_desc), + iconRes = R.drawable.ic_simcard, + checked = showCardNumber, + onCheckedChange = onShowCardNumberChanged + ) + SettingsDivider() + SettingRow( + title = stringResource(R.string.help_center), + subtitle = stringResource(R.string.settings_help_subtitle), + iconRes = R.drawable.ic_activity_outline, + onClick = onHelpCenterTapped + ) + SettingsDivider() + SettingRow( + title = stringResource(R.string.about_app), + subtitle = stringResource(R.string.settings_about_subtitle), + iconRes = R.drawable.ic_card_outline, + onClick = onAboutTapped + ) + } + } + + Box(modifier = Modifier.padding(bottom = 84.dp)) + } +} + +@Composable +private fun SettingRow(title: String, subtitle: String, iconRes: Int, onClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + colors = CardDefaults.cardColors(containerColor = Primary.copy(alpha = 0.18f)), + shape = RoundedCornerShape(14.dp) + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.padding(12.dp), + contentScale = ContentScale.Fit + ) + } + Column(modifier = Modifier.weight(1f)) { + Text(title, fontWeight = FontWeight.SemiBold) + Text(subtitle, style = MaterialTheme.typography.bodySmall, color = TextSecondary) + } + } +} + +@Composable +private fun SettingsDivider() { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(start = 72.dp, end = 16.dp) + ) { + androidx.compose.foundation.Canvas( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 0.5.dp) + ) { + drawLine( + color = TextSecondary.copy(alpha = 0.18f), + start = androidx.compose.ui.geometry.Offset(0f, 0f), + end = androidx.compose.ui.geometry.Offset(size.width, 0f), + strokeWidth = 1.dp.toPx() + ) + } + } +} + +@Composable +private fun ToggleSettingRow( + title: String, + subtitle: String, + iconRes: Int, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + colors = CardDefaults.cardColors(containerColor = Primary.copy(alpha = 0.18f)), + shape = RoundedCornerShape(14.dp) + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.padding(12.dp), + contentScale = ContentScale.Fit + ) + } + Column(modifier = Modifier.weight(1f)) { + Text(title, fontWeight = FontWeight.SemiBold) + Text(subtitle, style = MaterialTheme.typography.bodySmall, color = TextSecondary) + } + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/TermsScreen.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/TermsScreen.kt new file mode 100644 index 0000000..6e5a70c --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/screens/TermsScreen.kt @@ -0,0 +1,45 @@ +package com.iiyh.emoneyinfo.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.iiyh.emoneyinfo.R +import com.iiyh.emoneyinfo.ui.components.ScreenTopBar +import com.iiyh.emoneyinfo.ui.theme.TextSecondary + +@Composable +fun TermsScreen(onBack: () -> Unit) { + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + ScreenTopBar( + title = stringResource(R.string.terms_conditions), + onBack = onBack + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text(stringResource(R.string.terms_intro), color = TextSecondary) + Text(stringResource(R.string.terms_point_1)) + Text(stringResource(R.string.terms_point_2)) + Text(stringResource(R.string.terms_point_3)) + } + } +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Color.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Color.kt new file mode 100644 index 0000000..6c35eea --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Color.kt @@ -0,0 +1,13 @@ +package com.iiyh.emoneyinfo.ui.theme + +import androidx.compose.ui.graphics.Color + +val Background = Color(0xFFF3F3F8) +val SystemBar = Color(0xFFDCE2EA) +val Primary = Color(0xFF7AD4D1) +val Secondary = Color(0xFF5D7D7B) +val Card = Color(0xFFFFFFFF) +val TextPrimary = Color(0xFF1A1A2E) +val TextSecondary = Color(0xFF8E8E93) +val Success = Color(0xFF34C759) +val Danger = Color(0xFFE53935) diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Theme.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Theme.kt new file mode 100644 index 0000000..9431b91 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Theme.kt @@ -0,0 +1,31 @@ +package com.iiyh.emoneyinfo.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColors = lightColorScheme( + primary = Primary, + secondary = Secondary, + background = Background, + surface = Card, + onPrimary = TextPrimary, + onSecondary = Card, + onBackground = TextPrimary, + onSurface = TextPrimary +) + +private val DarkColors = darkColorScheme( + primary = Primary, + secondary = Secondary +) + +@Composable +fun EmoneyInfoTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = LightColors, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Type.kt b/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Type.kt new file mode 100644 index 0000000..5ce2672 --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/ui/theme/Type.kt @@ -0,0 +1,5 @@ +package com.iiyh.emoneyinfo.ui.theme + +import androidx.compose.material3.Typography + +val Typography = Typography() diff --git a/app/src/main/java/com/iiyh/emoneyinfo/util/AppLog.kt b/app/src/main/java/com/iiyh/emoneyinfo/util/AppLog.kt new file mode 100644 index 0000000..955e37d --- /dev/null +++ b/app/src/main/java/com/iiyh/emoneyinfo/util/AppLog.kt @@ -0,0 +1,23 @@ +package com.iiyh.emoneyinfo.util + +import android.util.Log +import com.iiyh.emoneyinfo.BuildConfig + +object AppLog { + fun d(tag: String, message: String) { + if (BuildConfig.DEBUG) Log.d(tag, message) + } + + fun w(tag: String, message: String) { + if (BuildConfig.DEBUG) Log.w(tag, message) + } + + fun e(tag: String, message: String, throwable: Throwable? = null) { + if (!BuildConfig.DEBUG) return + if (throwable != null) { + Log.e(tag, message, throwable) + } else { + Log.e(tag, message) + } + } +} diff --git a/app/src/main/res/drawable-nodpi/app_logo.png b/app/src/main/res/drawable-nodpi/app_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..b82d77882e025d1c8edf06ce1986122dc32f08da GIT binary patch literal 16273 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 TAVLgSRpiVmQ^UfOPJjImpd_z7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-nodpi/ic_activity_outline.png b/app/src/main/res/drawable-nodpi/ic_activity_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..9ea069b9bc3da5d5211f7e019fe99408ef57c778 GIT binary patch literal 7666 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 Kxk?$+;Qs@u@8}=^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-nodpi/ic_card_outline.png b/app/src/main/res/drawable-nodpi/ic_card_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..f8d047b289408afa7f98fefdf2e8891037644c12 GIT binary patch literal 7976 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-nodpi/ic_simcard.png b/app/src/main/res/drawable-nodpi/ic_simcard.png new file mode 100644 index 0000000000000000000000000000000000000000..8fd99b51f25cc83c4ffdaa58793aa23ee97d67c7 GIT binary patch literal 11505 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

IAXu74(382cvcg9h-qm<2A8<~wB6?wadt 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 + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..3ae5a4b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/launch_background.xml b/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..d97c7ff --- /dev/null +++ b/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..65291b9 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..65291b9 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..a6d19e21015fc0ff365e645543f9c5a6d8b63c3d GIT binary patch literal 844 zcmV-S1GD^6Nk&FQ0{{S5MM6+kP&iCD0{{RoN5ByfZv`>7ZB@;}-JuUi`ftqMjx^yQ zk|af{duDzgB<20vz@6=K5J{3EJ^9Yy03aYBAfN$d2nYzsK^006P{{gl29NL$Df@@t3=~KNeyKnjq9aKNVG>C~ z5RwpP^bm$5k_@SmAVm0jPpXVI5M)#&Ni`Ih(ZUFs5<*D8Oa|(ylc^0!Z6E<*k`lrY zk|2Zx1gVFR%zxrv`gr|I+2wRJPmAdaTtmtNVmg|e471&AyvU%rUTmkE%&HL6 z(Hta5i|Hy&NAm$>&>9&xnbmw_HeQ$vGb3a5Ra3TGAMy|o@CJ(v#PjxELVYn^{lh!_ zENAP&YQ9NO*(`VS`DFR`{`?)D57+hM`g&MQSDXFjxLwURrO7ah$uI+HGR$)8#rAGw z94KT90Aol}uWj45ZQHhu-aXsCIotMMHY?|+BpuQJ36TE22l#Ovx)`}gkL z6`GNJB=XmR;MIg^ z{-bC}S4@PrI-}SEPqpu7u)T_i9b(AaTRWBs-Px99r3tZIS(IN|Jz!jOh(@zkl0zdy z5DSLE;n3pRbZb#)T&E0edva_^-e`)A?>DAB84(l27)(#Z@-ewRCU@SD^;sfn5o4;X zFe4&$V7J<;H(*D(R}6W_nyZUWUz$$NTh&G7C5pLX;CRQvZf!`NTv0l!YdS4Nvw3YH zSyO8%RXdyV9U%@Y1Ug0par>NyXE zqmJu269^>O_Fd19kl-)~9CYQq65f-Y WACn%U=dYeV5uRUJlA(40rN5~(ppw!6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..551cdd0416aef2c1510d1eac92baf47c0ab6e8d5 GIT binary patch literal 438 zcmV;n0ZIN+Nk&Gl0RRA3MM6+kP&iDX0RR9mp+G1QC!?Tk8_E1ZFS`c>g0^i_{*?d4 z0)nFdqnZCVI2bwD0zd{(gG|_pZdwCaf88=sE3~W|01LJLAQLPAYXI1w2B90Upw+~y z)?i)$Y#o`Xu>dvbQfL8e0KoF#dh5$scqY~?fw5h5$o1%ME@tC z>fitW{{PmjGW*?YHkrMdw2}Q!KucZOn@O(UAC|f^n@by+%ij6&OYLfiomIWmzfFT3qP;Lv2G{U{K+HjlTh zss6snjb}-Es0SL~ie)B|x%~M__4MCLda4EHiY42!r+6ChtxCEIfbY>bCM_;k3f4JE z%fOg%++OMJY&Htm)+9AR@U>tMXE~O?^e;nz??&1L)(bX7j+H_h2YMuhq0>0_`iHk9 zsR>*U(x9bg-Z)l)mS^d}0*li5g$iGJZY@BEq&jcwu8OnrCvcf?ce|Z{{KD-0AiWj4gdfE literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2d560e3862ac1b801a989d852c24b191d1d7a68 GIT binary patch literal 2016 zcmV<62Os!SNk&H42LJ$9MM6+kP&iD>2LJ#sN5Byf^@f7BZJ2~V>~;r2L`(o{HS8-U zA1G`o|Nm!5b1J$%r`Wb_+qP}nJleULH`}&tJL}y?-BX7_&-DBQ-#F`;sMe=n>~x&; zR=Do4qe{{ncUW<+_Z1;al5Nwr4z{js+qP}nwrzYqcEPr7s|(k*jkV6009o4p8M4#+ zU%@%y60!nk2V>hdt2$~{?I5;o{H_kKVjNd&+sWNrhtK0ck|fir`(Nbw+qP}nwm~3i zWB<QzklFeFL4b3j&CPN09=Px z@E3eJ25BN?DPQBy-+2)bj$Ql+;C}oH-;k0_Y7lJ_gNCN8Ns}g_S5@mKMDo5GZv&BU z3kTq#{3C~?syZagp{`0K-}nM>G8aYQX8fU&q?=Hhn`k2Wd59>=5N842!VhH=-GoeR zqMHyudJf2EBoD3CwHrBEfH^d{K1_#BC?~*24Ygv4BsVf{y z5xDYuQk$^3mX)W^P(x&mU_G`Q%C9-+#lNwE_ZLe?n#h}gTi1%k-_N-%408kvoA`I(O07X`na7r-zM4Umj?mGY|EX1h|aN_)+=$X{^a+#-b2AVZp z`i4n-00PIT8;cZXQ>boO-cSz$@NsRJG-Z@c)Ugr{Nx-2Dz!{Y3k~+Ig^2%w$fdlw% zg_~qIhvD+QR2ETgKTy!s&b#mW5Q0eyf7(vtl#;ycFXMy|7*i-7B`>=?b9hV%&t(~u(G@%q6qt%8r#_rNnCN9HCuO7%Is`p6SuduaX_XlHH&mE(}bcI%O#$@ zP~_a!v8i{|lyp7NYRXiZgN>ZycDJ$Q&`os}03cr~%RI5E!_fLJaXXrq?ogPTRh)D! z)pdr4Yn!xhp?}CX+uXK0%0{Se+FjEg5f@`Uy^>`s!mhtlM~~Wd<8@!UcAY1q^+P<) zW96p>O(6ujRVo}MgIW+Te4cye(2o#a=TuigeIJ+Q3`-QQdCHpIHwp$-S56eDLkPhX zD*PgYSgG&Qd8Ga1WwxEeN7vT&kUxd~9X0C8zBPvxbPN~#XUak_5tlN#>u*~t&q99e z2O1|n3BT_;&y0H*0+6I;6iKxd|M z-Agiv1*MXsViIIse5`VW=Km?W7M(wKD)XO#awr@+^o=_sX+86IN-2;d)}U z5_ai*_9Qk=Fm?vJDl24C9s8W-6Q=6$)KzQaxqdF%qcMav!8Dz2;h<;JUi@7-P@4jZqimW1r~50`^f`A zdK1Qd@~~EGGO1uqf$1(l-vAkrgRL@R3Fc$@)lzauIT%GrLTSO_8AV`jtBmk$DJ~MK z4nb=8o~iZXa_Knb?3R;fPW0r_WPTP_=a*+Xb0l21qqM!Wx@32TluqBm`Yg)KaJ}*y zsnT}R>MFWw4KtcWIY>h=r7APsu<3fL9^1;w)|_g-UfTzgs0B!;JE~gEH7l>&rkSm5 zY@Lyp?5tE%b0l>a>2=4^U}m!>pF7b48Re z`9AcR^;j_nzPbUWP$u!x~}H(RQr^}SEDN9KcJi?OW*yeRE(HJd!^L#JnuPAS0$zX ztbkS@&sWr;=0C&82e4)R5tRGTtJ!*xuuX>!dw=2F&8P(5gi0#?$|!@2<_>>X1FdVe z*Y%mmw)IC*8dQfFF`EW`1uav#&BX&ApZxdSkxLh@UcY|(+m4qloHz3KfsePn*rpwG z{*f8i9Yu*rnaI{|zkKeWPk&>!jk|H{#;e`P(PPJb{ad?kJR3BAhgp33Pwq<>_IC&2 yI*g<~xOR8CuYP(jGx_`8PhYJ)xOSrsFT2Rvz#WqrhMkBXQ+VXHXP$BcWEcQdyyH9o literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..a483f08d0a01d8eab46c3147fbe175b9e9de5b00 GIT binary patch literal 648 zcmV;30(bpVNk&G10ssJ4MM6+kP&iC<0ssInFTe{BFXT3oWLIfa{#g+PuC~DXA9a(4 z<4BSmrS1R=@fj=l>X+8rU2r4Wb`>=$|E!1tS6g7I-FF9aY}=}$^&SONCk2mzcqkI^ zh%^9#VeD@0{Q^LM2w4aaAp_;?4-&lL1HK_ag1-#xvMdxJlIS0P;YL+urU9qTw22U= zQ}9JWl12zZ0#efmQ(;KJl>8q5lt$_()M$eKX? zJK8Z1l3Y8sZQHhO6;sxiAML$MuWPLn(f;jaGSishP}M|N!Z0FM%G41eBCkXT?H*HksJj#ZLNaZwA!4;mw7-tXZ+(1pWzW%E z(Tf}8*7K4G)=fB>4S5i`?YetQZH4_3QA1Vxa$m``y`34?t>QP>TVJ=>++t^cY=DT$ z`OOcmHg-+5?%UX@^b(PW-$-I;dg5UJ&`yQNl-tJS`k}+!rZNBs@#DF(jfgc|YBojk z5&fSLmE^|iEGEgVnS$35-Cy7n41VqFJ-alZn({NO=_r^mew?Z@v*gz&-#>pLp)P$p ieUof~kec!Q@FM9X_1-7o zd*KG61{FN=0JywXd<~+`CuiiF2MrPZ#mDzj%+w%&xmAB%tJyrF869753ZPQl%i9Pmg z+qV674$roIs`j&6pXET3B-85qU*y@=w{6?DsSrQ_B!A~IkhYC~AvbU?Z+j*Ho+Ty; z$&TR|t2ji!5|<(k7?9rfsbRrbRLZVXl`>nW zSAp1549Gg07LZyr`}j}FHZAIa*h376VI#nzBK9>>%!8vq>?#AIU^XA@7rc`4z%pTo zU4(!&vcXRuFs*Rc*a$)=4N+@8pFU`6GegkU0PP^z81%Af0oVtwm;w4jKd2%ZFm%cm z)dyGHeoi>D0*%2^lSa1TEZqUe(d9w-unHPL1}PX)vPCmr5{nS<*ZhxZSDld{12np` zjbQjv;L8K>Rv|P(pnr{3AZ${`+I&ac?|1)c%!)tVBpjh6@R^qw-4?P%%0uxDt?|8qW^n@z{F+GgmBldv+woNY6n~gv*L2=y*Je ztQ7Fa$3y~mi~7`oWvmsL&$Vmiz%wR;w=UR}c%o`tJ}gp&;LEZN=34IIUav^U-BwFt z;R&0eXBX;JJezrUdl+jI>M4LwlmCK##)|2DIphf3Yv)E5h_OC>WtQ%ICxqY7mB(fR zvCRcIY2JV5q9u?eP*W3VF;)|LPFGwpAAus*8iC7Nfg6gn-*2)3R>^zSE{OMl&>P4wjH{W`J;@*++OGjQ)?Vnt`!`U<<^k} zqll*4?W!{=;P*~R07(-0@r)auD6x}@u9%XKhqvc?vY zs1J@d-qt{dnPzP&wrG}zBXI=$;D5|}>M54U0BGXf)ieY)5~vRz<(H4P|E~cc0~vjE z$rc5t_|t$?BSD!SM~m7;7&n`B*~HF)xBC_IQ|Il)d9PW83M%t|228q@%o> zUD(gIox9fo0Q&(C418c<;6WTBVBiM>f9wywK=2E1@D2ij;2&N<@BF(UF0 zMgXBBKu2aoWCp=VM`jR2j5HWJB8dFsSB=OF9k0)UkqHJ7@LMAYB7i{<0SFF^Px$Zf zdO3%$*(b6)pXyrgFfjYfZI8!$*P5w}Rpz;CWx*=*U77PSc12}^V3lFDUSpL3a+SHS z%o$9r*6Uu^qB4L$&3H11?b&B_SuXDS{-3@)|3=5y6=0P)>RO?$rOK!>8e_YzrOE`i z$HUH#-%n-wyOvv)%iI3)I#{jOR;cY-Tf6f)+_h|fd99AI$G6Ah?{IxDZ1#Ki;rb4A zEgNJ1kFgcoK3w0;>+J@dRAzpRt?lvnf!SF5?4#}RaGU*pHO4MEBnSXlM$*ih=UblF zwr$(H=fzoT+q>tRvu)e9ZQn~4%}jDunsnNT{!f7D7i$B31j?nsaRiAR2TV^ybO#R{ z1UW0mgy;d{h|cj5!-foHp(ZRR@H!%WAbO$!!wrp1P{@zmOY{J6WY;L((#{Nl*plD# zndpN!CfJ&qAd$7$w+4U#d4JT{9wUW?l>0%ksJ7>HMAiBB?189i%=~B5+(|vhAgZ_A zkBf?&?5>frw{TutOXvK>GBC^8r1!EjFVV;8h+A-a;UZMc55nDt2aw zu*;YQNTQ1Q}D3O_KeDIlSmh~w#vCnDAHtcQR$qX6VcRb?)xb;3sOOLp#KxdRv;gcvh68| zS2ifw^q84}aP@?-Jt_#l@`~`933;Kp`hbxm8V(xmey90D#7zCx`u#@c4u_!U?2Oo@ zH#8nG6W`H#xMGKiAp(teE`OkPlQJVpm$a|HUAA`P1{P|}t`pu|)4r%g0E$&EYxh5O z``%s1<@hu&5Kat&D5zdI*{5$m=qYDU~G8F-*ib~4RV}Xi( GEd>CVwIu%l literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..f85b4f0a973514ff4e07d97b43845f9aec40bc82 GIT binary patch literal 550 zcmV+>0@?jiNk&E<0ssJ4MM6+kP&iBy0ssIn*T6Lpm!qI<8_E1ZFS`c>g0^i_{*?d4 z0)nFdqnUrTZQC%TTcSIR9Bcs~gLg>_9|Q}mr~!k9Rxrq*V4`7zZldkteTo$}XaLl- z`O^aQ^GIqzgEoo0J=a!3s3{wBVqszu)+6$0b~H#wrv}d)GNxiZQHhO+pgIE z{}xf1QM%ENi2hH&kHG(d{{#OAf!DYBkClrS4}HQR9{#7vb-#x`!PCFm_0)vyEAk21 z1YRHR9&Ep1uMO2^$3|LF3Av49`?imGj3MNl(JSAe($FAw;MOk;h6Bk9K4FD~;-;>? z_Ufz%H)whE1@~JoazCG|igcmq!ViRb)E?;|=geIOOjy4fn{}4HpeNqW3;UnBSi2-vF!W#-Ub#a`PJx0WxsC>lRRAe6OdH?8A|C2PvnL)Bv#ap=>X|zqcO;BLT{6NqZL1^N|)3 zy)*}DIT~O_k}7~y`vcA5rAcVP0y17C%>(S1o;}e$u=M0m2f%`)+bELT4opvlK>DiV zG7hMcq{+=!ePmPV1&Cahq;4SoSs&k279AvBNZN?2o4u58ICEi0lKrU9X)4MBth_0G oVq{= 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 ES!rKlr2qf` literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..25807d5d14608e2118c031f019e35b385420aaf5 GIT binary patch literal 1384 zcmV-u1(*6#Nk&Fs1pok7MM6+kP&iCe1pojqkH8}k)rNw$ZJ3ll@V5boh*jV>ee^8b z#1-UB`=?0CKf&GI`36Ln@d@1B$%)(v-F;&uKO%Q`caPoK=}xz|=1*k*J-?ecKY%6|NlDy(rsHiUg!RA z?_+G+$){g&^1k0WwcWSawXtj4a2Nn!+2sF6YTKA?+cs;300JQSJ5T>7fId>o$(+Ke zx*s;&$l$avp%~6H+$4{PIE(AKnVWRJv?7~`^4!9rJU+)y`7`hE5g+Qj=f}^u%sjH( zz#?Acw;ZHofT5JeyAoN#EOJgt$&iLLTq6)H&B~G*JemQLB`A+7q-3LXG_{d@3AY-Q zCxnyf1fAV+C{0K7XZpj($!s!2FKbta_A!2hFqq({FDq!Jce_BbQl+7sKqg$5Aq(O@J z(%xShY=ko|yLhRQ{B*?ja;$OxJUVPHU$y_@oz3!F`qExvHdo4Yex*}LC^g55x9qlo zYm(V)5nh|sBsDN^hXj>&Y%Lz$_kx4?AR&lx1BQ2+^K*qwG8G0`ZiNNCkjD6^U>Hc6 zEdu%(jP8lyVvN00rO007{|;Bs6|dg_dctHY8?M>VP9iq&r8 zHvs^^_(R%Bc8jXt)o!jw001UmQcfY@_Ho5&m9qQ}VEiH~B{`KZ-I(0j(%t>2(kW?W zl96Oouzwmr@9?wDN$8cE&$sks`~lN`!r)52&@&B^JU_e*oGsShoJ9jV!sdb*K#*|x z49SF%;{muH7OC6{mCpY60(=0bM-N_;s?-`lrJa(RNovz-aQv7__vS2s;dP6We0&9% z{J6IPCZWZ&WWJP0X6vZwbSD7RN9%}WzC3_Ts`aVn?Eg#p5#phWt2sXc0K9;N5oyyp zzk$Z|+v{sB$2zypqRnQ?W{fEvCz;89HUOYG`!N9wQ_ zfXU|wFPSvVqWp}gZkg~8K5I*aAqmaTih@#-2oX-9W0{OY`OQt!_4T7&YT$W;zPz6-G3^cWDt$g(Y5U3GCFp`!LlUinb zl^9Z98w4N+9hhr5>${uybpL-XN6z2kEZeI{wDJN2I9VQ0lz_S@Ro9D(O8g!W<`#Gr qiC4-C^b}y{0ynWz*Ao+S|D=SxN|9SCWtv_DO;agyV}&4ns1yLtgu7w@ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..e45c88d39159b5fe49db4dc80caab74530c2021f GIT binary patch literal 752 zcmV#Q3ig~(oZxYx2EO2EFF~KL^NbL*b3Geq|J+J`06@ZRO@<_GRAX%0wr$(C&E2u9 zcPf7PL>A80PQ@}K`ac0*r2nP=rT?Y>rT?Y>rT?Y>rH1iSD<^h0XC30nn=Ph?(j{nht32F%6+>D?g^pHV((+$~S9sngIt!DUK5AH`(kgM9v?X;PnJ~a~bx(nvs zMYtbHLATAA`{~0HT>y~2s0na8kP0~Fk8UFX)mH+Xq@aR7HjF{+wgBrXWs5zY0*pc6 zg5V*6fMWK*9Pot3#e}%^fMDCVC=joz1yD(05dmXA+!I*qFi`7tQ!r00Q^l-;IHFLgD&dPKyidG> zofM1{*iOJAduaD3-&7sni>M5Z*dg4#+@a;U0U|0_H42Xd31yp5qadJo=p4BNOW>nY zc12+;0ZjxB%-o?3I-qU>AmyC8@DeE+VUrGMLjV~s1eSkrXFajXW1s*!s7dfE=s!kV zbwHRS3c4Y#r>@a%oluJi1_?WMLURHoeS&c=-2mXp?>W$raWE;cbo49Qx+B_qM!R=J iKf&LJeCyV7iPiEc(*M%`(*M%`(*M%`(*M%`a;^XrTxQ(> literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..d1e3d05601c5dfc645497ddea52386cc2ee00201 GIT binary patch literal 4384 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..6765dd3d926f9a64341114d7fe0c605af27ce3e0 GIT binary patch literal 1886 zcmV-k2ch^ZMx=w!ejnfz2mk*>)ki>Ji`~n1` z1_P%)*G7AjCPnDP%eeZpzPr_ijX__I_v%1hOB1G!TeMx`yiK_njN}66!Z4v}g)++=8x>TAeH0r;m!(nXW>4Jf!ka7q+iG|KqBh5!NK z0^yiwx@eF&Jy^U75=?0dI*O4D^dPwCeij^qamfSyvN zT(S}oR3&wxDG4h&Yx}+n+Yewx7jji@S+)WiYvZsB8)sle0F_`}sIkFcZ(0%u8FRO- zrX0j1OEpj1+PnZUndaUrw}`MZszRtS>b!q0DrDQHK}G;I1QVr8LGHumhX?Yj=oFP-a>9CmV{1rKjgaK0+vtc=;*IRp`V~~ynTDq zbZvWi{x9z70xluxtiQWe6v(5JILB__BIRO;R z3pxD+ry@bs)en){y6!hR1TviSZLu^qt8QKPsX-0rx=?vGb5ebe&I)M0uaf#zBSG(@ zaX}}YQ^LAt*_i9h48;kzhl*6C)(u?2D_kX1Je>cx(JFmRP24t=kbDu<^p+W?_ zW~)Z)3Bq1`Y_govY(t9u7Z%4OaqJV6^^R5DFKA=uO`@2H#O$o^L1JOB016}m=sUEt z_v4R*#fg5+?Swl|pse?$suhN(s!t~?=X=+;&$F5SpEootv&}{k@jjG@NY`jc$#{Of z{mjCAveY5YxyIGHLTEoxX$Lus*10j{=dFB4go}i=ZXmJq+^5BfnRp=Op(J3|{en39-<0J)0$7#*GVXzyEli}~TTJi__*A3CX z?(ALvd8-e)fp$vr_YIHIH8i1-?(4O+Gxik|U{HVu!g`)Be@L3xf5}gg(0%#&2#8Fa z>0tEiDM>hV@)?|Q2Nu&aas>jw$|mvLH|o|%lDHQG7CigHQOGUz3@v;S@)e4r|K?nQ zy49ZSG7MCBIw7dQ_V$w(py?gt)Vls547~-1?P)~e3IPlFS&hM}P0_vBznYw}mvA0x z)wnX7Y_L%9C6lML0;drpx8o&hwS$)Am!kdbcL81>23!CKB&p589z2IF-iYV1XNiM| z0f%GBp827jmll7N%Hqel0tH}C-=5AMT)?~M)MdC2`|WnSMy@uzd-mAv?gwx-+RTH` zuA|$hdj;nxU66ulYG-hd<5Qz{*nQ~jcmngkl7`p4=_%ZWi_!0)Vtv?qsh$5bC+g!= zpajb#u8F+D;sjM`>b5lO`LzE_bypCV8w~E{#HIQ(|A#(zS+3L)Cphfy&$hfjQH@gp z7sMV{Jm<7N+v51e@sAyrub@~_iBh9myexRmk%*9O-~Q$Dtn#xIM~ZOYj~*8|#`Q?! zo5nAWZ%70IhAdvboUd?se)NT7C0Am6YzK3&{SI8?dL{Qw?VlkaL!iMu^igsj@y~Z) z8?=3Vjz0=w_<;!IV0;X0z;4BN;B0umTk*@mW1lYB=QmLzh6NnpuUfMGQ!ooUW_W`Z ze7b$fstt3kNfL8#u5bFjA5D<$-MhrV?%liB{BhrO-`t5ukwXb9+&kCL&u4J`{N{SY YEuki)EgC{t1SrIWmhDso$lu{Y4tM|5S^xk5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..6e69e5497785656b4d62f5dd979684d23af16faa GIT binary patch literal 978 zcmV;@110{{S5MM6+kP&iDz0{{Roufb~&2gRUm8$o{lprhaJ4ugQ8@PD-C zfBx0BZNrS|%o`$uf&zdF7{K<0BTrBOLD)@qUO}S(pdjcLlwczY6Rp~FP0UO159`Ogaw7V&#;%@-9w!nJK@Bkhyjq+@e!+qP}nw#K&6j3@7po%c(c z)9ifT>W%3C1k4csi~q&{;(zhK_+R`l{ulp?|Hc2}fAPQgU;HngrRk>l@Bg+&-iOjC zv?7$^Kk?sxE{wH?(kO&f{P)kWpZ9||6_*hFKMKYMx>{RXyZS~A$Vlz8^E;OpWv4o} zotjm+c>C!mRYT^Y_u;N$3;wNw9rqeNGw~I{J8SNCnTWx(-~ePEEH@9{>VyE!_8+E! zP9+F%_BI-Zmk9$huZ0ye zK1av(?BJ(=9f#)GP>d}+{?YY8?sf&t?Td_2H~8saj|JZ)=Udds-Kv12k&(#hpSRE- zkao0Hi3V3H*kAP@9S#D>zB!^o?Zq6HkLth2!2oM(Rp@(+!$bYo5EOD=snAV@(n0O# z6$S<8wx=+*p2KbJM>_+9b=l*nE|r@d*M7H?f9(Z=j*FCGKNnHD{GGbcD(Kto!F<&FXtc5t+x zJH;h;z4B89YhCUuV{J1JqZul=z>UrZ;4HsX=9vfDq?Fdk(4n*~cbh0OHOoaMoaEYi zDM~fVd$qzi6S115-%)aY8W38_rEb34L?-0;6ghjI^-yH%mS?a^kG&tJDAz6Rm&-hg zc-`_V`1hfYmqTh0LM#6J9|-Zk_+R`l{ulp?|Hc2}fAPQgU;Hor7ypa@#sAVv0ES!q ATmS$7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..7d2004d1a6fce8d77ab00e584f1c69299b017e5b GIT binary patch literal 6326 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 + Emoney Info + E-Money + Pengaturan + SALDO TERSEDIA + Cek Saldo + Tempelkan kartu di bagian belakang ponsel untuk membaca saldo dan riwayat transaksi. + Tempelkan kembali kartu untuk mengecek saldo dan riwayat transaksi. + Transaksi Terakhir + Lihat Semua Riwayat + Riwayat Transaksi + AKTIVITAS TERBARU + Ekspor PDF + Buka atau bagikan PDF + Gagal mengekspor PDF + Dibuat oleh aplikasi emoney Info: cek saldo dan riwayat uang elektronik. + Kartu + Saldo + Tanggal + Transaksi + Lokasi + Jumlah + Pengaturan + Umum + Aplikasi + Bahasa + Bahasa Indonesia + Tampilkan Nomor Kartu + Tampilkan nomor kartu setelah proses scan berhasil. + Pusat Bantuan + Tentang Aplikasi + Syarat & Ketentuan + Kebijakan Privasi + Apa yang bisa kami bantu hari ini? + Cari pertanyaan atau jawaban + Tidak ada hasil ditemukan + Masih butuh bantuan? + Kirim email ke kami dan kami akan membalas dalam 1–2 hari kerja. + Email Support + Cek kartu e-money yang didukung dengan NFC, lihat saldo, dan tinjau riwayat transaksi di satu tempat. + Mendukung Mandiri e-Money, Flazz, Brizzi, TapCash, JackCard, MegaCash, dan KMT. + © Emoney Info + Siap memindai kartu NFC + Perangkat ini tidak mendukung NFC. + NFC sedang dimatikan. Aktifkan NFC di Pengaturan, lalu tempelkan kembali kartu Anda. + Tidak ada transaksi ditemukan. + Kartu E-Money + Nomor kartu disembunyikan + Belum ada transaksi + Scan kartu untuk memuat riwayat terbaru + Rp 0 + Lokasi + Belum ada riwayat transaksi yang berhasil dibaca untuk kartu ini. + %1$d transaksi + Siap dipindai + Hasil scan + Nomor Kartu + Salin nomor kartu + Disalin ke clipboard + Jenis Kartu + AKTIVITAS TERAKHIR + Pesan pembaca terakhir + Ringkasan kartu + TRANSAKSI TERBARU + Tempelkan kartu yang didukung untuk membaca saldo dan aktivitasnya. + Preferensi aplikasi dan dukungan + PUSAT BANTUAN + Cari + TENTANG + Arsitektur siap NFC + Port Android disiapkan untuk alur ISO-DEP dan FeliCa, dengan parser yang mengikuti arsitektur iOS. + Isi Ulang + Pembayaran + Kartu E-Money + Mandiri e-Money + BCA Flazz + BRIZZI + TapCash + JackCard + MegaCash + KMT + Gagal membaca kartu: %1$s + Kesalahan tidak diketahui + Kartu tidak didukung + Gagal membaca nomor kartu Brizzi + Gagal pada proses Brizzi langkah 1 + Gagal pada proses Brizzi langkah 2 + Gagal pada proses Brizzi langkah 3 + Gagal membaca saldo Brizzi + Gagal membaca nomor kartu Mandiri + Gagal membaca saldo Mandiri + Kartu Brizzi berhasil dibaca. + Kartu Brizzi dan riwayat transaksinya berhasil dibaca. + Kartu Flazz berhasil dibaca. + Kartu Flazz dan riwayat transaksinya berhasil dibaca. + Kartu Flazz Classic terdeteksi. Data dasar kartu berhasil dibaca; parsing saldo dan riwayat detail masih terbatas. + TapCash terdeteksi tetapi data purse tidak dapat dibaca. + Kartu TapCash berhasil dibaca. + Kartu TapCash dan riwayat terbarunya berhasil dibaca. + Kartu Mandiri e-Money berhasil dibaca. Format log kartu lama ini belum di-port. + Kartu Mandiri e-Money dan log transaksinya berhasil dibaca. + JackCard berhasil dibaca. + MegaCash berhasil dibaca. + Kartu KMT dan riwayat perjalanannya berhasil dibaca. + Reaktivasi + Void + Pembaruan Saldo + Transaksi + Kartu Black List + Biaya Laporan + Masa Tenggang + Refund + Tutup + ATU + Kartu + Transaksi + Keuangan + Aplikasi + Kartu apa saja yang didukung? + Aplikasi mendukung Mandiri e-money, BCA Flazz, BNI TapCash, BRI Brizzi, JackCard, MegaCash, dan KMT. Pastikan ponsel Android Anda mendukung NFC. + Mengapa kartu saya tidak terdeteksi? + Pastikan NFC aktif di ponsel Android Anda. Tempelkan kartu secara rata di bagian belakang ponsel dekat antena NFC dan tahan diam selama pemindaian. + Kartu gagal dibaca terus, apa yang harus dilakukan? + Coba lepas casing tebal, bersihkan permukaan kartu, lalu coba lagi. Jika masalah berlanjut, kartu mungkin rusak. + Mengapa transaksi saya tidak muncul? + Aplikasi membaca transaksi terbaru yang tersimpan di chip kartu. Transaksi yang lebih lama mungkin tidak dapat diakses melalui NFC. + Cara ekspor riwayat ke PDF? + Setelah scan kartu, buka Lihat Semua Riwayat, lalu tekan tombol Ekspor PDF. Anda dapat membagikannya lewat WhatsApp, email, dan aplikasi lainnya. + Saldo yang ditampilkan tidak sesuai, kenapa? + Aplikasi membaca saldo langsung dari chip kartu secara real time. Perbedaan dapat terjadi jika top-up terbaru belum tersinkron ke chip. + Apakah bisa isi ulang saldo lewat aplikasi ini? + Tidak, aplikasi ini hanya bisa membaca saldo. Isi ulang harus dilakukan melalui aplikasi resmi bank, ATM, atau merchant. + Bagaimana cara ganti bahasa aplikasi? + Buka Pengaturan → Bahasa. Aplikasi mengikuti pilihan Anda dan langsung mengubah teks yang terlihat. + Apa fungsi Tampilkan Nomor Kartu? + Jika diaktifkan, nomor kartu penuh ditampilkan di beranda. Jika dimatikan, 12 digit pertama disamarkan untuk privasi di tempat umum. + Privasi lebih dulu + Anda bisa menyamarkan nomor kartu dan membuat detail scan lebih nyaman ditinjau. + FAQ dan panduan dukungan + Versi 1.0.0 + Port Android ini mengikuti arah produk yang sama dengan aplikasi iOS. Konten legal final sebaiknya disalin dari sumber produksi sebelum rilis. + 1. Aplikasi membaca kartu NFC yang didukung secara lokal di perangkat. + 2. Aplikasi tidak mengubah saldo kartu atau menulis data ke kartu. + 3. Penempatan iklan dan analitik harus ditinjau sebelum rilis. + Pembacaan NFC diproses secara lokal di perangkat. Teks privasi final harus diselaraskan dengan implementasi Android akhir dan penggunaan AdMob. + 1. Scan kartu dipicu oleh pengguna. + 2. Tidak ada operasi tulis yang dilakukan pada kartu. + 3. Perilaku SDK iklan harus ditinjau untuk kepatuhan produksi. + Kembali + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..e2e2d58 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + #F3F3F8 + #D7DEE8 + #7AD4D1 + #5D7D7B + #1A1A2E + #8E8E93 + #FFFFFF + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..dd03738 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,150 @@ + + Emoney Info + E-Money + Settings + AVAILABLE BALANCE + Check Balance + Tap your card on the back of your phone to read balance and transaction history. + Tap your card again to check balance and transaction history. + Last Transaction + View Full History + Transaction History + RECENT ACTIVITY + Export PDF + Open or share PDF + Failed to export PDF + Generated by emoney Info: check your e-money balance and transaction history. + Card + Balance + Date + Transaction + Location + Amount + Settings + General + App + Language + English + Show Card Number + Display the card number after a successful scan. + Help Center + About App + Terms & Conditions + Privacy Policy + How can we help you today? + Search questions or answers + No results found + Still need help? + Send us an email and we\'ll get back to you within 1–2 business days. + Email Support + Check supported e-money cards with NFC, view balance, and review transaction history in one place. + Supports Mandiri e-Money, Flazz, Brizzi, TapCash, JackCard, MegaCash, and KMT. + © Emoney Info + Ready to scan NFC card + This device does not support NFC. + NFC is turned off. Enable NFC in Settings, then tap your card again. + No transactions found. + E-Money Card + Card number hidden + No transaction yet + Scan a card to load recent history + Rp 0 + Location + No transaction history has been read for this card yet. + %1$d transactions + Ready to scan + Scan result + Card Number + Copy card number + Copied to clipboard + Card Type + LAST ACTIVITY + Latest reader message + Card overview + RECENT TRANSACTIONS + Tap a supported card to read its balance and activity. + App preferences and support + HELP CENTER + Search + ABOUT + NFC ready architecture + Android port prepared for ISO-DEP and FeliCa flows, with parser work following the iOS architecture. + Top Up + Payment + E-Money Card + Mandiri e-Money + BCA Flazz + BRIZZI + TapCash + JackCard + MegaCash + KMT + Failed to read card: %1$s + Unknown error + Card not supported + Failed to read Brizzi card number + Failed Brizzi process step 1 + Failed Brizzi process step 2 + Failed Brizzi process step 3 + Failed Brizzi balance read + Failed to read Mandiri card number + Failed to read Mandiri balance + Brizzi card read successfully. + Brizzi card and transaction history read successfully. + Flazz card read successfully. + Flazz card and transaction history read successfully. + Flazz Classic card detected. Basic card data was read; detailed balance and history parsing is still limited. + TapCash detected but purse data could not be read. + TapCash card read successfully. + TapCash card and recent history read successfully. + Mandiri e-Money card read successfully. This older card log format is not ported yet. + Mandiri e-Money card and transaction log read successfully. + JackCard read successfully. + MegaCash read successfully. + KMT card and travel history read successfully. + Reactivation + Void + Update Balance + Transaction + Black List Card + Statement Fee + Grace Period + Refund + Close + ATU + Cards + Transactions + Balance + App + What cards are supported? + The app supports Mandiri e-money, BCA Flazz, BNI TapCash, BRI Brizzi, JackCard, MegaCash and KMT. Make sure your Android phone supports NFC. + Why is my card not detected? + Make sure NFC is enabled on your Android phone. Hold the card flat against the back of your phone near the NFC antenna and keep it still during scanning. + Card read keeps failing — what should I do? + Try removing any thick phone case, clean the card surface, and try again. If the issue persists, the card may be damaged. + Why are my transactions not showing? + The app reads the recent transactions stored on the card chip itself. Older transactions may not be accessible via NFC. + How do I export transactions to PDF? + After scanning your card, open View Full History, then tap the Export PDF button. You can then share it through WhatsApp, email, or other apps. + The balance shown doesn\'t match. Why? + The app reads balance directly from the card chip in real time. Differences may occur if a recent top-up has not yet been synced to the chip. + Can I top up my card through the app? + No, this app is a read-only reader. Top-up must be done via your bank\'s official app, ATM, or merchant. + How do I change the app language? + Open Settings → Language. The app follows your selection and changes the visible text immediately. + What does Show Card Number do? + When enabled, the full card number is shown on the home screen. When disabled, the first 12 digits are masked for privacy in public spaces. + Privacy first + You can mask card numbers and keep scan details easier to review. + FAQ and support guidance + Version 1.0.0 + This Android port follows the same product direction as the iOS app. Final legal content should be copied from the production source before release. + 1. The app reads supported NFC cards locally on the device. + 2. The app does not modify card balances or write card data. + 3. Ad placements and analytics should be reviewed before release. + NFC reads are processed locally on the device. Release privacy text should be aligned with the final Android implementation and AdMob usage. + 1. Card scans are initiated by the user. + 2. No write operation is performed on cards. + 3. Advertising SDK behavior must be reviewed for production compliance. + Back + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..8f99d16 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..90d21cf --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml new file mode 100644 index 0000000..2906dd9 --- /dev/null +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -0,0 +1,9 @@ + + + + android.nfc.tech.IsoDep + + + android.nfc.tech.NfcF + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4893ab4 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("com.android.application") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" apply false +} diff --git a/docs/CODE_DOCUMENTATION.md b/docs/CODE_DOCUMENTATION.md new file mode 100644 index 0000000..82365db --- /dev/null +++ b/docs/CODE_DOCUMENTATION.md @@ -0,0 +1,414 @@ +# Code Documentation + +Dokumen ini menjelaskan struktur code Android `Emoney Info` agar developer lain bisa lebih cepat memahami project, mencari area yang relevan, dan melakukan perubahan dengan aman. + +## 1. Ringkasan Project + +Project ini adalah versi Android dari aplikasi iOS `Emoney Info`. + +Fungsi utama aplikasi: +- membaca kartu uang elektronik via NFC +- menampilkan saldo dan riwayat transaksi +- menampilkan halaman bantuan dan informasi aplikasi +- mengekspor history transaksi ke PDF +- menampilkan iklan AdMob banner dan interstitial + +Stack utama: +- Kotlin +- Jetpack Compose +- Android NFC (`IsoDep`, `NfcF`, `MifareClassic`) +- Google Mobile Ads SDK + +Package utama: +- `com.korancrew.emoneyinfo` + +## 2. Struktur Folder + +Struktur code utama ada di: + +```text +app/src/main/java/com/korancrew/emoneyinfo +├── ads +├── data +├── nfc +├── pdf +├── ui +├── util +└── MainActivity.kt +``` + +Penjelasan singkat: + +- `ads` + Menyimpan konfigurasi AdMob. + +- `data` + Menyimpan model UI utama dan data FAQ. + +- `nfc` + Berisi seluruh logic pembacaan kartu, helper APDU/FeliCa, kriptografi Brizzi, dan router NFC. + +- `pdf` + Logic export history transaksi menjadi PDF. + +- `ui` + Semua screen Compose, komponen UI, tema, dan root app navigation. + +- `util` + Helper umum. Saat ini dipakai untuk logging debug-only. + +## 3. Entry Point Aplikasi + +File utama: +- [MainActivity.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/MainActivity.kt) + +Tanggung jawab `MainActivity`: +- mengaktifkan theme utama +- inisialisasi AdMob +- membuat `UnifiedNfcReader` +- me-render root Compose lewat `EmoneyInfoApp` +- meneruskan event NFC dari `onNewIntent` +- mengaktifkan dan menonaktifkan foreground dispatch NFC pada `onResume` / `onPause` + +Secara sederhana, `MainActivity` adalah jembatan antara lifecycle Android dengan UI dan NFC reader. + +## 4. Root UI dan Navigasi + +File utama: +- [EmoneyInfoApp.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/EmoneyInfoApp.kt) + +`EmoneyInfoApp` adalah root Compose application. File ini menangani: +- bottom navigation +- `NavHost` dan route screen +- membaca `uiState` dari `UnifiedNfcReader` +- membaca dan menyimpan preference `show card number` + +Route utama: +- `home` +- `settings` +- `history` +- `faq` +- `about` +- `terms` +- `privacy` + +Bottom navigation hanya menampilkan: +- `E-Money` +- `Settings` + +## 5. Screen Utama + +Screen Compose ada di folder: +- `/ui/screens` + +Yang paling penting: + +- [HomeScreen.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/screens/HomeScreen.kt) + Menampilkan hasil scan terakhir, saldo, nomor kartu, dan transaksi terakhir. + +- [HistoryScreen.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/screens/HistoryScreen.kt) + Menampilkan list riwayat transaksi, banner ads, dan tombol export PDF fixed di bawah. + +- [SettingsScreen.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/screens/SettingsScreen.kt) + Menampilkan 4 menu utama: bahasa, tampilkan nomor kartu, pusat bantuan, dan tentang aplikasi. + +- [FaqScreen.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/screens/FaqScreen.kt) + Menampilkan FAQ dan pencarian pertanyaan. + +- [AboutScreen.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/screens/AboutScreen.kt) + Menampilkan info aplikasi dan navigasi ke terms/privacy. + +## 6. Model Data UI + +File utama: +- [Models.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/data/Models.kt) + +Model inti: + +### `CardType` +Enum untuk jenis kartu: +- `MANDIRI` +- `FLAZZ` +- `BRIZZI` +- `TAPCASH` +- `JACKCARD` +- `MEGACASH` +- `KMT` + +### `TransactionItem` +Mewakili 1 item riwayat transaksi. + +Field penting: +- `title` +- `date` +- `amount` +- `isCredit` +- `locationName` + +Method penting: +- `formattedAmount()` +- `formattedDate()` +- `subtitle()` + +Catatan: +- currency selalu diformat sebagai `IDR` / `Rp` +- warna amount di UI ditentukan dari `isCredit` + +### `EmoneyUiState` +State utama yang dipakai UI. + +Field penting: +- `cardType` +- `balance` +- `cardNumber` +- `transactions` +- `scanMessage` +- `isNfcSupported` + +Semua screen utama membaca state ini, terutama `HomeScreen` dan `HistoryScreen`. + +## 7. Arsitektur NFC + +Folder utama: +- `/nfc` + +File yang paling penting: +- [UnifiedNfcReader.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/nfc/UnifiedNfcReader.kt) +- [CardReaders.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/nfc/CardReaders.kt) +- [NfcUtils.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/nfc/NfcUtils.kt) +- [BrizziCrypto.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/nfc/BrizziCrypto.kt) +- [AndroidStrings.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/nfc/AndroidStrings.kt) + +### 7.1 `UnifiedNfcReader` + +`UnifiedNfcReader` adalah orchestrator NFC aplikasi. + +Tanggung jawab: +- mengecek apakah device support NFC +- menyimpan `uiState` dalam `StateFlow` +- menerima `Tag` dari `onNewIntent` +- memilih reader yang sesuai berdasarkan teknologi kartu +- mengubah hasil pembacaan menjadi `EmoneyUiState` +- mengatur pesan UI seperti `tap card hint` dan `tap again hint` + +Reader yang didaftarkan saat ini: +- `IsoDepCardRouter()` +- `MifareClassicCardReader()` +- `FelicaCardReader()` + +### 7.2 `CardReaders.kt` + +File ini berisi implementasi pembacaan kartu per keluarga teknologi dan per jenis kartu. + +Struktur utamanya: +- `CardReader` +- `IsoDepCardRouter` +- `FelicaCardReader` +- `MifareClassicCardReader` +- object reader spesifik, seperti `BrizziReader`, `MandiriReader`, `FlazzReader`, `TapCashReader`, dan lainnya + +Alur umum: +1. router menentukan teknologi kartu +2. router memanggil reader spesifik +3. reader mengirim command APDU atau FeliCa +4. response di-parse menjadi saldo, nomor kartu, dan history +5. hasil akhir dikembalikan sebagai `EmoneyUiState` + +### 7.3 `NfcUtils.kt` + +File ini berisi helper low-level: +- `ApduResponse` +- `transceiveApdu` +- `transceiveSelect` +- helper FeliCa read +- helper hex, endian, format card number, parsing tanggal + +Ini adalah file utility inti untuk operasi byte-level dan APDU. + +### 7.4 `BrizziCrypto.kt` + +Khusus untuk flow Brizzi yang membutuhkan proses auth/crypto. + +File ini sebaiknya disentuh dengan hati-hati karena error kecil pada: +- mode cipher +- IV +- padding +- urutan challenge + +bisa membuat kartu gagal dibaca. + +## 8. Dukungan Kartu + +Kartu yang saat ini sudah menjadi target utama: +- Brizzi +- Flazz BCA +- Mandiri e-Money +- BNI TapCash +- KMT +- JackCard +- MegaCash + +Catatan penting: +- tidak semua kartu memakai flow yang sama +- beberapa kartu memakai `IsoDep` +- KMT memakai `NfcF` +- ada fallback `MifareClassic` untuk kasus tertentu + +Untuk perubahan parser, validasi terbaik tetap lewat test pada kartu fisik nyata. + +## 9. Alur Scan NFC + +Secara sederhana: + +1. `MainActivity.onResume()` mengaktifkan foreground dispatch +2. user menempelkan kartu +3. Android mengirim `Intent` NFC ke activity yang sedang aktif +4. `MainActivity.onNewIntent()` meneruskan intent ke `UnifiedNfcReader` +5. `UnifiedNfcReader` memilih reader yang cocok +6. reader membaca data kartu +7. hasil scan dimasukkan ke `uiState` +8. UI Compose otomatis recompose dan menampilkan saldo/history terbaru + +Setelah scan berhasil, `scanMessage` akan berubah lalu kembali ke hint scan ulang setelah 5 detik. + +## 10. Localization + +String resource: +- [values/strings.xml](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/res/values/strings.xml) +- [values-id/strings.xml](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/res/values-id/strings.xml) + +App mendukung: +- Inggris +- Indonesia + +Catatan: +- bahasa UI mengikuti locale device +- currency tetap `IDR` +- beberapa pesan scan dari layer NFC juga dilokalisasi melalui `AndroidStrings` + +## 11. AdMob + +File utama: +- [AdMobConfig.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ads/AdMobConfig.kt) +- [AdMobBanner.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/components/AdMobBanner.kt) + +Penempatan saat ini: +- banner di `Home` +- banner di `History` +- banner di `Settings` +- interstitial sebelum export PDF + +Catatan implementasi: +- `test device IDs` hanya diaktifkan pada `BuildConfig.DEBUG` +- logging ads dibatasi ke debug build melalui `AppLog` + +## 12. Export PDF + +File utama: +- [HistoryPdfExporter.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/pdf/HistoryPdfExporter.kt) + +Fungsi utama: +- menghasilkan PDF dari `EmoneyUiState` +- menyertakan tipe kartu, saldo, nomor kartu, dan tabel transaksi +- membuka chooser agar file bisa dibuka atau dibagikan + +Flow dari UI: +1. user menekan `Export PDF` +2. jika interstitial tersedia, iklan ditampilkan dulu +3. setelah iklan selesai atau gagal tampil, PDF dibuat +4. file dibuka lewat chooser + +## 13. Preference Lokal + +Saat ini preference utama yang dipakai: +- `masked` + +Disimpan di: +- `SharedPreferences("emoney_info_prefs")` + +Dipakai untuk: +- menentukan apakah nomor kartu ditampilkan penuh atau dimasking + +Catatan: +- key `masked` saat ini bernilai `true` ketika nomor kartu ditampilkan penuh +- nama key ini tidak ideal secara semantik +- kalau nanti ingin dirapikan, sebaiknya diganti menjadi key yang lebih jelas, misalnya `show_card_number` + +## 14. Logging dan Build Production + +File utama: +- [AppLog.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/util/AppLog.kt) +- [app/build.gradle.kts](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/build.gradle.kts) + +Prinsip saat ini: +- log sensitif hanya aktif di debug build +- release build memakai: + - `minifyEnabled = true` + - `shrinkResources = true` + +Ini penting karena flow NFC dan ads punya banyak data internal yang tidak perlu muncul di production log. + +## 15. File Yang Paling Sering Diedit + +Kalau ingin ubah tampilan: +- `/ui/screens` +- `/ui/theme` +- `/res/values` + +Kalau ingin ubah parser kartu: +- `/nfc/CardReaders.kt` +- `/nfc/NfcUtils.kt` +- `/nfc/BrizziCrypto.kt` + +Kalau ingin ubah model dan formatting: +- `/data/Models.kt` + +Kalau ingin ubah ads: +- `/ads/AdMobConfig.kt` +- `/ui/components/AdMobBanner.kt` + +Kalau ingin ubah PDF: +- `/pdf/HistoryPdfExporter.kt` + +## 16. Risiko dan Catatan Maintenance + +Beberapa area sensitif: + +- parser NFC + Perubahan kecil bisa memutus pembacaan kartu tertentu. + +- flow Brizzi + Paling sensitif karena ada auth dan kriptografi. + +- formatting UI berdasarkan `locationName` + KMT memakai lokasi valid, tapi kartu lain tidak selalu punya mapping lokasi yang berguna. + +- preference `masked` + Namanya membingungkan dan bisa memicu bug kalau dibaca tanpa konteks. + +- ads production + Pastikan testing device ID tidak dipakai untuk release path, dan lakukan uji di device nyata. + +## 17. Saran Pengembangan Selanjutnya + +Beberapa perbaikan yang layak dipertimbangkan: + +- pecah `CardReaders.kt` menjadi beberapa file per kartu agar lebih mudah dirawat +- ganti `SharedPreferences` sederhana ke wrapper preference yang lebih eksplisit +- buat test parser terpisah untuk data response yang sudah diketahui +- tambahkan dokumentasi mapping kartu dan APDU per provider +- rapikan naming key `masked` + +## 18. Ringkasan Cepat + +Kalau hanya ingin memahami project dengan cepat, mulai dari file ini: + +1. [MainActivity.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/MainActivity.kt) +2. [EmoneyInfoApp.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/EmoneyInfoApp.kt) +3. [Models.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/data/Models.kt) +4. [UnifiedNfcReader.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/nfc/UnifiedNfcReader.kt) +5. [CardReaders.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/nfc/CardReaders.kt) +6. [HistoryScreen.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/ui/screens/HistoryScreen.kt) +7. [HistoryPdfExporter.kt](/Users/wirabasalamah/work/gitlab/EmoneyInfo-andro/app/src/main/java/com/korancrew/emoneyinfo/pdf/HistoryPdfExporter.kt) + +Dengan urutan itu, sebagian besar arsitektur project sudah akan terlihat. diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8f2e28c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2733ed5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..85aacdb --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Emoney Info" +include(":app")