diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8923f71..b81fe2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,8 +27,10 @@ jobs: distribution: 'temurin' cache: gradle - - name: Setup Android SDK - uses: android-actions/setup-android@v3 + # Runner ubuntu-latest sudah menyertakan Android SDK (ANDROID_SDK_ROOT). + # setup-android@v3 dihapus karena crash saat accept licenses di Node 24. + - name: Accept Android licenses + run: yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null || true - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4fc15c5..2811967 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,15 +5,15 @@ plugins { } android { - namespace = "com.itsazni.notificationforwarder" + namespace = "my.id.adngopi.bridge" compileSdk = 36 defaultConfig { - applicationId = "com.itsazni.notificationforwarder" + applicationId = "my.id.adngopi.bridge" minSdk = 26 targetSdk = 36 - versionCode = 1 - versionName = "1.1.0" + versionCode = 2 + versionName = "1.2.0-formify.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -76,4 +76,7 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") implementation("com.google.code.gson:gson:2.11.0") + + // Formify: QR config scanner (dashboard provisioning) + implementation("com.journeyapps:zxing-android-embedded:4.3.0") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d78209..d3e6df6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,14 +1,18 @@ - + + + + + + + + + + diff --git a/app/src/main/java/my/id/adngopi/bridge/BridgeConfig.kt b/app/src/main/java/my/id/adngopi/bridge/BridgeConfig.kt new file mode 100644 index 0000000..ae1443c --- /dev/null +++ b/app/src/main/java/my/id/adngopi/bridge/BridgeConfig.kt @@ -0,0 +1,56 @@ +package my.id.adngopi.bridge + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.JsonObject +import my.id.adngopi.bridge.settings.AuthMode +import my.id.adngopi.bridge.settings.FilterMode +import my.id.adngopi.bridge.settings.SettingsStore + +/** + * Formify Bridge provisioning. + * + * QR config JSON dari dashboard Formify, contoh: + * { + * "v": 1, + * "webhookUrl": "https://adngopi.my.id/api/webhooks/dana-bridge", + * "bearerToken": "", + * "filterPackages": ["id.dana"], + * "deviceLabel": "HP-KASIR-01" + * } + */ +object BridgeConfig { + const val CONFIG_VERSION = 1 + + fun applyJson(context: Context, json: String): Boolean { + return try { + val obj = Gson().fromJson(json, JsonObject::class.java) ?: return false + if (!obj.has("webhookUrl") || obj.get("webhookUrl").asString.isBlank()) return false + val version = if (obj.has("v")) obj.get("v").asInt else 1 + if (version != CONFIG_VERSION) return false + + val store = SettingsStore(context) + store.webhookUrl = obj.get("webhookUrl").asString.trim() + if (obj.has("webhookMethod")) { + store.webhookMethod = obj.get("webhookMethod").asString + } + store.authMode = AuthMode.BEARER + if (obj.has("bearerToken")) { + store.bearerToken = obj.get("bearerToken").asString + } + store.filterMode = FilterMode.WHITELIST + if (obj.has("filterPackages")) { + val pkgs = obj.getAsJsonArray("filterPackages").map { it.asString.trim() } + .filter { it.isNotEmpty() }.toSet() + if (pkgs.isNotEmpty()) store.filterPackages = pkgs + } + if (obj.has("deviceLabel")) { + store.customHeadersRaw = "X-Device-Label: ${obj.get("deviceLabel").asString.trim()}" + } + store.forwardingEnabled = true + true + } catch (_: Exception) { + false + } + } +} diff --git a/app/src/main/java/com/itsazni/notificationforwarder/MainActivity.kt b/app/src/main/java/my/id/adngopi/bridge/MainActivity.kt similarity index 96% rename from app/src/main/java/com/itsazni/notificationforwarder/MainActivity.kt rename to app/src/main/java/my/id/adngopi/bridge/MainActivity.kt index 9b2bd35..ae45eab 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/MainActivity.kt +++ b/app/src/main/java/my/id/adngopi/bridge/MainActivity.kt @@ -1,4 +1,4 @@ -package com.itsazni.notificationforwarder +package my.id.adngopi.bridge import android.content.ComponentName import android.content.Context @@ -59,17 +59,17 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.itsazni.notificationforwarder.data.NotificationRepository -import com.itsazni.notificationforwarder.data.QueueItem -import com.itsazni.notificationforwarder.data.QueueStats -import com.itsazni.notificationforwarder.data.QueueStatus -import com.itsazni.notificationforwarder.network.WebhookClient -import com.itsazni.notificationforwarder.settings.AppSettings -import com.itsazni.notificationforwarder.settings.AuthMode -import com.itsazni.notificationforwarder.settings.FilterMode -import com.itsazni.notificationforwarder.settings.SettingsStore -import com.itsazni.notificationforwarder.ui.theme.AppTheme -import com.itsazni.notificationforwarder.worker.WorkerScheduler +import my.id.adngopi.bridge.data.NotificationRepository +import my.id.adngopi.bridge.data.QueueItem +import my.id.adngopi.bridge.data.QueueStats +import my.id.adngopi.bridge.data.QueueStatus +import my.id.adngopi.bridge.network.WebhookClient +import my.id.adngopi.bridge.settings.AppSettings +import my.id.adngopi.bridge.settings.AuthMode +import my.id.adngopi.bridge.settings.FilterMode +import my.id.adngopi.bridge.settings.SettingsStore +import my.id.adngopi.bridge.ui.theme.AppTheme +import my.id.adngopi.bridge.worker.WorkerScheduler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -303,6 +303,12 @@ private fun HomeScreen(modifier: Modifier, stats: QueueStats) { ) { Text("Sync Queue") } + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { context.startActivity(Intent(context, ScanConfigActivity::class.java)) } + ) { + Text("Scan QR Config Formify") + } } } } @@ -692,7 +698,7 @@ private fun isNotificationListenerEnabled(context: Context): Boolean { if (enabled.isNullOrBlank()) { return false } - val target = ComponentName(context, com.itsazni.notificationforwarder.service.AppNotificationListenerService::class.java) + val target = ComponentName(context, my.id.adngopi.bridge.service.AppNotificationListenerService::class.java) return enabled.contains(target.flattenToString()) } diff --git a/app/src/main/java/com/itsazni/notificationforwarder/NotificationForwarderApp.kt b/app/src/main/java/my/id/adngopi/bridge/NotificationForwarderApp.kt similarity index 64% rename from app/src/main/java/com/itsazni/notificationforwarder/NotificationForwarderApp.kt rename to app/src/main/java/my/id/adngopi/bridge/NotificationForwarderApp.kt index 88ed3af..54f6ea0 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/NotificationForwarderApp.kt +++ b/app/src/main/java/my/id/adngopi/bridge/NotificationForwarderApp.kt @@ -1,7 +1,7 @@ -package com.itsazni.notificationforwarder +package my.id.adngopi.bridge import android.app.Application -import com.itsazni.notificationforwarder.worker.WorkerScheduler +import my.id.adngopi.bridge.worker.WorkerScheduler class NotificationForwarderApp : Application() { override fun onCreate() { diff --git a/app/src/main/java/my/id/adngopi/bridge/ScanConfigActivity.kt b/app/src/main/java/my/id/adngopi/bridge/ScanConfigActivity.kt new file mode 100644 index 0000000..9d9ce7f --- /dev/null +++ b/app/src/main/java/my/id/adngopi/bridge/ScanConfigActivity.kt @@ -0,0 +1,42 @@ +package my.id.adngopi.bridge + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import com.google.zxing.integration.android.IntentIntegrator + +/** + * Scan QR config dari dashboard Formify, terapkan via [BridgeConfig], lalu tutup. + * Dibuka dari tombol "Scan QR Config Formify" di tab Home. + */ +class ScanConfigActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + IntentIntegrator(this) + .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) + .setPrompt("Scan QR config dari dashboard Formify") + .setBeepEnabled(true) + .setOrientationLocked(false) + .initiateScan() + } + + @Deprecated("Used by zxing IntentIntegrator") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + if (result != null && !result.contents.isNullOrBlank()) { + val applied = BridgeConfig.applyJson(this, result.contents) + Toast.makeText( + this, + if (applied) "Config Formify diterapkan. Cek tab Webhook & Filter." + else "QR tidak valid (bukan config Formify).", + Toast.LENGTH_LONG + ).show() + } else { + Toast.makeText(this, "Scan dibatalkan.", Toast.LENGTH_SHORT).show() + } + finish() + super.onActivityResult(requestCode, resultCode, data) + } +} diff --git a/app/src/main/java/com/itsazni/notificationforwarder/data/AppDatabase.kt b/app/src/main/java/my/id/adngopi/bridge/data/AppDatabase.kt similarity index 94% rename from app/src/main/java/com/itsazni/notificationforwarder/data/AppDatabase.kt rename to app/src/main/java/my/id/adngopi/bridge/data/AppDatabase.kt index 1d2c354..0543928 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/data/AppDatabase.kt +++ b/app/src/main/java/my/id/adngopi/bridge/data/AppDatabase.kt @@ -1,4 +1,4 @@ -package com.itsazni.notificationforwarder.data +package my.id.adngopi.bridge.data import android.content.Context import androidx.room.Database diff --git a/app/src/main/java/com/itsazni/notificationforwarder/data/NotificationRepository.kt b/app/src/main/java/my/id/adngopi/bridge/data/NotificationRepository.kt similarity index 94% rename from app/src/main/java/com/itsazni/notificationforwarder/data/NotificationRepository.kt rename to app/src/main/java/my/id/adngopi/bridge/data/NotificationRepository.kt index e6c4502..ab3eacd 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/data/NotificationRepository.kt +++ b/app/src/main/java/my/id/adngopi/bridge/data/NotificationRepository.kt @@ -1,8 +1,8 @@ -package com.itsazni.notificationforwarder.data +package my.id.adngopi.bridge.data import android.content.Context -import com.itsazni.notificationforwarder.settings.FilterMode -import com.itsazni.notificationforwarder.settings.SettingsStore +import my.id.adngopi.bridge.settings.FilterMode +import my.id.adngopi.bridge.settings.SettingsStore class NotificationRepository(private val context: Context) { private val dao = AppDatabase.getInstance(context).queueDao() diff --git a/app/src/main/java/com/itsazni/notificationforwarder/data/QueueDao.kt b/app/src/main/java/my/id/adngopi/bridge/data/QueueDao.kt similarity index 97% rename from app/src/main/java/com/itsazni/notificationforwarder/data/QueueDao.kt rename to app/src/main/java/my/id/adngopi/bridge/data/QueueDao.kt index 00fdcd4..850f005 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/data/QueueDao.kt +++ b/app/src/main/java/my/id/adngopi/bridge/data/QueueDao.kt @@ -1,4 +1,4 @@ -package com.itsazni.notificationforwarder.data +package my.id.adngopi.bridge.data import androidx.room.Dao import androidx.room.Insert diff --git a/app/src/main/java/com/itsazni/notificationforwarder/data/QueueItem.kt b/app/src/main/java/my/id/adngopi/bridge/data/QueueItem.kt similarity index 95% rename from app/src/main/java/com/itsazni/notificationforwarder/data/QueueItem.kt rename to app/src/main/java/my/id/adngopi/bridge/data/QueueItem.kt index f59a58c..0d3b339 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/data/QueueItem.kt +++ b/app/src/main/java/my/id/adngopi/bridge/data/QueueItem.kt @@ -1,4 +1,4 @@ -package com.itsazni.notificationforwarder.data +package my.id.adngopi.bridge.data import androidx.room.Entity import androidx.room.Index diff --git a/app/src/main/java/com/itsazni/notificationforwarder/network/WebhookClient.kt b/app/src/main/java/my/id/adngopi/bridge/network/WebhookClient.kt similarity index 97% rename from app/src/main/java/com/itsazni/notificationforwarder/network/WebhookClient.kt rename to app/src/main/java/my/id/adngopi/bridge/network/WebhookClient.kt index 7eb6830..57098b5 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/network/WebhookClient.kt +++ b/app/src/main/java/my/id/adngopi/bridge/network/WebhookClient.kt @@ -1,8 +1,8 @@ -package com.itsazni.notificationforwarder.network +package my.id.adngopi.bridge.network import com.google.gson.Gson import com.google.gson.JsonParser -import com.itsazni.notificationforwarder.data.QueueItem +import my.id.adngopi.bridge.data.QueueItem import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient diff --git a/app/src/main/java/com/itsazni/notificationforwarder/receiver/BootCompletedReceiver.kt b/app/src/main/java/my/id/adngopi/bridge/receiver/BootCompletedReceiver.kt similarity index 81% rename from app/src/main/java/com/itsazni/notificationforwarder/receiver/BootCompletedReceiver.kt rename to app/src/main/java/my/id/adngopi/bridge/receiver/BootCompletedReceiver.kt index 5c99aa4..31a7582 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/receiver/BootCompletedReceiver.kt +++ b/app/src/main/java/my/id/adngopi/bridge/receiver/BootCompletedReceiver.kt @@ -1,9 +1,9 @@ -package com.itsazni.notificationforwarder.receiver +package my.id.adngopi.bridge.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.itsazni.notificationforwarder.worker.WorkerScheduler +import my.id.adngopi.bridge.worker.WorkerScheduler class BootCompletedReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { diff --git a/app/src/main/java/com/itsazni/notificationforwarder/service/AppNotificationListenerService.kt b/app/src/main/java/my/id/adngopi/bridge/service/AppNotificationListenerService.kt similarity index 95% rename from app/src/main/java/com/itsazni/notificationforwarder/service/AppNotificationListenerService.kt rename to app/src/main/java/my/id/adngopi/bridge/service/AppNotificationListenerService.kt index 8683ff1..a73e04f 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/service/AppNotificationListenerService.kt +++ b/app/src/main/java/my/id/adngopi/bridge/service/AppNotificationListenerService.kt @@ -1,11 +1,11 @@ -package com.itsazni.notificationforwarder.service +package my.id.adngopi.bridge.service import android.app.Notification import android.os.Build import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification -import com.itsazni.notificationforwarder.data.NotificationRepository -import com.itsazni.notificationforwarder.worker.WorkerScheduler +import my.id.adngopi.bridge.data.NotificationRepository +import my.id.adngopi.bridge.worker.WorkerScheduler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/app/src/main/java/com/itsazni/notificationforwarder/settings/SettingsStore.kt b/app/src/main/java/my/id/adngopi/bridge/settings/SettingsStore.kt similarity index 86% rename from app/src/main/java/com/itsazni/notificationforwarder/settings/SettingsStore.kt rename to app/src/main/java/my/id/adngopi/bridge/settings/SettingsStore.kt index b3903ee..5214737 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/settings/SettingsStore.kt +++ b/app/src/main/java/my/id/adngopi/bridge/settings/SettingsStore.kt @@ -1,4 +1,4 @@ -package com.itsazni.notificationforwarder.settings +package my.id.adngopi.bridge.settings import android.content.Context import android.content.SharedPreferences @@ -26,8 +26,9 @@ class SettingsStore(context: Context) { private val prefs: SharedPreferences = context.applicationContext.getSharedPreferences("notif_settings", Context.MODE_PRIVATE) + // Formify defaults: webhook Formify + whitelist DANA + auth bearer (token via QR config). var webhookUrl: String - get() = prefs.getString(KEY_WEBHOOK_URL, "") ?: "" + get() = prefs.getString(KEY_WEBHOOK_URL, FORMIFY_DEFAULT_WEBHOOK_URL) ?: FORMIFY_DEFAULT_WEBHOOK_URL set(value) = prefs.edit { putString(KEY_WEBHOOK_URL, value.trim()) } var forwardingEnabled: Boolean @@ -35,15 +36,18 @@ class SettingsStore(context: Context) { set(value) = prefs.edit { putBoolean(KEY_FORWARDING_ENABLED, value) } var filterMode: FilterMode - get() = FilterMode.valueOf(prefs.getString(KEY_FILTER_MODE, FilterMode.ALL_APPS.name)!!) + get() = FilterMode.valueOf(prefs.getString(KEY_FILTER_MODE, FilterMode.WHITELIST.name)!!) set(value) = prefs.edit { putString(KEY_FILTER_MODE, value.name) } var filterPackages: Set - get() = parsePackages(prefs.getString(KEY_FILTER_PACKAGES, "") ?: "") + get() { + val raw = prefs.getString(KEY_FILTER_PACKAGES, null) + return if (raw.isNullOrEmpty()) FORMIFY_DEFAULT_PACKAGES else parsePackages(raw) + } set(value) = prefs.edit { putString(KEY_FILTER_PACKAGES, value.joinToString(",")) } var authMode: AuthMode - get() = AuthMode.valueOf(prefs.getString(KEY_AUTH_MODE, AuthMode.NONE.name)!!) + get() = AuthMode.valueOf(prefs.getString(KEY_AUTH_MODE, AuthMode.BEARER.name)!!) set(value) = prefs.edit { putString(KEY_AUTH_MODE, value.name) } var bearerToken: String @@ -124,6 +128,12 @@ class SettingsStore(context: Context) { } companion object { + // Ganti dengan domain produksi bila berbeda. + const val FORMIFY_DEFAULT_WEBHOOK_URL = "https://adngopi.my.id/api/webhooks/dana-bridge" + // Package DANA Indonesia. Verifikasi di tab Queue setelah install: + // jika notifikasi DANA tidak masuk, sesuaikan whitelist dengan package yang tampil. + val FORMIFY_DEFAULT_PACKAGES: Set = setOf("id.dana") + private const val KEY_WEBHOOK_URL = "webhook_url" private const val KEY_FORWARDING_ENABLED = "forwarding_enabled" private const val KEY_FILTER_MODE = "filter_mode" diff --git a/app/src/main/java/com/itsazni/notificationforwarder/ui/theme/AppTheme.kt b/app/src/main/java/my/id/adngopi/bridge/ui/theme/AppTheme.kt similarity index 94% rename from app/src/main/java/com/itsazni/notificationforwarder/ui/theme/AppTheme.kt rename to app/src/main/java/my/id/adngopi/bridge/ui/theme/AppTheme.kt index 9be07b2..28a653a 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/ui/theme/AppTheme.kt +++ b/app/src/main/java/my/id/adngopi/bridge/ui/theme/AppTheme.kt @@ -1,4 +1,4 @@ -package com.itsazni.notificationforwarder.ui.theme +package my.id.adngopi.bridge.ui.theme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/com/itsazni/notificationforwarder/worker/QueueWorker.kt b/app/src/main/java/my/id/adngopi/bridge/worker/QueueWorker.kt similarity index 89% rename from app/src/main/java/com/itsazni/notificationforwarder/worker/QueueWorker.kt rename to app/src/main/java/my/id/adngopi/bridge/worker/QueueWorker.kt index d2d2842..e0b612b 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/worker/QueueWorker.kt +++ b/app/src/main/java/my/id/adngopi/bridge/worker/QueueWorker.kt @@ -1,13 +1,13 @@ -package com.itsazni.notificationforwarder.worker +package my.id.adngopi.bridge.worker import android.content.Context import android.provider.Settings import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.itsazni.notificationforwarder.data.NotificationRepository -import com.itsazni.notificationforwarder.network.WebhookClient -import com.itsazni.notificationforwarder.settings.AuthMode -import com.itsazni.notificationforwarder.settings.SettingsStore +import my.id.adngopi.bridge.data.NotificationRepository +import my.id.adngopi.bridge.network.WebhookClient +import my.id.adngopi.bridge.settings.AuthMode +import my.id.adngopi.bridge.settings.SettingsStore class QueueWorker( appContext: Context, diff --git a/app/src/main/java/com/itsazni/notificationforwarder/worker/WorkerScheduler.kt b/app/src/main/java/my/id/adngopi/bridge/worker/WorkerScheduler.kt similarity index 97% rename from app/src/main/java/com/itsazni/notificationforwarder/worker/WorkerScheduler.kt rename to app/src/main/java/my/id/adngopi/bridge/worker/WorkerScheduler.kt index 1521d34..087dce8 100644 --- a/app/src/main/java/com/itsazni/notificationforwarder/worker/WorkerScheduler.kt +++ b/app/src/main/java/my/id/adngopi/bridge/worker/WorkerScheduler.kt @@ -1,4 +1,4 @@ -package com.itsazni.notificationforwarder.worker +package my.id.adngopi.bridge.worker import android.content.Context import androidx.work.BackoffPolicy