Skip to content

Commit 055e5fa

Browse files
committed
Improves WebView setup and optimizes URL loading
Refactors WebView configuration into reusable methods for better code reuse and readability. Introduces enhanced URL loading logic with improved timeout handling and caching for faster responses. Replaces hardcoded log tags with a consistent, class-specific tag for cleaner logging. Removes unused methods and redundant status bar configuration code to simplify the activity.
1 parent 878504d commit 055e5fa

File tree

1 file changed

+85
-170
lines changed

1 file changed

+85
-170
lines changed

app/src/main/java/com/book/gofarsi/WebActivity.kt

Lines changed: 85 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,19 @@ package com.book.gofarsi
33
import android.content.Context
44
import android.content.Intent
55
import android.graphics.Bitmap
6-
import android.graphics.Color
76
import android.net.Uri
87
import android.os.Build
98
import android.os.Bundle
109
import android.util.Log
1110
import android.view.View
12-
import android.view.WindowManager
1311
import android.webkit.*
1412
import android.widget.RelativeLayout
1513
import android.widget.TextView
1614
import androidx.activity.OnBackPressedCallback
1715
import androidx.appcompat.app.AppCompatActivity
18-
import androidx.core.view.WindowCompat
19-
import androidx.core.view.WindowInsetsCompat
20-
import androidx.core.view.WindowInsetsControllerCompat
2116
import kotlinx.coroutines.*
2217
import java.util.concurrent.ConcurrentHashMap
2318
import 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

Comments
 (0)