Option to clear single source cookies
This commit is contained in:
@@ -79,7 +79,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:f096ca2ad3') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +115,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.5.1'
|
implementation 'androidx.room:room-runtime:2.5.1'
|
||||||
implementation 'androidx.room:room-ktx:2.5.1'
|
implementation 'androidx.room:room-ktx:2.5.1'
|
||||||
|
//noinspection KaptUsageInsteadOfKsp
|
||||||
kapt 'androidx.room:room-compiler:2.5.1'
|
kapt 'androidx.room:room-compiler:2.5.1'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.webkit.CookieManager
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
@@ -36,6 +37,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
||||||
}
|
}
|
||||||
|
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||||
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
||||||
|
|||||||
@@ -33,11 +33,21 @@ abstract class ErrorObserver(
|
|||||||
return resolver != null && ExceptionResolver.canResolve(error)
|
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) {
|
protected fun resolve(error: Throwable) {
|
||||||
lifecycleScope.launch {
|
if (isAlive()) {
|
||||||
val isResolved = resolver?.resolve(error) ?: false
|
lifecycleScope.launch {
|
||||||
if (isActive) {
|
val isResolved = resolver?.resolve(error) ?: false
|
||||||
onResolved?.accept(isResolved)
|
if (isActive) {
|
||||||
|
onResolved?.accept(isResolved)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.webkit.CookieManager
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newBuilder
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
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 ->
|
override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
||||||
cookieManager.removeAllCookies(continuation::resume)
|
cookieManager.removeAllCookies(continuation::resume)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,8 @@ interface MutableCookieJar : CookieJar {
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun removeCookies(url: HttpUrl)
|
||||||
|
|
||||||
suspend fun clear(): Boolean
|
suspend fun clear(): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class PreferencesCookieJar(
|
|||||||
private var isLoaded = false
|
private var isLoaded = false
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
@Synchronized
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
loadPersistent()
|
loadPersistent()
|
||||||
val expired = HashSet<String>()
|
val expired = HashSet<String>()
|
||||||
@@ -40,6 +41,7 @@ class PreferencesCookieJar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
@Synchronized
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
val wrapped = cookies.map { CookieWrapper(it) }
|
val wrapped = cookies.map { CookieWrapper(it) }
|
||||||
prefs.edit(commit = true) {
|
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 {
|
override suspend fun clear(): Boolean {
|
||||||
cache.clear()
|
cache.clear()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import okhttp3.Cookie
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
@@ -38,3 +39,23 @@ fun Response.ensureSuccess() = apply {
|
|||||||
throw IllegalStateException(message)
|
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 android.view.View
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
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.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.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
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.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||||
@@ -49,10 +48,18 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
|||||||
getString(R.string.logged_in_as, it)
|
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 ->
|
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||||
findPreference<Preference>(KEY_AUTH)?.isEnabled = !isLoading
|
findPreference<Preference>(KEY_AUTH)?.isEnabled = !isLoading
|
||||||
}
|
}
|
||||||
|
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
@@ -61,32 +68,15 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
|||||||
startActivity(SourceAuthActivity.newIntent(preference.context, viewModel.source))
|
startActivity(SourceAuthActivity.newIntent(preference.context, viewModel.source))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
AppSettings.KEY_COOKIES_CLEAR -> {
|
||||||
|
viewModel.clearCookies()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
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 {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_AUTH = "auth"
|
private const val KEY_AUTH = "auth"
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
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.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.core.util.ext.require
|
||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -17,11 +23,13 @@ import javax.inject.Inject
|
|||||||
class SourceSettingsViewModel @Inject constructor(
|
class SourceSettingsViewModel @Inject constructor(
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
private val cookieJar: MutableCookieJar,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val source = savedStateHandle.require<MangaSource>(SourceSettingsFragment.EXTRA_SOURCE)
|
val source = savedStateHandle.require<MangaSource>(SourceSettingsFragment.EXTRA_SOURCE)
|
||||||
val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
|
val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
|
||||||
|
|
||||||
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
val username = MutableStateFlow<String?>(null)
|
val username = MutableStateFlow<String?>(null)
|
||||||
private var usernameLoadJob: Job? = 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() {
|
private fun loadUsername() {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.webkit.CookieManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
@@ -68,6 +69,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
|||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
||||||
}
|
}
|
||||||
|
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||||
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
||||||
|
|||||||
@@ -437,4 +437,5 @@
|
|||||||
<string name="show_pages_numbers_summary">Show page numbers in bottom corner</string>
|
<string name="show_pages_numbers_summary">Show page numbers in bottom corner</string>
|
||||||
<string name="pages_animation_summary">Animate page switching</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="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>
|
</resources>
|
||||||
|
|||||||
@@ -10,4 +10,11 @@
|
|||||||
android:title="@string/sign_in"
|
android:title="@string/sign_in"
|
||||||
app:allowDividerAbove="true" />
|
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