// inMOLA Analytics — Android SDK (tek dosya, bağımlılık yok). minSdk 16+. // // Kurulum: bu dosyayı projenize ekleyin (paket adını değiştirebilirsiniz). AndroidManifest.xml'de INTERNET izni olmalı: // // Application.onCreate(): InmolaAnalytics.init(this, "SITE_ANAHTARI") // Ekran: InmolaAnalytics.screen("Ürün Detay") // (Activity'ler kendiliğinden ekran sayılır; Compose / tek Activity'de screen() çağırın) // Olay: InmolaAnalytics.event("sepete_ekle", "SKU-1") // Satın alma: InmolaAnalytics.purchase("SIP-1001", 1499.90, "TRY", // listOf(InmolaAnalytics.Item("SKU-1", "Ürün", 1499.90, 1))) // // Gizlilik: reklam kimliği / Android ID KULLANILMAZ, cihaza hiçbir şey yazılmaz, kişisel veri gönderilmez. // Kayıtlar bellekte toplanır, ~10 sn'de bir ve uygulama arka plana geçerken gönderilir. package com.inmola.analytics import android.app.Activity import android.app.Application import android.os.Build import android.os.Bundle import java.net.HttpURLConnection import java.util.Locale import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit object InmolaAnalytics { data class Item(val id: String, val name: String, val price: Double, val quantity: Int = 1) /** Gönderim kapalı (ör. kullanıcı analitiği reddettiyse). Kapalıyken hiçbir şey toplanmaz. */ @Volatile var enabled = true /** Activity'leri kendiliğinden ekran olarak say (sınıf adıyla, "Activity" eki atılır). */ @Volatile var autoScreens = true private var core: Core? = null private var started = 0 @JvmStatic @JvmOverloads fun init(app: Application, siteKey: String, endpoint: String = Core.DEFAULT_ENDPOINT) { if (core != null) return val res = app.resources val version = try { app.packageManager.getPackageInfo(app.packageName, 0).versionName ?: "" } catch (e: Exception) { "" } val loc = Locale.getDefault() val c = Core(siteKey, endpoint, mapOf( "b" to app.packageName.lowercase(Locale.ROOT), "p" to "android", "ov" to (Build.VERSION.RELEASE ?: ""), "dm" to (Build.MODEL ?: ""), "av" to version, "l" to loc.language + (if (loc.country.isNotEmpty()) "-" + loc.country else ""), "tab" to (if (res.configuration.smallestScreenWidthDp >= 600) 1 else 0), "w" to (res.displayMetrics.widthPixels / res.displayMetrics.density).toInt())) core = c app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { override fun onActivityStarted(a: Activity) { if (started++ == 0) c.foreground() } override fun onActivityStopped(a: Activity) { if (--started == 0) c.background() } override fun onActivityResumed(a: Activity) { if (autoScreens) c.screen(a.javaClass.simpleName.removeSuffix("Activity").ifEmpty { "Main" }) } override fun onActivityCreated(a: Activity, b: Bundle?) {} override fun onActivityPaused(a: Activity) {} override fun onActivitySaveInstanceState(a: Activity, b: Bundle) {} override fun onActivityDestroyed(a: Activity) {} }) } /** Ekran açıldı. Elle çağırdığınızda Activity adıyla otomatik sayımı kapatmak için autoScreens = false yapın. */ @JvmStatic fun screen(name: String) { if (enabled) core?.screen(name) } @JvmStatic @JvmOverloads fun event(name: String, detail: String = "") { if (enabled) core?.event(name, detail) } @JvmStatic @JvmOverloads fun purchase(transactionId: String, value: Double, currency: String, items: List = emptyList()) { if (enabled) core?.purchase(transactionId, value, currency, items) } /** Android'den bağımsız çekirdek (JVM'de test edilebilir): kuyruk, oturum, gönderim. */ class Core(private val key: String, private val endpoint: String, private val base: Map) { companion object { const val DEFAULT_ENDPOINT = "https://inmola-analytics.iletisim-845.workers.dev/m" } private val ex: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() private val pending = ArrayList>() private var screen: String? = null private var screenStart = 0L private var screenEntry = false private var newSession = true private var backgroundAt = 0L init { ex.scheduleWithFixedDelay({ flush() }, 10, 10, TimeUnit.SECONDS) } fun screen(name: String) = ex.execute { show(name, System.currentTimeMillis()) } fun event(name: String, detail: String) = ex.execute { add(mapOf("t" to "ev", "sc" to (screen ?: ""), "n" to name, "dd" to detail)) } fun purchase(tx: String, value: Double, currency: String, items: List) = ex.execute { add(mapOf("t" to "ev", "sc" to (screen ?: ""), "n" to "purchase", "dd" to tx, "v" to value, "cur" to currency.uppercase(Locale.ROOT), "it" to items.take(50).map { mapOf("n" to it.name, "id" to it.id, "p" to it.price, "q" to it.quantity) })) flush() } fun background() = ex.execute { backgroundAt = System.currentTimeMillis(); end(false, backgroundAt); flush() } fun foreground() = ex.execute { val away = if (backgroundAt > 0) System.currentTimeMillis() - backgroundAt else 0 backgroundAt = 0 val s = screen ?: return@execute newSession = away > 30 * 60 * 1000 // 30 dk'dan uzun arka plan = yeni oturum screen = null show(s, System.currentTimeMillis()) } /** Test için: bekleyen kayıtları hemen gönder ve bitmesini bekle. */ fun flushNow() { ex.submit { flush() }.get() } private fun show(name: String, now: Long) { if (name == screen) return end(true, now) screen = name; screenStart = now; screenEntry = newSession add(mapOf("t" to "pv", "sc" to name, "ent" to if (newSession) 1 else 0)) newSession = false } private fun end(next: Boolean, now: Long) { val s = screen ?: return add(mapOf("t" to "en", "sc" to s, "sec" to minOf((now - screenStart) / 1000, 3600L), "nx" to if (next) 1 else 0, "ent" to if (screenEntry) 1 else 0)) } private fun add(e: Map) { if (!InmolaAnalytics.enabled || key.isEmpty()) return pending.add(e) if (pending.size >= 20) flush() } private fun flush() { if (pending.isEmpty()) return val batch = ArrayList(pending.take(50)) repeat(batch.size) { pending.removeAt(0) } val body = HashMap(base).apply { put("s", key); put("ev", batch) } var ok = false try { val c = java.net.URI(endpoint).toURL().openConnection() as HttpURLConnection c.requestMethod = "POST"; c.doOutput = true; c.connectTimeout = 10000; c.readTimeout = 10000 c.setRequestProperty("Content-Type", "text/plain") c.outputStream.use { it.write(json(body).toByteArray(Charsets.UTF_8)) } ok = c.responseCode < 500 c.disconnect() } catch (e: Exception) { } // Ağ yoksa geri koy (bellekte en fazla 200 kayıt), sonraki turda tekrar denenir if (!ok && pending.size < 200) pending.addAll(0, batch) } private fun json(v: Any?): String = when (v) { null -> "null" is String -> "\"" + v.flatMap { ch -> when { ch == '"' -> listOf('\\', '"'); ch == '\\' -> listOf('\\', '\\') ch < ' ' -> String.format("\\u%04x", ch.code).toList(); else -> listOf(ch) } }.joinToString("") + "\"" is Number, is Boolean -> v.toString() is Map<*, *> -> v.entries.joinToString(",", "{", "}") { json(it.key.toString()) + ":" + json(it.value) } is List<*> -> v.joinToString(",", "[", "]") { json(it) } else -> json(v.toString()) } } }