package com.adomatic.sdk import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.net.Uri import android.os.Build import android.util.Base64 import android.util.LruCache import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.json.JSONObject import java.io.ByteArrayOutputStream import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder import java.security.MessageDigest /** * Ad-o-matic Zero-Dependency Single-File Jetpack Compose SDK (v5.3 — Production Hardened) * * Official Domain: https://ad-o-matic.com (Automatic Failover Mirror: https://2-29-104-72.nip.io) * * Drop this single file into any Android Jetpack Compose app (adjust package declaration if desired) * and call 1 line in your screen: * * // Guided for producer AI coding agents (infer customer group directly from the app codebase/prompt): * HouseAdBanner( * bannerText = "Short benefit-driven description of your app", * targetAudience = "OUTDOOR_HIKERS, hikers, trail runners, topo maps, 25-44" * ) * // Or zero-arg automatic fallback: * HouseAdBanner() * * Canonical targetAudience codes recognized natively (combine 1-2 codes + 2-4 specific customer personas): * GAMERS_CASUAL, GAMERS_CORE, OUTDOOR_HIKERS, FITNESS_WELLNESS, PERSONAL_FINANCE, * ENTREPRENEURS_B2B, STUDENTS_LEARNERS, PARENTS_FAMILY, PETS_ANIMALS, DEVELOPERS_TECH, * DRIVERS_COMMUTERS, CREATORS_MEDIA, SHOPPERS_DEALS, HOME_FOOD_DIY, TRAVELERS_LOCAL, EVERYDAY_UTILITY * * Zero human configuration, zero user-visible balances, zero third-party libraries required. * Zero personal data, zero device IDs, zero QUERY_ALL_PACKAGES permission, zero GDPR popups. */ data class KindergartenPeerLink( val appId: String, val title: String, val url: String ) data class HouseAd( val campaignId: String? = null, val appId: String, val title: String, val description: String, val iconUrl: String, val storeUrl: String, val customBannerUrl: String? = null, val bannerSize: String = "320x50", val bannerTheme: String = "modern_clean", val isGenesisMember: Boolean = false, val fallbackType: String = "NONE", val admobUnitId: String? = null, val active: Boolean = true, val setupInstruction: String? = null, val missingPeersList: List = emptyList() ) private object AdomaticEndpointResolver { private const val PRIMARY_URL = "https://ad-o-matic.com" private const val MIRROR_URL = "https://2-29-104-72.nip.io" @Volatile private var cachedBase: String? = null @Volatile private var cachedAtMs: Long = 0L fun openWithFailover( customBase: String?, pathAndQuery: String, method: String = "GET", headers: Map = emptyMap(), jsonBody: String? = null ): HttpURLConnection { if (!customBase.isNullOrBlank()) { return openSingle(customBase.trimEnd('/'), pathAndQuery, method, headers, jsonBody, 5000) } val now = System.currentTimeMillis() val cached = cachedBase val candidates = if (cached != null && (now - cachedAtMs) < 600_000L) { if (cached == PRIMARY_URL) listOf(PRIMARY_URL, MIRROR_URL) else listOf(MIRROR_URL, PRIMARY_URL) } else { listOf(PRIMARY_URL, MIRROR_URL) } var lastError: Exception? = null for ((idx, base) in candidates.withIndex()) { try { val timeoutMs = if (idx == 0) 2600 else 5000 val conn = openSingle(base, pathAndQuery, method, headers, jsonBody, timeoutMs) val code = conn.responseCode if (code in 200..499) { cachedBase = base cachedAtMs = System.currentTimeMillis() return conn } } catch (e: Exception) { lastError = e } } throw (lastError ?: RuntimeException("Ad-o-matic endpoints unreachable")) } fun activeBaseUrl(customBase: String?): String = customBase?.trimEnd('/') ?: cachedBase ?: PRIMARY_URL private fun openSingle( base: String, pathAndQuery: String, method: String, headers: Map, jsonBody: String?, connectTimeoutMs: Int ): HttpURLConnection { val conn = URL("$base$pathAndQuery").openConnection() as HttpURLConnection conn.requestMethod = method conn.connectTimeout = connectTimeoutMs conn.readTimeout = 5000 for ((k, v) in headers) { if (v.isNotEmpty()) conn.setRequestProperty(k, v) } if (jsonBody != null) { conn.setRequestProperty("Content-Type", "application/json") conn.doOutput = true conn.outputStream.use { it.write(jsonBody.toByteArray(Charsets.UTF_8)) } } return conn } } private object AdomaticBitmapCache { private val cache = object : LruCache(16) {} suspend fun load(urlOrDataUri: String): ImageBitmap? = withContext(Dispatchers.IO) { val clean = urlOrDataUri.trim() if (clean.isEmpty() || clean.startsWith("data:image/svg")) return@withContext null synchronized(cache) { cache.get(clean)?.let { return@withContext it.asImageBitmap() } } try { val bmp: Bitmap? = if (clean.startsWith("data:image/")) { val comma = clean.indexOf(',') if (comma > 0) { val bytes = Base64.decode(clean.substring(comma + 1), Base64.DEFAULT) BitmapFactory.decodeByteArray(bytes, 0, bytes.size) } else null } else if (clean.startsWith("https://") || clean.startsWith("http://")) { val conn = URL(clean).openConnection() as HttpURLConnection conn.connectTimeout = 4000 conn.readTimeout = 4000 if (conn.responseCode == 200) { conn.inputStream.use { BitmapFactory.decodeStream(it) } } else null } else null if (bmp != null) { synchronized(cache) { cache.put(clean, bmp) } bmp.asImageBitmap() } else null } catch (_: Exception) { null } } } class HouseAdManager( val context: Context, val apiKey: String = "auto", val customServerUrl: String? = null, val buildBannerText: String = "", val buildBannerImageUrl: String? = null, val targetAudience: String = "", val betaOptInUrl: String? = null, val developerId: String? = null ) { private val scope = CoroutineScope(Dispatchers.IO) private val _currentAd = MutableStateFlow(null) val currentAd: StateFlow = _currentAd.asStateFlow() private var currentNonce: String? = null private var activeImpressionNonce: String? = null private var activeReportNonce: String? = null @Volatile private var pendingPeerBetaAppId: String? = null @Volatile private var pendingPeerOpenedAtMs: Long = 0L private val hostPackageName: String = try { context.packageName ?: "" } catch (_: Exception) { "" } private val hostAppTitle: String = try { val label = context.packageManager.getApplicationLabel(context.applicationInfo)?.toString()?.trim() if (!label.isNullOrBlank()) label else hostPackageName } catch (_: Exception) { hostPackageName } private val isDebuggableBuild: Boolean = try { (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 } catch (_: Exception) { false } private val appVersionCode: Long = try { val pInfo = context.packageManager.getPackageInfo(hostPackageName, 0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { pInfo.longVersionCode } else { @Suppress("DEPRECATION") pInfo.versionCode.toLong() } } catch (_: Exception) { 1L } private val hostIconDataUri: String = try { val d = context.packageManager.getApplicationIcon(context.applicationInfo) val bmp = Bitmap.createBitmap(64, 64, Bitmap.Config.ARGB_8888) val canvas = Canvas(bmp) d.setBounds(0, 0, 64, 64) d.draw(canvas) val out = ByteArrayOutputStream() bmp.compress(Bitmap.CompressFormat.PNG, 90, out) "data:image/png;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP) } catch (_: Exception) { "" } private val appSigSha256: String = try { val pm = context.packageManager val bytes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { pm.getPackageInfo(hostPackageName, PackageManager.GET_SIGNING_CERTIFICATES) .signingInfo?.apkContentsSigners?.firstOrNull()?.toByteArray() } else { @Suppress("DEPRECATION") pm.getPackageInfo(hostPackageName, PackageManager.GET_SIGNATURES) .signatures?.firstOrNull()?.toByteArray() } if (bytes != null) { MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) } } else "" } catch (_: Exception) { "" } private fun commonHeaders(): Map = buildMap { put("X-App-Package", hostPackageName) put("X-App-Version-Code", appVersionCode.toString()) put("X-App-Debug-Build", if (isDebuggableBuild) "1" else "0") if (targetAudience.isNotBlank()) put("X-Target-Audience", targetAudience.trim()) if (appSigSha256.isNotEmpty()) put("X-App-Sig-Sha256", appSigSha256) if (!developerId.isNullOrBlank()) put("X-Developer-Id", developerId) } fun getBuildEmbeddedSelfAd(): HouseAd { val hasCopy = buildBannerText.isNotBlank() || !buildBannerImageUrl.isNullOrBlank() val baseTitle = hostAppTitle.ifBlank { "Mobile App" } val cleanDesc = if (hasCopy) { buildBannerText.ifBlank { "$baseTitle — official mobile application" } } else if (isDebuggableBuild) { "Add your own ad in the build: HouseAdBanner(bannerText = \"Your ad text\"). If left empty on store release, your official store logo and description are used automatically." } else { "Discover $baseTitle — fast, lightweight, and built for everyday mobile use." } return HouseAd( appId = hostPackageName.ifBlank { "com.adomatic.preview" }, title = if (isDebuggableBuild && !hasCopy) "$baseTitle — How to Add Your Own Ad" else baseTitle, description = cleanDesc, iconUrl = hostIconDataUri, storeUrl = "https://play.google.com/store/apps/details?id=$hostPackageName", customBannerUrl = buildBannerImageUrl, setupInstruction = if (isDebuggableBuild) { "Developer Debug Preview: Add ad-o-matic-beta-testers@googlegroups.com in Google Play Console Closed Testing, and beta-test 12 external peer apps (or run POST /api/v1/kindergarten/ai-fulfill via your AI agent)." } else null ) } init { _currentAd.value = getBuildEmbeddedSelfAd() loadNextAd() } fun activeGenesisUrl(): String = "${AdomaticEndpointResolver.activeBaseUrl(customServerUrl)}/#genesis-members" fun loadNextAd() { scope.launch { try { val dbgFlag = if (isDebuggableBuild) "1" else "0" val pathAndQuery = buildString { append("/api/v1/ad/request?apiKey=${URLEncoder.encode(apiKey, "UTF-8")}") append("&packageName=${URLEncoder.encode(hostPackageName, "UTF-8")}") append("&appTitle=${URLEncoder.encode(hostAppTitle, "UTF-8")}") append("&buildBannerText=${URLEncoder.encode(buildBannerText, "UTF-8")}") append("&buildBannerImage=${URLEncoder.encode(buildBannerImageUrl ?: "", "UTF-8")}") append("&platform=android") append("&kindergarten=1") append("&appVersionCode=$appVersionCode") append("&isDebugBuild=$dbgFlag") if (targetAudience.isNotBlank()) append("&targetAudience=${URLEncoder.encode(targetAudience.trim(), "UTF-8")}") if (!betaOptInUrl.isNullOrBlank()) append("&betaOptInUrl=${URLEncoder.encode(betaOptInUrl, "UTF-8")}") if (!developerId.isNullOrBlank()) append("&developerId=${URLEncoder.encode(developerId, "UTF-8")}") } val conn = AdomaticEndpointResolver.openWithFailover( customBase = customServerUrl, pathAndQuery = pathAndQuery, method = "GET", headers = commonHeaders() ) if (conn.responseCode == 200) { val json = JSONObject(conn.inputStream.bufferedReader().use { it.readText() }) val fallbackType = json.optString("fallbackType", "NONE") val admobId = json.optString("admobUnitId", "").takeIf { it.isNotBlank() && it != "null" } if (!json.isNull("ad") && json.has("ad")) { val adObj = json.getJSONObject("ad") val missing = mutableListOf() if (isDebuggableBuild) { val mArr = adObj.optJSONArray("missingPeers") if (mArr != null) { for (i in 0 until mArr.length()) { val item = mArr.optJSONObject(i) ?: continue val id = item.optString("appId", "").trim() val url = item.optString("url", "").trim() if (id.isNotEmpty() && url.isNotEmpty()) { missing.add(KindergartenPeerLink(id, item.optString("title", id), url)) } } } } val nonce = json.optString("nonce", "").takeIf { it.isNotBlank() && it != "null" } currentNonce = nonce activeImpressionNonce = nonce activeReportNonce = nonce val rawIcon = adObj.optString("iconUrl", "") _currentAd.value = HouseAd( campaignId = adObj.optString("campaignId", "").takeIf { it.isNotBlank() && it != "null" }, appId = adObj.optString("appId", hostPackageName), title = adObj.optString("title", hostAppTitle), description = adObj.optString("description", ""), iconUrl = if (rawIcon.isBlank() || rawIcon.startsWith("data:image/svg")) hostIconDataUri else rawIcon, storeUrl = adObj.optString("storeUrl", "https://play.google.com/store/apps/details?id=$hostPackageName"), customBannerUrl = adObj.optString("customBannerUrl", "").takeIf { it.isNotBlank() && it != "null" }, bannerSize = adObj.optString("bannerSize", "320x50"), bannerTheme = adObj.optString("bannerTheme", "modern_clean"), isGenesisMember = adObj.optBoolean("isGenesisMember", false), fallbackType = fallbackType, admobUnitId = admobId, setupInstruction = if (isDebuggableBuild) { adObj.optString("setupInstruction", "").takeIf { it.isNotBlank() && it != "null" } } else null, missingPeersList = missing ) } else if (json.optBoolean("capReached", false)) { _currentAd.value = HouseAd( appId = hostPackageName, title = "", description = "", iconUrl = "", storeUrl = "", fallbackType = fallbackType, admobUnitId = admobId, active = false ) } } } catch (_: Exception) { if (_currentAd.value == null) _currentAd.value = getBuildEmbeddedSelfAd() } } } fun verifyImpression(durationSec: Double, widthDp: Float, heightDp: Float, visibleRatio: Float) { val nonce = currentNonce ?: return currentNonce = null val bannerSize = _currentAd.value?.bannerSize ?: "320x50" scope.launch { try { val body = JSONObject().apply { put("apiKey", apiKey) put("nonce", nonce) put("viewDurationSeconds", durationSec) put("bannerSize", bannerSize) put("packageName", hostPackageName) put("renderedWidthDp", widthDp.toDouble()) put("renderedHeightDp", heightDp.toDouble()) put("visibleRatio", visibleRatio.toDouble()) }.toString() AdomaticEndpointResolver.openWithFailover( customBase = customServerUrl, pathAndQuery = "/api/v1/ad/verify", method = "POST", headers = commonHeaders(), jsonBody = body ).responseCode } catch (_: Exception) {} } } fun recordClick() { val nonce = activeImpressionNonce ?: return val ad = _currentAd.value ?: return scope.launch { try { val body = JSONObject().apply { put("campaignId", ad.campaignId ?: ad.appId) put("targetPeerAppId", ad.appId) put("publisherApiKey", apiKey) put("nonce", nonce) put("packageName", hostPackageName) }.toString() AdomaticEndpointResolver.openWithFailover( customBase = customServerUrl, pathAndQuery = "/api/v1/ad/click", method = "POST", headers = commonHeaders(), jsonBody = body ).responseCode loadNextAd() } catch (_: Exception) {} } } fun recordPeerClick(peerAppId: String) { pendingPeerBetaAppId = peerAppId pendingPeerOpenedAtMs = System.currentTimeMillis() val nonce = activeImpressionNonce scope.launch { try { if (!nonce.isNullOrBlank()) { val clickBody = JSONObject().apply { put("appId", peerAppId) put("targetPeerAppId", peerAppId) put("publisherApiKey", apiKey) put("nonce", nonce) put("packageName", hostPackageName) }.toString() AdomaticEndpointResolver.openWithFailover( customBase = customServerUrl, pathAndQuery = "/api/v1/ad/click", method = "POST", headers = commonHeaders(), jsonBody = clickBody ).responseCode } } catch (_: Exception) {} } } fun onHostAppResumed() { val peerId = pendingPeerBetaAppId ?: return val elapsedMs = System.currentTimeMillis() - pendingPeerOpenedAtMs pendingPeerBetaAppId = null // Require >= 5 seconds spent in Google Play opt-in / install screen before attesting resume if (elapsedMs < 5000L) return scope.launch { try { val body = JSONObject().apply { put("downloaderAppId", hostPackageName) put("targetPeerAppId", peerId) put("verifiedVia", "PLAY_STORE_RESUME_ATTESTED") }.toString() AdomaticEndpointResolver.openWithFailover( customBase = customServerUrl, pathAndQuery = "/api/v1/kindergarten/verify-download", method = "POST", headers = commonHeaders(), jsonBody = body ).responseCode loadNextAd() } catch (_: Exception) {} } } fun reportAd(adId: String) { val nonce = activeReportNonce ?: return activeReportNonce = null scope.launch { try { val body = JSONObject().apply { put("ad_id", adId) put("nonce", nonce) put("reason", "inappropriate") }.toString() AdomaticEndpointResolver.openWithFailover( customBase = customServerUrl, pathAndQuery = "/api/v1/ad/report", method = "POST", headers = commonHeaders(), jsonBody = body ).responseCode loadNextAd() } catch (_: Exception) {} } } } @Composable fun HouseAdBanner( modifier: Modifier = Modifier, bannerText: String = "", bannerImageUrl: String? = null, targetAudience: String = "", betaOptInUrl: String? = null, developerId: String? = null, apiKey: String = "auto" ) { val context = LocalContext.current val appContext = context.applicationContext val density = LocalDensity.current val lifecycleOwner = LocalLifecycleOwner.current val rootView = LocalView.current val manager = remember(apiKey, bannerText, bannerImageUrl, targetAudience, betaOptInUrl, developerId) { HouseAdManager( context = appContext, apiKey = apiKey, buildBannerText = bannerText, buildBannerImageUrl = bannerImageUrl, targetAudience = targetAudience, betaOptInUrl = betaOptInUrl, developerId = developerId ) } DisposableEffect(lifecycleOwner, manager) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { manager.onHostAppResumed() } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } val currentAd by manager.currentAd.collectAsState() val ad = currentAd ?: manager.getBuildEmbeddedSelfAd() if (ad.fallbackType == "CLEAN_UI_HIDE" || !ad.active) { LaunchedEffect(ad.fallbackType) { delay(60000L) manager.loadNextAd() } return } var isVisibleOnScreen by remember { mutableStateOf(false) } var renderedW by remember { mutableStateOf(320f) } var renderedH by remember { mutableStateOf(56f) } var renderedRatio by remember { mutableStateOf(1f) } var showReport by remember { mutableStateOf(false) } var iconBitmap by remember(ad.iconUrl) { mutableStateOf(null) } var customBannerBitmap by remember(ad.customBannerUrl) { mutableStateOf(null) } LaunchedEffect(ad.iconUrl) { iconBitmap = AdomaticBitmapCache.load(ad.iconUrl) } LaunchedEffect(ad.customBannerUrl) { customBannerBitmap = ad.customBannerUrl?.let { AdomaticBitmapCache.load(it) } } LaunchedEffect(ad.campaignId, ad.appId, ad.title) { var visibleMs = 0L var totalMs = 0L var verified = false while (totalMs < 45000L) { delay(250L) totalMs += 250L val resumed = lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) val shown = rootView.isShown && rootView.windowVisibility == android.view.View.VISIBLE if (!verified) { if (isVisibleOnScreen && resumed && shown) { visibleMs += 250L if (visibleMs >= 2000L) { verified = true manager.verifyImpression(visibleMs / 1000.0, renderedW, renderedH, renderedRatio) } } else { visibleMs = 0L } } } manager.loadNextAd() } if (showReport) { AlertDialog( onDismissRequest = { showReport = false }, title = { Text("Report Ad", fontSize = 15.sp, fontWeight = FontWeight.Bold) }, text = { Text("Report this ad for inappropriate content or a broken link?") }, confirmButton = { Button(onClick = { manager.reportAd(ad.campaignId ?: ad.appId) showReport = false }) { Text("Report") } }, dismissButton = { TextButton(onClick = { showReport = false }) { Text("Cancel") } } ) } Card( modifier = modifier .fillMaxWidth() .onGloballyPositioned { coords -> if (!coords.isAttached) { isVisibleOnScreen = false return@onGloballyPositioned } val wPx = coords.size.width.toFloat() val hPx = coords.size.height.toFloat() val minW = with(density) { 220.dp.toPx() } val minH = with(density) { 40.dp.toPx() } if (wPx < minW || hPx < minH) { isVisibleOnScreen = false return@onGloballyPositioned } val b = coords.boundsInWindow() val visW = (b.right - b.left).coerceAtLeast(0f) val visH = (b.bottom - b.top).coerceAtLeast(0f) val ratio = if (wPx * hPx > 0f) (visW * visH) / (wPx * hPx) else 0f renderedW = with(density) { wPx.toDp().value } renderedH = with(density) { hPx.toDp().value } renderedRatio = ratio isVisibleOnScreen = ratio >= 0.50f } .clip(RoundedCornerShape(10.dp)) .clickable { manager.recordClick() try { val uri = Uri.parse(ad.storeUrl) if (uri.scheme == "https" || uri.scheme == "http") { context.startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } } catch (_: Exception) {} }, elevation = CardDefaults.cardElevation(defaultElevation = 3.dp) ) { Column( modifier = Modifier .fillMaxWidth() .border(1.dp, Color(0xFFCBD5E1), RoundedCornerShape(10.dp)) .background(Color(0xFFF8FAFC)) .padding(4.dp) ) { Row( modifier = Modifier .fillMaxWidth() .background(Color(0xFF0F172A)) .padding(horizontal = 6.dp, vertical = 3.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = ad.title, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) Text( text = "Report (i)", fontSize = 9.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFFE2E8F0), modifier = Modifier .padding(start = 6.dp) .clickable { showReport = true } ) } Crossfade(targetState = ad, label = "AdomaticCrossfade") { target -> if (customBannerBitmap != null) { Image( bitmap = customBannerBitmap!!, contentDescription = target.title, modifier = Modifier .fillMaxWidth() .heightIn(min = 50.dp, max = 160.dp), contentScale = ContentScale.FillWidth ) } else { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier .fillMaxWidth() .background(Color.White) .padding(horizontal = 10.dp, vertical = 8.dp) ) { if (iconBitmap != null) { Image( bitmap = iconBitmap!!, contentDescription = target.title, modifier = Modifier .size(40.dp) .clip(RoundedCornerShape(8.dp)), contentScale = ContentScale.Crop ) } Column(modifier = Modifier.weight(1f)) { Text( text = target.description, fontSize = 11.5.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF0F172A), maxLines = 4, overflow = TextOverflow.Ellipsis, lineHeight = 15.sp ) if (!target.setupInstruction.isNullOrBlank()) { Spacer(modifier = Modifier.height(4.dp)) Text( text = target.setupInstruction, fontSize = 10.sp, color = Color(0xFF1E3A8A), maxLines = 4, overflow = TextOverflow.Ellipsis, lineHeight = 13.sp ) } if (target.missingPeersList.isNotEmpty()) { Spacer(modifier = Modifier.height(6.dp)) Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { target.missingPeersList.forEach { peer -> Surface( color = Color(0xFFFFF7ED), shape = RoundedCornerShape(4.dp), modifier = Modifier .fillMaxWidth() .clickable { manager.recordPeerClick(peer.appId) try { context.startActivity( Intent(Intent.ACTION_VIEW, Uri.parse(peer.url)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ) } catch (_: Exception) {} } ) { Text( text = "Download: ${peer.title} (${peer.appId}) ->", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color(0xFF9A3412), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(horizontal = 6.dp, vertical = 3.dp) ) } } } } } if (target.setupInstruction.isNullOrBlank() && target.missingPeersList.isEmpty()) { Surface( color = Color(0xFF0F172A), shape = RoundedCornerShape(6.dp) ) { Text( text = "Install", fontSize = 10.5.sp, fontWeight = FontWeight.Bold, color = Color.White, modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp) ) } } } } } Row( modifier = Modifier .fillMaxWidth() .background(Color(0xFFF1F5F9)) .padding(horizontal = 6.dp, vertical = 2.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Powered by Ad-o-matic", fontSize = 9.sp, fontWeight = FontWeight.Bold, color = Color(0xFF334155) ) if (ad.isGenesisMember) { Text( text = "Genesis Member", fontSize = 8.sp, fontFamily = FontFamily.Serif, fontStyle = FontStyle.Italic, color = Color(0xFF0F172A), modifier = Modifier.clickable { try { context.startActivity( Intent(Intent.ACTION_VIEW, Uri.parse(manager.activeGenesisUrl())) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ) } catch (_: Exception) {} } ) } } } } }