package com.freeexile.arpg import android.annotation.SuppressLint import android.graphics.Color import android.os.Build import android.os.Bundle import android.view.View import android.view.WindowInsets import android.view.WindowInsetsController import android.view.WindowManager import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.Purchase import org.json.JSONArray import org.json.JSONObject /** * FreeExile ARPG Android Main Activity. * Provides hardware-accelerated WebGL / Canvas rendering container in Immersive Sticky Fullscreen. */ class MainActivity : AppCompatActivity(), PlayBillingManager.BillingEventListener { private lateinit var webView: WebView private lateinit var hapticManager: HapticFeedbackManager private lateinit var billingManager: PlayBillingManager private lateinit var integrityManager: PlayIntegrityManager private lateinit var bridgeInterface: NativeBridgeInterface override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Ensure display cutout extends to the edges if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } enableImmersiveStickyMode() hapticManager = HapticFeedbackManager(this) billingManager = PlayBillingManager(this, lifecycleScope, this) integrityManager = PlayIntegrityManager(this) setupWebView() setContentView(webView) loadGameClient() } private fun enableImmersiveStickyMode() { WindowCompat.setDecorFitsSystemWindows(window, false) val controller = WindowInsetsControllerCompat(window, window.decorView) controller.hide(WindowInsetsCompat.Type.systemBars()) controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE window.statusBarColor = Color.BLACK window.navigationBarColor = Color.BLACK } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { enableImmersiveStickyMode() } } @SuppressLint("SetJavaScriptEnabled") private fun setupWebView() { webView = WebView(this).apply { setBackgroundColor(Color.BLACK) // Hardware acceleration enforcement on view layer setLayerType(View.LAYER_TYPE_HARDWARE, null) isVerticalScrollBarEnabled = false isHorizontalScrollBarEnabled = false } val settings: WebSettings = webView.settings settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.databaseEnabled = true settings.mediaPlaybackRequiresUserGesture = false settings.allowFileAccess = true settings.allowContentAccess = true settings.cacheMode = WebSettings.LOAD_DEFAULT bridgeInterface = NativeBridgeInterface( activity = this, webView = webView, hapticManager = hapticManager, billingManager = billingManager, integrityManager = integrityManager, scope = lifecycleScope ) webView.addJavascriptInterface(bridgeInterface, "NativeBridge") webView.webViewClient = object : WebViewClient() { override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { super.onReceivedError(view, request, error) } } webView.webChromeClient = WebChromeClient() } private fun loadGameClient() { // Load local asset index.html or fallback to local development server val localAssetUrl = "file:///android_asset/webapp/index.html" val devServerUrl = "http://10.0.2.2:8080" // Android emulator host alias try { assets.open("webapp/index.html").use { webView.loadUrl(localAssetUrl) } } catch (_: Exception) { // Local assets not bundled in debug/test run, fallback to emulator host dev server webView.loadUrl(devServerUrl) } } // MARK: - PlayBillingManager.BillingEventListener override fun onPurchaseSuccess(purchase: Purchase) { val payload = JSONObject().apply { put("event", "purchaseSuccess") put("orderId", purchase.orderId) put("purchaseToken", purchase.purchaseToken) put("purchaseTime", purchase.purchaseTime) put("products", JSONArray(purchase.products)) } bridgeInterface.dispatchToWeb("onBillingResponse", payload.toString()) } override fun onPurchaseFailed(errorCode: Int, message: String) { val payload = JSONObject().apply { put("event", "purchaseFailed") put("errorCode", errorCode) put("message", message) } bridgeInterface.dispatchToWeb("onBillingResponse", payload.toString()) } override fun onProductDetailsReady(products: List) { val array = JSONArray() for (p in products) { val item = JSONObject().apply { put("productId", p.productId) put("title", p.title) put("name", p.name) put("description", p.description) p.oneTimePurchaseOfferDetails?.let { offer -> put("price", offer.formattedPrice) put("priceAmountMicros", offer.priceAmountMicros) put("priceCurrencyCode", offer.priceCurrencyCode) } } array.put(item) } val payload = JSONObject().apply { put("event", "productDetailsReady") put("products", array) } bridgeInterface.dispatchToWeb("onBillingResponse", payload.toString()) } override fun onDestroy() { super.onDestroy() billingManager.destroy() webView.removeJavascriptInterface("NativeBridge") webView.destroy() } }