@@ -3,25 +3,19 @@ package com.book.gofarsi
33import android.content.Context
44import android.content.Intent
55import android.graphics.Bitmap
6- import android.graphics.Color
76import android.net.Uri
87import android.os.Build
98import android.os.Bundle
109import android.util.Log
1110import android.view.View
12- import android.view.WindowManager
1311import android.webkit.*
1412import android.widget.RelativeLayout
1513import android.widget.TextView
1614import androidx.activity.OnBackPressedCallback
1715import androidx.appcompat.app.AppCompatActivity
18- import androidx.core.view.WindowCompat
19- import androidx.core.view.WindowInsetsCompat
20- import androidx.core.view.WindowInsetsControllerCompat
2116import kotlinx.coroutines.*
2217import java.util.concurrent.ConcurrentHashMap
2318import kotlin.system.measureTimeMillis
24- import java.util.concurrent.atomic.AtomicBoolean
2519
2620
2721// import org.jsoup.Jsoup
@@ -39,12 +33,55 @@ class WebActivity : AppCompatActivity() {
3933 var urlIndex = 0
4034 var hasErrorInLoading = false
4135 var currentUrl = " "
36+
37+ companion object {
38+ private const val TAG = " WebActivity" // Better log tag
39+ val KEY_TITLE = " title"
40+ val KEY_LINK = " link"
41+
42+ // Cache for storing response times and last check time
43+ private val responseTimeCache = ConcurrentHashMap <String , Pair <Long , Long >>() // URL -> (responseTime, timestamp)
44+ private const val CACHE_VALIDITY_MS = 5 * 60 * 1000L // 5 minutes
45+ private const val MAX_TIMEOUT_MS = 2000L
46+ private const val MIN_TIMEOUT_MS = 500L
47+
48+ fun navigate (context : Context , title : String , link : String ) {
49+ val intent = Intent (context, WebActivity ::class .java).apply {
50+ putExtra(KEY_TITLE , title)
51+ putExtra(KEY_LINK , link)
52+ }
53+ context.startActivity(intent)
54+ }
55+
56+ fun loadWebView (content : String , myWebView : WebView ) {
57+ setSettingWebView(myWebView)
58+ myWebView.loadUrl(content)
59+ }
60+
61+ private fun setSettingWebView (myWebView : WebView ) {
62+ val settings = myWebView.settings
63+ settings.builtInZoomControls = false
64+ settings.domStorageEnabled = true
65+ settings.setGeolocationEnabled(true )
66+ settings.loadWithOverviewMode = true
67+ settings.useWideViewPort = true
68+ myWebView.isScrollbarFadingEnabled = false
69+ myWebView.clearCache(false )
70+ settings.allowFileAccess = true
71+ settings.javaScriptEnabled = true
72+ settings.cacheMode = WebSettings .LOAD_DEFAULT
73+ settings.defaultTextEncodingName = " utf-8"
74+ settings.javaScriptCanOpenWindowsAutomatically = true
75+ settings.setSupportMultipleWindows(true )
76+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
77+ CookieManager .getInstance().setAcceptThirdPartyCookies(myWebView, true )
78+ }
79+ }
80+ }
81+
4282 override fun onCreate (savedInstanceState : Bundle ? ) {
4383 super .onCreate(savedInstanceState)
4484 try {
45- // Configure status bar and navigation bar
46- configureStatusBar()
47-
4885 setContentView(R .layout.activity_web)
4986
5087 // Find the fastest URL before loading
@@ -60,14 +97,6 @@ class WebActivity : AppCompatActivity() {
6097 // Note: allowFileAccessFromFileURLs and allowUniversalAccessFromFileURLs are deprecated
6198 // and removed for security reasons. Modern apps should use alternative approaches.
6299 webSettings.mixedContentMode = WebSettings .MIXED_CONTENT_ALWAYS_ALLOW
63-
64- // Prevent WebView from going fullscreen
65- webSettings.loadWithOverviewMode = true
66- webSettings.useWideViewPort = true
67- webSettings.setSupportZoom(true ) // ✅ ENABLE zoom functionality
68- webSettings.builtInZoomControls = true // ✅ ENABLE zoom controls
69- webSettings.displayZoomControls = false // ❌ HIDE zoom buttons (users can still pinch-to-zoom)
70-
71100 webSettings.cacheMode = if (isNetworkAvailable()) {
72101 WebSettings .LOAD_DEFAULT
73102 } else {
@@ -92,7 +121,7 @@ class WebActivity : AppCompatActivity() {
92121
93122 myWebView?.webViewClient = object : WebViewClient () {
94123 override fun shouldOverrideUrlLoading (view : WebView , url : String ): Boolean {
95- Log .d(" bootlegger " , url)
124+ Log .d(TAG , " shouldOverrideUrlLoading: $ url" )
96125 if (! url.contains(" gofarsi.ir" )) {
97126 loadBrowser(url)
98127 return true
@@ -102,12 +131,12 @@ class WebActivity : AppCompatActivity() {
102131
103132 override fun onPageStarted (view : WebView ? , url : String? , favicon : Bitmap ? ) {
104133 super .onPageStarted(view, url, favicon)
105- Log .d(" bootlegger " , " onPageStarted: $url " )
134+ Log .d(TAG , " onPageStarted: $url " )
106135 hasErrorInLoading = false
107136 }
108137
109138 override fun onReceivedError (view : WebView ? , request : WebResourceRequest ? , error : WebResourceError ? ) {
110- Log .d(" bootlegger " , " onReceivedError" )
139+ Log .d(TAG , " onReceivedError: ${error?.description} " )
111140 super .onReceivedError(view, request, error)
112141 hasErrorInLoading = true
113142 if (urlIndex == list.size - 1 ) return
@@ -118,7 +147,7 @@ class WebActivity : AppCompatActivity() {
118147 super .onPageFinished(view, url)
119148 currentUrl = url ? : " "
120149 if (! hasErrorInLoading) findViewById<RelativeLayout >(R .id.loading).visibility = View .INVISIBLE
121- Log .d(" bootlegger " , " onPageFinished: $url " )
150+ Log .d(TAG , " onPageFinished: $url " )
122151 }
123152
124153 }
@@ -136,65 +165,19 @@ class WebActivity : AppCompatActivity() {
136165 onBackPressedDispatcher.addCallback(this , callback)
137166 }
138167
139- private fun configureStatusBar () {
140- // Make sure status bar is visible and properly configured
141- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
142- // Set status bar color
143- window.statusBarColor = Color .parseColor(" #2196F3" ) // Blue color, change as needed
144-
145- // For Android 6.0+ (API 23), we can control light/dark status bar content
146- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
147- // Use WindowInsetsController for better control (API 30+)
148- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
149- // Use AndroidX WindowCompat instead of deprecated setDecorFitsSystemWindows
150- WindowCompat .setDecorFitsSystemWindows(window, false )
151- val controller = window.insetsController
152- controller?.show(WindowInsetsCompat .Type .statusBars())
153- // Use light content (white icons) on dark status bar
154- controller?.setSystemBarsAppearance(0 , android.view.WindowInsetsController .APPEARANCE_LIGHT_STATUS_BARS )
155- } else {
156- // For older versions, use deprecated flags
157- @Suppress(" DEPRECATION" )
158- window.decorView.systemUiVisibility = View .SYSTEM_UI_FLAG_LAYOUT_STABLE
159- }
160- }
161- }
162-
163- // Ensure the activity doesn't go full screen
164- @Suppress(" DEPRECATION" )
165- window.clearFlags(WindowManager .LayoutParams .FLAG_FULLSCREEN )
166-
167- // Show the status bar if it was hidden
168- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
169- window.insetsController?.show(WindowInsetsCompat .Type .statusBars())
170- } else {
171- @Suppress(" DEPRECATION" )
172- window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and
173- View .SYSTEM_UI_FLAG_FULLSCREEN .inv ()
174- }
175- }
176-
177168 private fun findFastestUrlAndLoad () {
178- // Start loading the first URL immediately while testing others in background
179- CoroutineScope (Dispatchers .Main ).launch {
180- // Load first URL immediately to show something to user
181- myWebView?.loadUrl(list[0 ])
182- urlIndex = 0
183- }
184-
185- // Test URLs in background and switch if we find a faster one
186169 CoroutineScope (Dispatchers .IO ).launch {
187170 val fastestUrl = findFastestUrlOptimized()
188171
189172 withContext(Dispatchers .Main ) {
190- if (fastestUrl != null && fastestUrl != list[ 0 ] ) {
191- // Only switch if we found a different, faster URL
192- val newIndex = list.indexOf (fastestUrl)
193- if (newIndex != urlIndex) {
194- urlIndex = newIndex
195- myWebView?.loadUrl(fastestUrl)
196- Log .d( " WebActivity " , " Switched to faster URL: $fastestUrl " )
197- }
173+ if (fastestUrl != null ) {
174+ urlIndex = list.indexOf(fastestUrl)
175+ myWebView?.loadUrl (fastestUrl)
176+ } else {
177+ // Fallback to first URL if none are responsive
178+ urlIndex = 0
179+ myWebView?.loadUrl(list[ 0 ] )
180+ findViewById< RelativeLayout >( R .id.loading).visibility = View . VISIBLE
198181 }
199182 }
200183 }
@@ -219,80 +202,58 @@ class WebActivity : AppCompatActivity() {
219202 return cachedResults.minByOrNull { it.second }?.first
220203 }
221204
222- // Use a race condition approach - first successful response wins
205+ // Otherwise, test URLs concurrently with race condition
223206 return withContext(Dispatchers .IO ) {
224- val firstSuccessful = AtomicBoolean (false )
225- var fastestUrl: String? = null
226- var fastestTime = Long .MAX_VALUE
227-
228- // Launch all requests concurrently
229- val jobs = list.mapIndexed { index, url ->
207+ val jobs = list.map { url ->
230208 async {
231- try {
232- // Use shorter timeout for first quick scan
233- val timeout = if (firstSuccessful.get()) MAX_TIMEOUT_MS else FAST_LOAD_TIMEOUT_MS
234- val responseTime = measureUrlResponseTime(url, timeout)
235-
236- if (responseTime != null && responseTime < fastestTime) {
237- synchronized(this @WebActivity) {
238- if (responseTime < fastestTime) {
239- fastestTime = responseTime
240- fastestUrl = url
241-
242- // Cache the result
243- responseTimeCache[url] = responseTime to currentTime
244-
245- // If we find a very fast response, we can use it immediately
246- if (responseTime < MIN_TIMEOUT_MS ) {
247- firstSuccessful.set(true )
248- }
249- }
250- }
251- }
252- responseTime
253- } catch (e: Exception ) {
254- Log .d(" WebActivity" , " URL $url failed: ${e.message} " )
209+ val responseTime = measureUrlResponseTime(url)
210+ if (responseTime != null ) {
211+ // Cache the result
212+ responseTimeCache[url] = responseTime to currentTime
213+ url to responseTime
214+ } else {
255215 null
256216 }
257217 }
258218 }
259219
260- // Wait for either the first very fast response or all jobs to complete
220+ // Wait for first successful response (race condition)
221+ var fastestResult: Pair <String , Long >? = null
222+
261223 try {
262- // Wait a short time for a quick response
263- withTimeout(FAST_LOAD_TIMEOUT_MS + 200 ) {
264- for (job in jobs) {
265- if (firstSuccessful.get()) break
266- try {
267- job.await()
268- } catch (e: Exception ) {
269- // Continue with other jobs
224+ // Use select to get the first successful result
225+ for (job in jobs) {
226+ val result = job.await()
227+ if (result != null ) {
228+ if (fastestResult == null || result.second < fastestResult.second) {
229+ fastestResult = result
230+ }
231+ // If we find a very fast response (< 500ms), use it immediately
232+ if (result.second < MIN_TIMEOUT_MS ) {
233+ break
270234 }
271235 }
272236 }
273- } catch (e: TimeoutCancellationException ) {
274- // If we timeout, use whatever we have so far
275- Log .d(" WebActivity" , " Quick scan timeout, using best result so far" )
237+ } catch (e: Exception ) {
238+ Log .e(" WebActivity" , " Error in concurrent URL testing: ${e.message} " )
276239 }
277240
278- fastestUrl
241+ fastestResult?.first
279242 }
280243 }
281244
282- private suspend fun measureUrlResponseTime (url : String , timeoutMs : Long = MAX_TIMEOUT_MS ): Long? {
245+ private suspend fun measureUrlResponseTime (url : String ): Long? {
283246 return withContext(Dispatchers .IO ) {
284247 try {
285248 val responseTime = measureTimeMillis {
286249 val connection = java.net.URL (url).openConnection() as java.net.HttpURLConnection
287250 connection.apply {
288251 requestMethod = " HEAD"
289- connectTimeout = timeoutMs .toInt()
290- readTimeout = timeoutMs .toInt()
252+ connectTimeout = MAX_TIMEOUT_MS .toInt()
253+ readTimeout = MAX_TIMEOUT_MS .toInt()
291254 setRequestProperty(" User-Agent" , " Android-Book-App" )
292255 // Add cache control to get fresh response
293256 setRequestProperty(" Cache-Control" , " no-cache" )
294- // Add connection keep-alive for faster subsequent requests
295- setRequestProperty(" Connection" , " keep-alive" )
296257 }
297258
298259 val responseCode = connection.responseCode
@@ -334,52 +295,6 @@ class WebActivity : AppCompatActivity() {
334295 }*/
335296
336297
337- companion object {
338- val KEY_TITLE = " title"
339- val KEY_LINK = " link"
340-
341- // Cache for storing response times and last check time
342- private val responseTimeCache = ConcurrentHashMap <String , Pair <Long , Long >>() // URL -> (responseTime, timestamp)
343- private const val CACHE_VALIDITY_MS = 5 * 60 * 1000L // 5 minutes
344- private const val MAX_TIMEOUT_MS = 1500L // Reduced from 2000ms for faster first load
345- private const val MIN_TIMEOUT_MS = 400L // Reduced for quicker selection
346- private const val FAST_LOAD_TIMEOUT_MS = 800L // Very quick timeout for first attempt
347-
348- fun navigate (context : Context , title : String , link : String ) {
349- val intent = Intent (context, WebActivity ::class .java).apply {
350- putExtra(KEY_TITLE , title)
351- putExtra(KEY_LINK , link)
352- }
353- context.startActivity(intent)
354- }
355-
356- fun loadWebView (content : String , myWebView : WebView ) {
357- setSettingWebView(myWebView)
358- myWebView.loadUrl(content)
359- }
360-
361- private fun setSettingWebView (myWebView : WebView ) {
362- val settings = myWebView.settings
363- settings.builtInZoomControls = false
364- settings.domStorageEnabled = true
365- settings.setGeolocationEnabled(true )
366- settings.loadWithOverviewMode = true
367- settings.useWideViewPort = true
368- myWebView.isScrollbarFadingEnabled = false
369- myWebView.clearCache(false )
370- settings.allowFileAccess = true
371- settings.javaScriptEnabled = true
372- settings.cacheMode = WebSettings .LOAD_DEFAULT
373- settings.defaultTextEncodingName = " utf-8"
374- settings.javaScriptCanOpenWindowsAutomatically = true
375- settings.setSupportMultipleWindows(true )
376- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
377- CookieManager .getInstance().setAcceptThirdPartyCookies(myWebView, true )
378- }
379- }
380- }
381-
382-
383298 // Checks if network is available
384299 private fun isNetworkAvailable (): Boolean {
385300 val connectivityManager = getSystemService(Context .CONNECTIVITY_SERVICE ) as ? android.net.ConnectivityManager
0 commit comments