Native splash held while connecting; offline shows the same picture with a connecting line; web splash hidden inside the app

Inside the app the web splash is invisible with the new APK (the system splash is still on
top) and icon-only for old APKs, so there is no second picture to switch to. Offline or a
5xx on the main frame: system splash released onto the same icon (288dp drawable) with
'در حال اتصال به اینترنت…', retried every 5s. A slow-but-online load shows nothing extra.
Debug build compiles; not run on a device.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 22:48:06 +03:30
parent 446cf36103
commit ab02bb43b9
2 changed files with 94 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ package com.tadrisino.app
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -11,7 +12,14 @@ import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
import android.util.Log import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebResourceResponse
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
@@ -46,11 +54,60 @@ class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView private lateinit var webView: WebView
/** /**
* اسپلشِ سیستم تا وقتی true نشده روی صفحه می‌ماند. فرانت وقتی اولین صفحه‌ی واقعی (ورود/خانه) کشیده شد * اسپلشِ سیستم تا وقتی true نشده روی صفحه می‌ماند، هرچقدر اتصال کند باشد (خواستِ مالک): فرانت وقتی
* `TadrisinoAndroid.appReady()` را صدا می‌زند. اگر هرگز نرسید (سایت بالا نیامد، JS خطا داد) بعد از * اولین صفحه‌ی واقعی (ورود/خانه) کشیده شد `TadrisinoAndroid.appReady()` را صدا می‌زند.
* SPLASH_MAX_MS رها می‌شود تا کاربر به‌جای اسپلشِ بی‌انتها صفحه‌ی خطای WebView را ببیند، نه چیزِ دیگر. * فقط دو حالت زودتر رهایش می‌کند: صفحه‌ی اصلی بالا نیامد (آفلاین/خطای سرور → [showOffline]، با همان
* تصویر و یک جمله)، یا صفحه لود شد ولی تا [READY_WATCHDOG_MS] appReady نیامد (خطای JS؛ لاگ می‌شود).
*/ */
@Volatile private var webReady = false @Volatile private var webReady = false
private val handler = Handler(Looper.getMainLooper())
private lateinit var offlineView: View
private var offlineShown = false
private val retryLoad = object : Runnable {
override fun run() {
if (!offlineShown) return
webView.loadUrl(BuildConfig.WEB_URL)
handler.postDelayed(this, RETRY_MS)
}
}
private fun dp(v: Int): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, v.toFloat(), resources.displayMetrics).toInt()
/** همان تصویرِ اسپلشِ سیستم (۲۸۸dp، همان drawable) با یک جمله زیرش؛ جای آیکون با اسپلشِ سیستم یکی است. */
private fun buildOfflineView(): View = FrameLayout(this).apply {
setBackgroundColor(Color.WHITE)
visibility = View.GONE
isClickable = true
addView(ImageView(context).apply { setImageResource(R.drawable.splash_icon) },
FrameLayout.LayoutParams(dp(288), dp(288), Gravity.CENTER))
addView(TextView(context).apply {
text = "در حال اتصال به اینترنت…"
setTextColor(Color.parseColor("#1a1a2e"))
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
layoutDirection = View.LAYOUT_DIRECTION_RTL
gravity = Gravity.CENTER
}, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)
.apply { topMargin = dp(150) })
}
private fun showOffline() {
if (!offlineShown) {
offlineShown = true
webView.visibility = View.INVISIBLE // صفحه‌ی خطای پیش‌فرضِ WebView دیده نشود
offlineView.visibility = View.VISIBLE
handler.postDelayed(retryLoad, RETRY_MS)
}
webReady = true
}
private fun hideOffline() {
if (!offlineShown) return
offlineShown = false
handler.removeCallbacks(retryLoad)
offlineView.visibility = View.GONE
webView.visibility = View.VISIBLE
}
private var payment: Payment? = null private var payment: Payment? = null
private var connection: Connection? = null private var connection: Connection? = null
private val webHost: String = Uri.parse(BuildConfig.WEB_URL).host ?: "" private val webHost: String = Uri.parse(BuildConfig.WEB_URL).host ?: ""
@@ -70,13 +127,13 @@ class MainActivity : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().setKeepOnScreenCondition { !webReady } installSplashScreen().setKeepOnScreenCondition { !webReady }
Handler(Looper.getMainLooper()).postDelayed({
if (!webReady) Log.w(TAG, "web app never reported ready; releasing the splash after ${SPLASH_MAX_MS}ms")
webReady = true
}, SPLASH_MAX_MS)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
webView = WebView(this) webView = WebView(this)
setContentView(webView) offlineView = buildOfflineView()
setContentView(FrameLayout(this).apply {
addView(webView)
addView(offlineView)
})
webView.settings.javaScriptEnabled = true webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true // توکنِ ورود در localStorage است webView.settings.domStorageEnabled = true // توکنِ ورود در localStorage است
@@ -86,9 +143,21 @@ class MainActivity : AppCompatActivity() {
// صداهای کوتاهِ رابط (Web Audio) بعد از پاسخِ سرور پخش می‌شوند، نه درست در لحظه‌ی لمس. // صداهای کوتاهِ رابط (Web Audio) بعد از پاسخِ سرور پخش می‌شوند، نه درست در لحظه‌ی لمس.
webView.settings.mediaPlaybackRequiresUserGesture = false webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webViewClient = object : WebViewClient() { webView.webViewClient = object : WebViewClient() {
// صفحه‌ی اصلی بالا نیامد (نبودِ شبکه): اسپلش را رها کن تا خطای WebView دیده شود، نه ۱۰ ثانیه انتظار. // صفحه‌ی اصلی بالا نیامد (بی‌اینترنت یا خطای سرور): همان تصویر با «در حال اتصال»، و هر چند ثانیه دوباره.
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: android.webkit.WebResourceError) { override fun onReceivedError(view: WebView, request: WebResourceRequest, error: android.webkit.WebResourceError) {
if (request.isForMainFrame) webReady = true if (request.isForMainFrame) showOffline()
}
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
if (request.isForMainFrame && errorResponse.statusCode >= 500) showOffline()
}
override fun onPageFinished(view: WebView, url: String?) {
if (offlineShown) return // صفحه‌ی خطا هم onPageFinished می‌زند؛ تا بارِ موفق همان تصویر بماند
handler.postDelayed({
if (!webReady) Log.e(TAG, "page loaded but appReady never came in ${READY_WATCHDOG_MS}ms; releasing the splash")
webReady = true
}, READY_WATCHDOG_MS)
} }
// هر آدرسی بیرونِ دامنه‌ی خودمان در مرورگرِ گوشی باز می‌شود، نه داخلِ اپ؛ پس پل فقط // هر آدرسی بیرونِ دامنه‌ی خودمان در مرورگرِ گوشی باز می‌شود، نه داخلِ اپ؛ پس پل فقط
@@ -229,6 +298,7 @@ class MainActivity : AppCompatActivity() {
@JavascriptInterface @JavascriptInterface
fun appReady() { fun appReady() {
webReady = true webReady = true
runOnUiThread { hideOffline() }
} }
/** فرانت با این می‌فهمد درگاه داخلِ اپ باز می‌شود (isPaymentPage)، نه در مرورگرِ بیرونی. */ /** فرانت با این می‌فهمد درگاه داخلِ اپ باز می‌شود (isPaymentPage)، نه در مرورگرِ بیرونی. */
@@ -251,6 +321,7 @@ class MainActivity : AppCompatActivity() {
companion object { companion object {
private const val TAG = "Tadrisino" private const val TAG = "Tadrisino"
private const val SPLASH_MAX_MS = 10_000L private const val RETRY_MS = 5_000L
private const val READY_WATCHDOG_MS = 15_000L
} }
} }

View File

@@ -14,10 +14,22 @@
Vazirmatn بعد از CSS می‌رسد و عوض‌شدنِ فونت خودش یک پرش است. --> Vazirmatn بعد از CSS می‌رسد و عوض‌شدنِ فونت خودش یک پرش است. -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preload" as="image" href="/app-icon.png" /> <link rel="preload" as="image" href="/app-icon.png" />
<script>
(function () {
var b = window.TadrisinoAndroid;
if (b) document.documentElement.className = typeof b.appReady === 'function' ? 'in-app' : 'in-old-app';
})();
</script>
<style> <style>
.boot-splash { position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column; .boot-splash { position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 20px; background: #fff; } align-items: center; justify-content: center; gap: 20px; background: #fff; }
.boot-splash img { width: 144px; height: 144px; display: block; } .boot-splash img { width: 144px; height: 144px; display: block; }
/* داخلِ اپِ اندروید اسپلشِ بومی روی صفحه است و سایت زیرش بار می‌شود؛ اسپلشِ وب نباید یک بار دیگر
«عوض شدن» نشان دهد. APKِ تازه (appReady دارد): اسپلشِ وب نامرئی است. APKِ قدیمی: فقط آیکون،
بی‌نام، تا کمتر از اسپلشِ سیستم فرق کند. */
.in-app .boot-splash > * { visibility: hidden; }
.in-old-app .boot-splash span { display: none; }
.in-old-app .boot-splash img { width: 160px; height: 160px; }
.boot-splash span { font: 900 34px/1.2 system-ui, -apple-system, sans-serif; color: #1a1a2e; } .boot-splash span { font: 900 34px/1.2 system-ui, -apple-system, sans-serif; color: #1a1a2e; }
</style> </style>
</head> </head>