Option to clear single source cookies
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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<ActivityBrowserBinding>(), 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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Boolean> { continuation ->
|
||||
cookieManager.removeAllCookies(continuation::resume)
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ interface MutableCookieJar : CookieJar {
|
||||
@WorkerThread
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
|
||||
|
||||
@WorkerThread
|
||||
fun removeCookies(url: HttpUrl)
|
||||
|
||||
suspend fun clear(): Boolean
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class PreferencesCookieJar(
|
||||
private var isLoaded = false
|
||||
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
loadPersistent()
|
||||
val expired = HashSet<String>()
|
||||
@@ -40,6 +41,7 @@ class PreferencesCookieJar(
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
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<String>()
|
||||
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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Preference>(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"
|
||||
|
||||
@@ -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<MangaSource>(SourceSettingsFragment.EXTRA_SOURCE)
|
||||
val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val username = MutableStateFlow<String?>(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 {
|
||||
|
||||
@@ -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<ActivityBrowserBinding>(), 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)
|
||||
|
||||
@@ -437,4 +437,5 @@
|
||||
<string name="show_pages_numbers_summary">Show page numbers in bottom corner</string>
|
||||
<string name="pages_animation_summary">Animate page switching</string>
|
||||
<string name="details_button_tip">Press and hold the Read button to see more options</string>
|
||||
<string name="clear_source_cookies_summary">Clear cookies for specified domain only. In most cases will invalidate authorization</string>
|
||||
</resources>
|
||||
|
||||
@@ -10,4 +10,11 @@
|
||||
android:title="@string/sign_in"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
<Preference
|
||||
android:key="cookies_clear"
|
||||
android:order="101"
|
||||
android:persistent="false"
|
||||
android:summary="@string/clear_source_cookies_summary"
|
||||
android:title="@string/clear_cookies" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user