Skip to content

Commit 2209031

Browse files
committed
fix: load issue
1 parent 055e5fa commit 2209031

File tree

12 files changed

+206
-19
lines changed

12 files changed

+206
-19
lines changed

app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ dependencies {
6565
implementation 'androidx.appcompat:appcompat:1.7.0'
6666
implementation 'com.google.android.material:material:1.12.0'
6767
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
68+
implementation 'com.airbnb.android:lottie:6.6.7'
69+
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
6870
testImplementation 'junit:junit:4.13.2'
6971
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
7072
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.book.gofarsi
2+
3+
import android.content.Context
4+
import android.webkit.WebResourceRequest
5+
import android.webkit.WebResourceResponse
6+
import java.io.File
7+
import java.io.FileInputStream
8+
import java.io.FileOutputStream
9+
import java.io.IOException
10+
import java.math.BigInteger
11+
import java.net.URLConnection
12+
import java.security.MessageDigest
13+
14+
class CacheInterceptor(context: Context) {
15+
private val cacheDir = File(context.cacheDir, "web_cache")
16+
private val maxCacheSize = 50 * 1024 * 1024 // 50 MB
17+
18+
init {
19+
if (!cacheDir.exists()) cacheDir.mkdirs()
20+
}
21+
22+
fun getResponse(request: WebResourceRequest): WebResourceResponse? {
23+
val url = request.url.toString()
24+
val cacheFile = File(cacheDir, url.toMD5())
25+
26+
if (cacheFile.exists()) {
27+
return createResponse(cacheFile, URLConnection.guessContentTypeFromName(url))
28+
}
29+
return null
30+
}
31+
32+
fun saveResponse(url: String, response: WebResourceResponse) {
33+
val cacheFile = File(cacheDir, url.toMD5())
34+
response.data?.let { data ->
35+
try {
36+
FileOutputStream(cacheFile).use { output ->
37+
data.copyTo(output)
38+
}
39+
} catch (e: IOException) {
40+
e.printStackTrace()
41+
}
42+
}
43+
manageCacheSize()
44+
}
45+
46+
private fun manageCacheSize() {
47+
var totalSize = cacheDir.listFiles()?.sumOf { it.length() } ?: 0
48+
if (totalSize > maxCacheSize) {
49+
cacheDir.listFiles()
50+
?.sortedBy { it.lastModified() }
51+
?.forEach {
52+
if (totalSize > maxCacheSize) {
53+
totalSize -= it.length()
54+
it.delete()
55+
}
56+
}
57+
}
58+
}
59+
60+
private fun createResponse(file: File, mimeType: String?): WebResourceResponse {
61+
return WebResourceResponse(
62+
mimeType,
63+
"UTF-8",
64+
FileInputStream(file)
65+
)
66+
}
67+
68+
private fun String.toMD5(): String {
69+
val md = MessageDigest.getInstance("MD5")
70+
return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0')
71+
}
72+
}

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

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import android.widget.TextView
1414
import androidx.activity.OnBackPressedCallback
1515
import androidx.appcompat.app.AppCompatActivity
1616
import kotlinx.coroutines.*
17+
import okhttp3.OkHttpClient
18+
import okhttp3.Request
19+
import java.io.IOException
1720
import java.util.concurrent.ConcurrentHashMap
21+
import java.util.concurrent.TimeUnit
1822
import kotlin.system.measureTimeMillis
1923

2024

@@ -24,15 +28,24 @@ class WebActivity : AppCompatActivity() {
2428
var myWebView: WebView? = null
2529
private val list = arrayListOf(
2630
"https://book.gofarsi.ir/",
27-
"https://cloud-book.gofarsi.ir/",
28-
"https://ir1-book.gofarsi.ir/",
29-
"https://ipfs-book.gofarsi.ir/",
30-
"https://hku1-book.gofarsi.ir/",
31-
"https://aws1-book.gofarsi.ir/"
31+
// "https://cloud-book.gofarsi.ir/",
32+
// "https://ir1-book.gofarsi.ir/",
33+
// "https://ipfs-book.gofarsi.ir/",
34+
// "https://hku1-book.gofarsi.ir/",
35+
// "https://aws1-book.gofarsi.ir/"
3236
)
3337
var urlIndex = 0
3438
var hasErrorInLoading = false
3539
var currentUrl = ""
40+
private val client = OkHttpClient.Builder()
41+
.connectTimeout(3, TimeUnit.SECONDS)
42+
.readTimeout(3, TimeUnit.SECONDS)
43+
.build()
44+
45+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
46+
private val job = Job()
47+
private val coroutineScope = CoroutineScope(Dispatchers.Main + job)
48+
private lateinit var cacheInterceptor:CacheInterceptor
3649

3750
companion object {
3851
private const val TAG = "WebActivity" // Better log tag
@@ -83,7 +96,7 @@ class WebActivity : AppCompatActivity() {
8396
super.onCreate(savedInstanceState)
8497
try {
8598
setContentView(R.layout.activity_web)
86-
99+
cacheInterceptor = CacheInterceptor(applicationContext)
87100
// Find the fastest URL before loading
88101
findViewById<TextView>(R.id.tvVersion).text = BuildConfig.VERSION_NAME
89102
myWebView = findViewById(R.id.webview)
@@ -97,23 +110,38 @@ class WebActivity : AppCompatActivity() {
97110
// Note: allowFileAccessFromFileURLs and allowUniversalAccessFromFileURLs are deprecated
98111
// and removed for security reasons. Modern apps should use alternative approaches.
99112
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
100-
webSettings.cacheMode = if (isNetworkAvailable()) {
101-
WebSettings.LOAD_DEFAULT
102-
} else {
103-
WebSettings.LOAD_CACHE_ELSE_NETWORK
113+
// webSettings.cacheMode = if (isNetworkAvailable()) {
114+
// WebSettings.LOAD_DEFAULT
115+
// } else {
116+
// WebSettings.LOAD_CACHE_ELSE_NETWORK
117+
// }
118+
if (!isNetworkAvailable()){
119+
myWebView?.loadUrl("javascript:document.body.innerHTML='" +
120+
getOfflineHtml() + "'")
104121
}
105122

106123
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
107124
ServiceWorkerController.getInstance().setServiceWorkerClient(object : ServiceWorkerClient() {
108125
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
109-
return null
126+
//return null
127+
if (!isNetworkAvailable()) {
128+
return cacheInterceptor.getResponse(request)
129+
}
130+
131+
132+
val networkResponse = super.shouldInterceptRequest(request)
133+
networkResponse?.let {
134+
cacheInterceptor.saveResponse(request.url.toString(), it)
135+
}
136+
return networkResponse
110137
}
111138
})
112139
}
113140
// --- End WebView settings ---
114141

115142
// Launch coroutine to find fastest URL
116-
findFastestUrlAndLoad()
143+
//findFastestUrlAndLoad(list)
144+
myWebView?.loadUrl(list[0])
117145
} catch (e: Exception) {
118146
Log.e("WebActivity", "Error in onCreate: ${e.message}", e)
119147
finish()
@@ -146,7 +174,8 @@ class WebActivity : AppCompatActivity() {
146174
override fun onPageFinished(view: WebView?, url: String?) {
147175
super.onPageFinished(view, url)
148176
currentUrl = url ?: ""
149-
if (!hasErrorInLoading) findViewById<RelativeLayout>(R.id.loading).visibility = View.INVISIBLE
177+
// if (!hasErrorInLoading)
178+
findViewById<RelativeLayout>(R.id.loading).visibility = View.INVISIBLE
150179
Log.d(TAG, "onPageFinished: $url")
151180
}
152181

@@ -165,6 +194,21 @@ class WebActivity : AppCompatActivity() {
165194
onBackPressedDispatcher.addCallback(this, callback)
166195
}
167196

197+
198+
private fun getOfflineHtml(): String {
199+
return try {
200+
val inputStream = application.assets.open("offline.html")
201+
val size = inputStream.available()
202+
val buffer = ByteArray(size)
203+
inputStream.read(buffer)
204+
inputStream.close()
205+
String(buffer)
206+
} catch (e: IOException) {
207+
"<h1>You're offline</h1><p>No cached content available</p>"
208+
}
209+
}
210+
211+
168212
private fun findFastestUrlAndLoad() {
169213
CoroutineScope(Dispatchers.IO).launch {
170214
val fastestUrl = findFastestUrlOptimized()
@@ -238,6 +282,7 @@ class WebActivity : AppCompatActivity() {
238282
Log.e("WebActivity", "Error in concurrent URL testing: ${e.message}")
239283
}
240284

285+
241286
fastestResult?.first
242287
}
243288
}
@@ -263,6 +308,7 @@ class WebActivity : AppCompatActivity() {
263308
throw Exception("HTTP $responseCode")
264309
}
265310
}
311+
Log.i("aaa",responseTime.toString())
266312
responseTime
267313
} catch (e: Exception) {
268314
Log.d("WebActivity", "URL $url failed: ${e.message}")
@@ -271,6 +317,48 @@ class WebActivity : AppCompatActivity() {
271317
}
272318
}
273319

320+
321+
private fun findFastestUrlAndLoad(urls: List<String>) {
322+
coroutineScope.launch {
323+
val fastestUrl = findFastestUrl(urls)
324+
fastestUrl?.let {
325+
myWebView?.loadUrl(it)
326+
} ?: run {
327+
myWebView?.loadUrl(list[0])
328+
}
329+
}
330+
}
331+
332+
private suspend fun findFastestUrl(urls: List<String>): String? = coroutineScope {
333+
val jobs = urls.map { url ->
334+
async(Dispatchers.IO) {
335+
try {
336+
val request = Request.Builder()
337+
.url(url)
338+
.head() // فقط هدرها را دریافت می‌کند
339+
.build()
340+
341+
val responseTime = measureTimeMillis {
342+
client.newCall(request).execute().use { response ->
343+
if (!response.isSuccessful) null
344+
}
345+
}
346+
347+
if (responseTime > 0) url to responseTime else null
348+
} catch (e: Exception) {
349+
null
350+
}
351+
}
352+
}
353+
354+
// منتظر ماندن برای اولین پاسخ موفق
355+
val results = jobs.awaitAll().filterNotNull()
356+
357+
// پیدا کردن سریع‌ترین پاسخ
358+
results.minByOrNull { it.second }?.first
359+
}
360+
361+
274362
fun loadBrowser(url:String){
275363
val uri: Uri = Uri.parse(url)
276364
val intent = Intent(Intent.ACTION_VIEW, uri)
@@ -341,6 +429,7 @@ class WebActivity : AppCompatActivity() {
341429
super.onResume()
342430
try {
343431
myWebView?.onResume()
432+
scope.cancel()
344433
} catch (e: Exception) {
345434
Log.e("WebActivity", "Error in onResume: ${e.message}")
346435
}

app/src/main/res/layout/activity_web.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
33
android:layout_width="match_parent"
44
android:layout_height="match_parent"
5+
xmlns:app="http://schemas.android.com/apk/res-auto"
56
android:background="@color/white">
67

78
<WebView
@@ -27,8 +28,26 @@
2728
android:layout_height="34dp"
2829
android:layout_above="@id/tvVersion"
2930
android:layout_centerHorizontal="true"
31+
android:visibility="gone"
3032
android:layout_marginBottom="30dp" />
3133

34+
35+
<com.airbnb.lottie.LottieAnimationView
36+
android:id="@+id/animation_view"
37+
android:layout_width="match_parent"
38+
android:layout_height="150dp"
39+
android:layout_above="@id/tvVersion"
40+
android:layout_centerHorizontal="true"
41+
android:layout_gravity="center"
42+
android:scaleType="fitCenter"
43+
app:lottie_autoPlay="true"
44+
app:lottie_loop="true"
45+
app:lottie_progress="0.5"
46+
app:lottie_rawRes="@raw/bookblue"
47+
app:lottie_renderMode="automatic"
48+
app:lottie_repeatMode="restart"
49+
app:lottie_speed="1.0" />
50+
3251
<TextView
3352
android:id="@+id/tvVersion"
3453
android:layout_width="match_parent"

app/src/main/res/raw/bookblue.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

app/src/main/res/values-night/themes.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
<item name="colorSecondaryVariant">@color/teal_200</item>
1111
<item name="colorOnSecondary">@color/black</item>
1212
<!-- Status bar color. -->
13-
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
13+
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
14+
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
15+
<item name="android:windowTranslucentStatus">false</item>
1416
<!-- Customize your theme here. -->
1517
</style>
1618
</resources>

app/src/main/res/values/themes.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
<item name="colorSecondaryVariant">@color/teal_700</item>
1111
<item name="colorOnSecondary">@color/black</item>
1212
<!-- Status bar color. -->
13-
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
13+
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
14+
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
15+
<item name="android:windowTranslucentStatus">false</item>
1416
<!-- Customize your theme here. -->
1517
</style>
1618
</resources>

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22
plugins {
3-
id 'com.android.application' version '8.7.3' apply false
3+
id 'com.android.application' version '8.8.0' apply false
44
id 'com.android.library' version '8.7.3' apply false
55
id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
66
}

generate-keystore.sh

100755100644
File mode changed.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
#Mon Nov 28 09:08:13 IRST 2022
1+
#Sat Jul 26 16:14:10 IRST 2025
22
distributionBase=GRADLE_USER_HOME
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
43
distributionPath=wrapper/dists
5-
zipStorePath=wrapper/dists
4+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
65
zipStoreBase=GRADLE_USER_HOME
6+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)