package id.pribadi.qrisnotifier import android.content.Context import android.content.SharedPreferences import org.json.JSONArray import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.atomic.AtomicBoolean /** Antrean kirim ke server: notifikasi disimpan dulu, dikirim ulang sampai server menjawab. */ object Sender { private const val PREF = "qris_notifier" private const val MAX_QUEUE = 500 private val lock = Any() private val flushing = AtomicBoolean(false) // Paket aplikasi umum. Periksa & sesuaikan dengan aplikasi yang kamu pakai menerima uang. const val DEFAULT_PACKAGES = "com.gojek.gopaymerchant,com.gojek.app,id.dana,ovo.id,com.shopee.id," + "com.telkom.mwallet,com.bca,id.co.bri.brimo,id.bmri.livin,src.com.bni" fun prefs(c: Context): SharedPreferences = c.getSharedPreferences(PREF, Context.MODE_PRIVATE) /** Mode diagnosa: selama belum lewat batas waktu, SEMUA notifikasi (selain milik aplikasi ini) dikirim. */ fun diagnosticLeftMs(c: Context): Long = maxOf(0L, prefs(c).getLong("diag_until", 0L) - System.currentTimeMillis()) fun startDiagnostic(c: Context, minutes: Int = 10) { prefs(c).edit().putLong("diag_until", System.currentTimeMillis() + minutes * 60_000L).apply() } fun stopDiagnostic(c: Context) { prefs(c).edit().putLong("diag_until", 0L).apply() } /** Paket ada di daftar pantauan (pembayaran sungguhan, diproses normal oleh server). */ fun inList(c: Context, pkg: String): Boolean { val list = prefs(c).getString("packages", DEFAULT_PACKAGES).orEmpty() .split(",").map { it.trim().lowercase() }.filter { it.isNotEmpty() } return pkg.lowercase() in list } fun allowed(c: Context, pkg: String): Boolean = inList(c, pkg) || (diagnosticLeftMs(c) > 0 && pkg != c.packageName) fun enqueue(c: Context, pkg: String, title: String, text: String, postedAt: Long, diag: Boolean = false) { synchronized(lock) { val old = JSONArray(prefs(c).getString("queue", "[]")) val arr = JSONArray() val start = maxOf(0, old.length() - (MAX_QUEUE - 1)) for (i in start until old.length()) arr.put(old.get(i)) arr.put( JSONObject() .put("package", pkg) .put("title", title) .put("text", text) .put("posted_at", postedAt) .put("diag", diag) // true = hanya untuk dilihat, server tidak mencocokkan dengan order ) prefs(c).edit().putString("queue", arr.toString()).apply() } } fun queueSize(c: Context): Int = synchronized(lock) { JSONArray(prefs(c).getString("queue", "[]")).length() } private fun peek(c: Context): JSONObject? = synchronized(lock) { val arr = JSONArray(prefs(c).getString("queue", "[]")) if (arr.length() == 0) null else arr.getJSONObject(0) } private fun pop(c: Context) = synchronized(lock) { val arr = JSONArray(prefs(c).getString("queue", "[]")) if (arr.length() > 0) { arr.remove(0) prefs(c).edit().putString("queue", arr.toString()).apply() } } /** Kirim semua isi antrean di thread terpisah. Berhenti pada kegagalan pertama (dicoba lagi nanti). */ fun flush(c: Context) { val app = c.applicationContext if (!flushing.compareAndSet(false, true)) return Thread { try { while (true) { val item = peek(app) ?: break if (!post(app, item)) break pop(app) } } finally { flushing.set(false) } }.start() } /** true = item selesai (sukses, atau ditolak permanen karena datanya salah) dan boleh dibuang. */ private fun post(c: Context, item: JSONObject): Boolean { val p = prefs(c) val base = p.getString("server", "").orEmpty().trim().trimEnd('/') val key = p.getString("device_key", "").orEmpty().trim() if (base.isEmpty() || key.isEmpty()) return false return try { val conn = (URL("$base/api/notify.php").openConnection() as HttpURLConnection).apply { requestMethod = "POST" connectTimeout = 10_000 readTimeout = 15_000 doOutput = true setRequestProperty("Content-Type", "application/json") setRequestProperty("X-Device-Key", key) } conn.outputStream.use { it.write(item.toString().toByteArray(Charsets.UTF_8)) } val code = conn.responseCode conn.disconnect() p.edit().putString("last", "HTTP $code ยท ${java.text.SimpleDateFormat("HH:mm:ss").format(java.util.Date())}").apply() code in 200..299 || (code in 400..499 && code !in listOf(401, 403, 404, 408, 429)) } catch (e: Exception) { p.edit().putString("last", "Gagal: ${e.javaClass.simpleName}").apply() false } } }