From 92af851d3ba476f3d2b3d4962a464a6eae7b3c49 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 20 Jun 2023 13:43:09 +0300 Subject: [PATCH] Option to clear single source cookies --- app/build.gradle | 3 +- .../kotatsu/browser/BrowserActivity.kt | 2 + .../core/exceptions/resolve/ErrorObserver.kt | 18 ++++++-- .../core/network/cookies/AndroidCookieJar.kt | 16 +++++++ .../core/network/cookies/MutableCookieJar.kt | 3 ++ .../network/cookies/PreferencesCookieJar.kt | 18 ++++++++ .../koitharu/kotatsu/core/util/ext/Http.kt | 21 ++++++++++ .../sources/SourceSettingsFragment.kt | 42 +++++++------------ .../sources/SourceSettingsViewModel.kt | 20 +++++++++ .../sources/auth/SourceAuthActivity.kt | 2 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_source.xml | 9 +++- 12 files changed, 123 insertions(+), 32 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 059544422..e357478a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,7 +79,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') { + implementation('com.github.KotatsuApp:kotatsu-parsers:f096ca2ad3') { exclude group: 'org.json', module: 'json' } @@ -115,6 +115,7 @@ dependencies { implementation 'androidx.room:room-runtime:2.5.1' implementation 'androidx.room:room-ktx:2.5.1' + //noinspection KaptUsageInsteadOfKsp kapt 'androidx.room:room-compiler:2.5.1' implementation 'com.squareup.okhttp3:okhttp:4.11.0' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt index 7d9217ea5..f7e85e129 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.webkit.CookieManager import androidx.core.graphics.Insets import androidx.core.view.isVisible import androidx.core.view.updatePadding @@ -36,6 +37,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback javaScriptEnabled = true userAgentString = CommonHeadersInterceptor.userAgentChrome } + CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) viewBinding.webView.webViewClient = BrowserClient(this) viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt index a64fb9edf..f14a42f79 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt @@ -33,11 +33,21 @@ abstract class ErrorObserver( return resolver != null && ExceptionResolver.canResolve(error) } + private fun isAlive(): Boolean { + return when { + fragment != null -> fragment.view != null + activity != null -> !activity.isDestroyed + else -> true + } + } + protected fun resolve(error: Throwable) { - lifecycleScope.launch { - val isResolved = resolver?.resolve(error) ?: false - if (isActive) { - onResolved?.accept(isResolved) + if (isAlive()) { + lifecycleScope.launch { + val isResolved = resolver?.resolve(error) ?: false + if (isActive) { + onResolved?.accept(isResolved) + } } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt index 5b0c3d822..8a6dabc30 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt @@ -4,6 +4,7 @@ import android.webkit.CookieManager import androidx.annotation.WorkerThread import okhttp3.Cookie import okhttp3.HttpUrl +import org.koitharu.kotatsu.core.util.ext.newBuilder import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -30,6 +31,21 @@ class AndroidCookieJar : MutableCookieJar { } } + override fun removeCookies(url: HttpUrl) { + val cookies = loadForRequest(url) + if (cookies.isEmpty()) { + return + } + val urlString = url.toString() + for (c in cookies) { + val nc = c.newBuilder() + .expiresAt(System.currentTimeMillis() - 100000) + .build() + cookieManager.setCookie(urlString, nc.toString()) + } + check(loadForRequest(url).isEmpty()) + } + override suspend fun clear() = suspendCoroutine { continuation -> cookieManager.removeAllCookies(continuation::resume) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt index 9059e5a6f..01d81a8d2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt @@ -13,5 +13,8 @@ interface MutableCookieJar : CookieJar { @WorkerThread override fun saveFromResponse(url: HttpUrl, cookies: List) + @WorkerThread + fun removeCookies(url: HttpUrl) + suspend fun clear(): Boolean } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt index 623824e57..bfd95f295 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt @@ -21,6 +21,7 @@ class PreferencesCookieJar( private var isLoaded = false @WorkerThread + @Synchronized override fun loadForRequest(url: HttpUrl): List { loadPersistent() val expired = HashSet() @@ -40,6 +41,7 @@ class PreferencesCookieJar( } @WorkerThread + @Synchronized override fun saveFromResponse(url: HttpUrl, cookies: List) { val wrapped = cookies.map { CookieWrapper(it) } prefs.edit(commit = true) { @@ -53,6 +55,22 @@ class PreferencesCookieJar( } } + @Synchronized + @WorkerThread + override fun removeCookies(url: HttpUrl) { + loadPersistent() + val toRemove = HashSet() + for ((key, cookie) in cache) { + if (cookie.isExpired() || cookie.cookie.matches(url)) { + toRemove += key + } + } + if (toRemove.isNotEmpty()) { + cache.removeAll(toRemove) + removePersistent(toRemove) + } + } + override suspend fun clear(): Boolean { cache.clear() withContext(Dispatchers.IO) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Http.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Http.kt index 45463f045..04c456b9b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Http.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Http.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.util.ext +import okhttp3.Cookie import okhttp3.HttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody @@ -38,3 +39,23 @@ fun Response.ensureSuccess() = apply { throw IllegalStateException(message) } } + +fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c -> + c.name(name) + c.value(value) + if (persistent) { + c.expiresAt(expiresAt) + } + if (hostOnly) { + c.hostOnlyDomain(domain) + } else { + c.domain(domain) + } + c.path(path) + if (secure) { + c.secure() + } + if (httpOnly) { + c.httpOnly() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt index 2fbae0771..3e0ddf42d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt @@ -4,16 +4,15 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels import androidx.preference.Preference -import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver +import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BasePreferenceFragment -import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity @@ -49,10 +48,18 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { getString(R.string.logged_in_as, it) } } - viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) + viewModel.onError.observeEvent( + viewLifecycleOwner, + SnackbarErrorObserver( + listView, + this, + exceptionResolver, + ) { viewModel.onResume() }, + ) viewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> findPreference(KEY_AUTH)?.isEnabled = !isLoading } + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView)) } override fun onPreferenceTreeClick(preference: Preference): Boolean { @@ -61,32 +68,15 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { startActivity(SourceAuthActivity.newIntent(preference.context, viewModel.source)) true } + AppSettings.KEY_COOKIES_CLEAR -> { + viewModel.clearCookies() + true + } else -> super.onPreferenceTreeClick(preference) } } - private fun onError(error: Throwable) { - val snackbar = Snackbar.make( - listView ?: return, - error.getDisplayMessage(resources), - Snackbar.LENGTH_INDEFINITE, - ) - if (ExceptionResolver.canResolve(error)) { - snackbar.setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) } - } - snackbar.show() - } - - private fun resolveError(error: Throwable) { - view ?: return - viewLifecycleScope.launch { - if (exceptionResolver.resolve(error)) { - viewModel.onResume() - } - } - } - companion object { private const val KEY_AUTH = "auth" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt index 173c53b69..4fcc269d1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt @@ -5,9 +5,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import okhttp3.HttpUrl +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.MangaSource @@ -17,11 +23,13 @@ import javax.inject.Inject class SourceSettingsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, mangaRepositoryFactory: MangaRepository.Factory, + private val cookieJar: MutableCookieJar, ) : BaseViewModel() { val source = savedStateHandle.require(SourceSettingsFragment.EXTRA_SOURCE) val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository + val onActionDone = MutableEventFlow() val username = MutableStateFlow(null) private var usernameLoadJob: Job? = null @@ -35,6 +43,18 @@ class SourceSettingsViewModel @Inject constructor( } } + fun clearCookies() { + launchLoadingJob(Dispatchers.Default) { + val url = HttpUrl.Builder() + .scheme("https") + .host(repository.domain) + .build() + cookieJar.removeCookies(url) + onActionDone.call(ReversibleAction(R.string.cookies_cleared, null)) + loadUsername() + } + } + private fun loadUsername() { launchLoadingJob(Dispatchers.Default) { try { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt index 68913fe55..42cef7ea6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import android.webkit.CookieManager import android.widget.Toast import androidx.activity.result.contract.ActivityResultContract import androidx.core.graphics.Insets @@ -68,6 +69,7 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba javaScriptEnabled = true userAgentString = CommonHeadersInterceptor.userAgentChrome } + CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) viewBinding.webView.webViewClient = BrowserClient(this) viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1dbc0180f..16701719e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -437,4 +437,5 @@ Show page numbers in bottom corner Animate page switching Press and hold the Read button to see more options + Clear cookies for specified domain only. In most cases will invalidate authorization diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml index 51a669888..5422bb214 100644 --- a/app/src/main/res/xml/pref_source.xml +++ b/app/src/main/res/xml/pref_source.xml @@ -10,4 +10,11 @@ android:title="@string/sign_in" app:allowDividerAbove="true" /> - \ No newline at end of file + + +