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.WebViewClient
|
||||
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"
|
||||
|
||||
class CloudFlareClient(
|
||||
private val cookieJar: AndroidCookieJar,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val callback: CloudFlareCallback,
|
||||
private val targetUrl: String,
|
||||
) : WebViewClient() {
|
||||
@@ -42,4 +42,4 @@ class CloudFlareClient(
|
||||
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
|
||||
.find { it.name == CF_CLEARANCE }?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import androidx.core.view.isInvisible
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
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.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||
@@ -27,7 +27,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
||||
private val pendingResult = Bundle(1)
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJar: AndroidCookieJar
|
||||
lateinit var cookieJar: MutableCookieJar
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import android.provider.SearchRecentSuggestions
|
||||
import android.text.Html
|
||||
import android.util.AndroidRuntimeException
|
||||
import androidx.collection.arraySetOf
|
||||
import androidx.room.InvalidationTracker
|
||||
import coil.ComponentRegistry
|
||||
@@ -25,6 +26,9 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
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.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||
@@ -52,7 +56,7 @@ import javax.inject.Singleton
|
||||
interface AppModule {
|
||||
|
||||
@Binds
|
||||
fun bindCookieJar(androidCookieJar: AndroidCookieJar): CookieJar
|
||||
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
||||
|
||||
@Binds
|
||||
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
||||
@@ -62,6 +66,17 @@ interface AppModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCookieJar(
|
||||
@ApplicationContext context: Context
|
||||
): MutableCookieJar = try {
|
||||
AndroidCookieJar()
|
||||
} catch (e: AndroidRuntimeException) {
|
||||
// WebView is not available
|
||||
PreferencesCookieJar(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
package org.koitharu.kotatsu.core.network.cookies
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import androidx.annotation.WorkerThread
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@Singleton
|
||||
class AndroidCookieJar @Inject constructor() : CookieJar {
|
||||
class AndroidCookieJar : MutableCookieJar {
|
||||
|
||||
private val cookieManager = CookieManager.getInstance()
|
||||
|
||||
@WorkerThread
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
|
||||
return rawCookie.split(';').mapNotNull {
|
||||
@@ -21,6 +19,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
if (cookies.isEmpty()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 androidx.core.os.LocaleListCompat
|
||||
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.withContext
|
||||
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.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
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
|
||||
class MangaLoaderContextImpl @Inject constructor(
|
||||
override val httpClient: OkHttpClient,
|
||||
override val cookieJar: AndroidCookieJar,
|
||||
override val cookieJar: MutableCookieJar,
|
||||
@ApplicationContext private val androidContext: Context,
|
||||
) : MangaLoaderContext() {
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
@@ -41,7 +41,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
lateinit var shikimoriRepository: ShikimoriRepository
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJar: AndroidCookieJar
|
||||
lateinit var cookieJar: MutableCookieJar
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||
|
||||
Reference in New Issue
Block a user