diff --git a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt index 6797915b1..97542c58d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import okhttp3.Cache import okhttp3.CookieJar import okhttp3.OkHttpClient import org.koitharu.kotatsu.BuildConfig @@ -88,14 +89,19 @@ interface AppModule { @Provides @Singleton - fun provideOkHttpClient( + fun provideHttpCache( localStorageManager: LocalStorageManager, + ): Cache = localStorageManager.createHttpCache() + + @Provides + @Singleton + fun provideOkHttpClient( + cache: Cache, commonHeadersInterceptor: CommonHeadersInterceptor, mirrorSwitchInterceptor: MirrorSwitchInterceptor, cookieJar: CookieJar, settings: AppSettings, ): OkHttpClient { - val cache = localStorageManager.createHttpCache() return OkHttpClient.Builder().apply { connectTimeout(20, TimeUnit.SECONDS) readTimeout(60, TimeUnit.SECONDS) @@ -106,6 +112,7 @@ interface AppModule { bypassSSLErrors() } cache(cache) + addNetworkInterceptor(CacheLimitInterceptor()) addInterceptor(GZipInterceptor()) addInterceptor(commonHeadersInterceptor) addInterceptor(CloudFlareInterceptor()) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt new file mode 100644 index 000000000..52710c57b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.core.network + +import okhttp3.CacheControl +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.TimeUnit + +class CacheLimitInterceptor : Interceptor { + + private val defaultMaxAge = TimeUnit.HOURS.toSeconds(1) + private val defaultCacheControl = CacheControl.Builder() + .maxAge(defaultMaxAge.toInt(), TimeUnit.SECONDS) + .build() + .toString() + + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + val responseCacheControl = CacheControl.parse(response.headers) + if (responseCacheControl.noStore || responseCacheControl.maxAgeSeconds <= defaultMaxAge) { + return response + } + return response.newBuilder() + .header(CommonHeaders.CACHE_CONTROL, defaultCacheControl) + .build() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeaders.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeaders.kt index 943e08f2e..f8976acd6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeaders.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeaders.kt @@ -13,6 +13,7 @@ object CommonHeaders { const val CONTENT_ENCODING = "Content-Encoding" const val ACCEPT_ENCODING = "Accept-Encoding" const val AUTHORIZATION = "Authorization" + const val CACHE_CONTROL = "Cache-Control" val CACHE_CONTROL_NO_STORE: CacheControl get() = CacheControl.Builder().noStore().build() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 6dab7e362..dc13b7abb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -345,6 +345,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_SOURCES_HIDDEN = "sources_hidden" const val KEY_TRAFFIC_WARNING = "traffic_warning" const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear" + const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear" const val KEY_COOKIES_CLEAR = "cookies_clear" const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear" const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index c0b53620a..82f6bc441 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -8,7 +8,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible +import okhttp3.Cache import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar @@ -39,6 +42,9 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach @Inject lateinit var cookieJar: MutableCookieJar + @Inject + lateinit var cache: Cache + @Inject lateinit var shortcutsUpdater: ShortcutsUpdater @@ -52,6 +58,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach super.onViewCreated(view, savedInstanceState) findPreference(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.PAGES) findPreference(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.THUMBS) + findPreference(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindSummaryToHttpCacheSize() findPreference(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref -> viewLifecycleScope.launch { lifecycle.awaitStateAtLeast(Lifecycle.State.RESUMED) @@ -90,6 +97,11 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach true } + AppSettings.KEY_HTTP_CACHE_CLEAR -> { + clearHttpCache() + true + } + AppSettings.KEY_UPDATES_FEED_CLEAR -> { viewLifecycleScope.launch { trackerRepo.clearLogs() @@ -131,6 +143,32 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach summary = FileSize.BYTES.format(context, size) } + private fun Preference.bindSummaryToHttpCacheSize() = viewLifecycleScope.launch { + val size = runInterruptible(Dispatchers.IO) { cache.size() } + summary = FileSize.BYTES.format(context, size) + } + + private fun clearHttpCache() { + val preference = findPreference(AppSettings.KEY_HTTP_CACHE_CLEAR) ?: return + val ctx = preference.context.applicationContext + viewLifecycleScope.launch { + try { + preference.isEnabled = false + val size = runInterruptible(Dispatchers.IO) { + cache.evictAll() + cache.size() + } + preference.summary = FileSize.BYTES.format(ctx, size) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + preference.summary = e.getDisplayMessage(ctx.resources) + } finally { + preference.isEnabled = true + } + } + } + private fun clearSearchHistory(preference: Preference) { MaterialAlertDialogBuilder(context ?: return) .setTitle(R.string.clear_search_history) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6521d51d0..c92e6527d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -417,4 +417,5 @@ Translations WebView not available: check if WebView provider is installed Invalid value + Clear network cache diff --git a/app/src/main/res/xml/pref_history.xml b/app/src/main/res/xml/pref_history.xml index a7304418c..de6088bc8 100644 --- a/app/src/main/res/xml/pref_history.xml +++ b/app/src/main/res/xml/pref_history.xml @@ -39,6 +39,12 @@ android:summary="@string/computing_" android:title="@string/clear_pages_cache" /> + +