CookieJar implementation for non-WebView environment
This commit is contained in:
@@ -4,12 +4,12 @@ import android.graphics.Bitmap
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
|
|
||||||
private const val CF_CLEARANCE = "cf_clearance"
|
private const val CF_CLEARANCE = "cf_clearance"
|
||||||
|
|
||||||
class CloudFlareClient(
|
class CloudFlareClient(
|
||||||
private val cookieJar: AndroidCookieJar,
|
private val cookieJar: MutableCookieJar,
|
||||||
private val callback: CloudFlareCallback,
|
private val callback: CloudFlareCallback,
|
||||||
private val targetUrl: String,
|
private val targetUrl: String,
|
||||||
) : WebViewClient() {
|
) : WebViewClient() {
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import androidx.core.view.isInvisible
|
|||||||
import androidx.fragment.app.setFragmentResult
|
import androidx.fragment.app.setFragmentResult
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
|
||||||
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||||
@@ -27,7 +27,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
|||||||
private val pendingResult = Bundle(1)
|
private val pendingResult = Bundle(1)
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var cookieJar: AndroidCookieJar
|
lateinit var cookieJar: MutableCookieJar
|
||||||
|
|
||||||
override fun onInflateView(
|
override fun onInflateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
|
import android.util.AndroidRuntimeException
|
||||||
import androidx.collection.arraySetOf
|
import androidx.collection.arraySetOf
|
||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
import coil.ComponentRegistry
|
import coil.ComponentRegistry
|
||||||
@@ -25,6 +26,9 @@ import org.koitharu.kotatsu.BuildConfig
|
|||||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.network.*
|
import org.koitharu.kotatsu.core.network.*
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
@@ -52,7 +56,7 @@ import javax.inject.Singleton
|
|||||||
interface AppModule {
|
interface AppModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindCookieJar(androidCookieJar: AndroidCookieJar): CookieJar
|
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
||||||
@@ -62,6 +66,17 @@ interface AppModule {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideCookieJar(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): MutableCookieJar = try {
|
||||||
|
AndroidCookieJar()
|
||||||
|
} catch (e: AndroidRuntimeException) {
|
||||||
|
// WebView is not available
|
||||||
|
PreferencesCookieJar(context)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideOkHttpClient(
|
fun provideOkHttpClient(
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import javax.inject.Inject
|
import androidx.annotation.WorkerThread
|
||||||
import javax.inject.Singleton
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
@Singleton
|
class AndroidCookieJar : MutableCookieJar {
|
||||||
class AndroidCookieJar @Inject constructor() : CookieJar {
|
|
||||||
|
|
||||||
private val cookieManager = CookieManager.getInstance()
|
private val cookieManager = CookieManager.getInstance()
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
|
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
|
||||||
return rawCookie.split(';').mapNotNull {
|
return rawCookie.split(';').mapNotNull {
|
||||||
@@ -21,6 +19,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
if (cookies.isEmpty()) {
|
if (cookies.isEmpty()) {
|
||||||
return
|
return
|
||||||
@@ -31,7 +30,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
||||||
cookieManager.removeAllCookies(continuation::resume)
|
cookieManager.removeAllCookies(continuation::resume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectOutputStream
|
||||||
|
|
||||||
|
|
||||||
|
class CookieWrapper(
|
||||||
|
val cookie: Cookie,
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(encodedString: String) : this(
|
||||||
|
ObjectInputStream(ByteArrayInputStream(Base64.decode(encodedString, Base64.NO_WRAP))).use {
|
||||||
|
val name = it.readUTF()
|
||||||
|
val value = it.readUTF()
|
||||||
|
val expiresAt = it.readLong()
|
||||||
|
val domain = it.readUTF()
|
||||||
|
val path = it.readUTF()
|
||||||
|
val secure = it.readBoolean()
|
||||||
|
val httpOnly = it.readBoolean()
|
||||||
|
val persistent = it.readBoolean()
|
||||||
|
val hostOnly = it.readBoolean()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fun encode(): String {
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
ObjectOutputStream(output).use {
|
||||||
|
it.writeUTF(cookie.name)
|
||||||
|
it.writeUTF(cookie.value)
|
||||||
|
it.writeLong(cookie.expiresAt)
|
||||||
|
it.writeUTF(cookie.domain)
|
||||||
|
it.writeUTF(cookie.path)
|
||||||
|
it.writeBoolean(cookie.secure)
|
||||||
|
it.writeBoolean(cookie.httpOnly)
|
||||||
|
it.writeBoolean(cookie.persistent)
|
||||||
|
it.writeBoolean(cookie.hostOnly)
|
||||||
|
}
|
||||||
|
return Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||||
|
|
||||||
|
fun key(): String {
|
||||||
|
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as CookieWrapper
|
||||||
|
|
||||||
|
if (cookie != other.cookie) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return cookie.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
interface MutableCookieJar : CookieJar {
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie>
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
|
||||||
|
|
||||||
|
suspend fun clear(): Boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.collection.ArrayMap
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
|
||||||
|
private const val PREFS_NAME = "cookies"
|
||||||
|
|
||||||
|
class PreferencesCookieJar(
|
||||||
|
context: Context,
|
||||||
|
) : MutableCookieJar {
|
||||||
|
|
||||||
|
private val cache = ArrayMap<String, CookieWrapper>()
|
||||||
|
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
private var isLoaded = false
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
loadPersistent()
|
||||||
|
val expired = HashSet<String>()
|
||||||
|
val result = ArrayList<Cookie>()
|
||||||
|
for ((key, cookie) in cache) {
|
||||||
|
if (cookie.isExpired()) {
|
||||||
|
expired += key
|
||||||
|
} else if (cookie.cookie.matches(url)) {
|
||||||
|
result += cookie.cookie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expired.isNotEmpty()) {
|
||||||
|
cache.removeAll(expired)
|
||||||
|
removePersistent(expired)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
val wrapped = cookies.map { CookieWrapper(it) }
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
for (cookie in wrapped) {
|
||||||
|
val key = cookie.key()
|
||||||
|
cache[key] = cookie
|
||||||
|
if (cookie.cookie.persistent) {
|
||||||
|
putString(key, cookie.encode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear(): Boolean {
|
||||||
|
cache.clear()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
prefs.edit(commit = true) { clear() }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun loadPersistent() {
|
||||||
|
if (!isLoaded) {
|
||||||
|
val map = prefs.all
|
||||||
|
cache.ensureCapacity(map.size)
|
||||||
|
for ((k, v) in map) {
|
||||||
|
val cookie = try {
|
||||||
|
CookieWrapper(v as String)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cache[k] = cookie
|
||||||
|
}
|
||||||
|
isLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removePersistent(keys: Collection<String>) {
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
for (key in keys) {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,25 +6,25 @@ import android.util.Base64
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.ext.toList
|
import org.koitharu.kotatsu.utils.ext.toList
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class MangaLoaderContextImpl @Inject constructor(
|
class MangaLoaderContextImpl @Inject constructor(
|
||||||
override val httpClient: OkHttpClient,
|
override val httpClient: OkHttpClient,
|
||||||
override val cookieJar: AndroidCookieJar,
|
override val cookieJar: MutableCookieJar,
|
||||||
@ApplicationContext private val androidContext: Context,
|
@ApplicationContext private val androidContext: Context,
|
||||||
) : MangaLoaderContext() {
|
) : MangaLoaderContext() {
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
@@ -41,7 +41,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
lateinit var shikimoriRepository: ShikimoriRepository
|
lateinit var shikimoriRepository: ShikimoriRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var cookieJar: AndroidCookieJar
|
lateinit var cookieJar: MutableCookieJar
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||||
|
|||||||
Reference in New Issue
Block a user