chore: merge upstream.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh
2023-10-11 21:44:54 +11:00
312 changed files with 4467 additions and 3171 deletions

View File

@ -39,7 +39,9 @@ class NetworkHelper(
builder.addNetworkInterceptor(httpLoggingInterceptor)
}
builder.addInterceptor(CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider))
builder.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
)
when (preferences.dohProvider().get()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()

View File

@ -17,6 +17,9 @@ class NetworkPreferences(
}
fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0")
return preferenceStore.getString(
"default_user_agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0",
)
}
}

View File

@ -58,6 +58,15 @@ fun Call.asObservable(): Observable<Response> {
}
}
fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response ->
if (!response.isSuccessful) {
response.close()
throw HttpException(response.code)
}
}
}
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
@ -95,6 +104,9 @@ suspend fun Call.await(): Response {
return await(callStack)
}
/**
* @since extensions-lib 1.5
*/
suspend fun Call.awaitSuccess(): Response {
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
val response = await(callStack)
@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response {
return response
}
fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response ->
if (!response.isSuccessful) {
response.close()
throw HttpException(response.code)
}
}
}
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)

View File

@ -9,7 +9,10 @@ import okio.Source
import okio.buffer
import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
class ProgressResponseBody(
private val responseBody: ResponseBody,
private val progressListener: ProgressListener,
) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer()
@ -36,7 +39,11 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
val bytesRead = super.read(sink, byteCount)
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
progressListener.update(
totalBytesRead,
responseBody.contentLength(),
bytesRead == -1L,
)
return bytesRead
}
}

View File

@ -31,7 +31,11 @@ class CloudflareInterceptor(
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
}
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
override fun intercept(
chain: Interceptor.Chain,
request: Request,
response: Response,
): Response {
try {
response.close()
cookieManager.remove(request.url, COOKIE_NAMES, 0)

View File

@ -33,7 +33,9 @@ fun OkHttpClient.Builder.rateLimitHost(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
) = addInterceptor(
RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())),
)
/**
* An OkHttp interceptor that handles given url host's rate limiting.
@ -69,8 +71,5 @@ fun OkHttpClient.Builder.rateLimitHost(
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
url: String,
permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
fun OkHttpClient.Builder.rateLimitHost(url: String, permits: Int, period: Duration = 1.seconds) =
addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))

View File

@ -10,7 +10,11 @@ import androidx.annotation.StringRes
* @param resource the text resource.
* @param duration the duration of the toast. Defaults to short.
*/
fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
fun Context.toast(
@StringRes resource: Int,
duration: Int = Toast.LENGTH_SHORT,
block: (Toast) -> Unit = {},
): Toast {
return toast(getString(resource), duration, block)
}
@ -20,7 +24,11 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT,
* @param text the text to display.
* @param duration the duration of the toast. Defaults to short.
*/
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
fun Context.toast(
text: String?,
duration: Int = Toast.LENGTH_SHORT,
block: (Toast) -> Unit = {},
): Toast {
return Toast.makeText(applicationContext, text.orEmpty(), duration).also {
block(it)
it.show()

View File

@ -47,10 +47,7 @@ abstract class WebViewClientCompat : WebViewClient() {
return shouldInterceptRequestCompat(view, request.url.toString())
}
final override fun shouldInterceptRequest(
view: WebView,
url: String,
): WebResourceResponse? {
final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
return shouldInterceptRequestCompat(view, url)
}

View File

@ -14,7 +14,24 @@ import kotlin.coroutines.resume
object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
const val MINIMUM_WEBVIEW_VERSION = 111
const val MINIMUM_WEBVIEW_VERSION = 114
/**
* Uses the WebView's user agent string to create something similar to what Chrome on Android
* would return.
*
* Example of WebView user agent string:
* Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36
*
* Example of Chrome on Android:
* Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.3
*/
fun getInferredUserAgent(context: Context): String {
return WebView(context)
.getDefaultUserAgentString()
.replace("; Android .*?\\)".toRegex(), "; Android 10; K)")
.replace("Version/.* Chrome/".toRegex(), "Chrome/")
}
fun supportsWebView(context: Context): Boolean {
try {

View File

@ -1,7 +1,7 @@
package tachiyomi.core
object Constants {
const val URL_HELP = "https://tachiyomi.org/help/"
const val URL_HELP = "https://tachiyomi.org/docs/guides/troubleshooting/"
const val MANGA_EXTRA = "manga"

View File

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import tachiyomi.core.util.system.logcat
sealed class AndroidPreference<T>(
private val preferences: SharedPreferences,
@ -29,7 +30,13 @@ sealed class AndroidPreference<T>(
}
override fun get(): T {
return read(preferences, key, defaultValue)
return try {
read(preferences, key, defaultValue)
} catch (e: ClassCastException) {
logcat { "Invalid value for $key; deleting" }
delete()
defaultValue
}
}
override fun set(value: T) {
@ -68,7 +75,11 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: String,
) : AndroidPreference<String>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String {
override fun read(
preferences: SharedPreferences,
key: String,
defaultValue: String,
): String {
return preferences.getString(key, defaultValue) ?: defaultValue
}
@ -128,7 +139,11 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Boolean,
) : AndroidPreference<Boolean>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean {
override fun read(
preferences: SharedPreferences,
key: String,
defaultValue: Boolean,
): Boolean {
return preferences.getBoolean(key, defaultValue)
}
@ -143,7 +158,11 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Set<String>,
) : AndroidPreference<Set<String>>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Set<String>): Set<String> {
override fun read(
preferences: SharedPreferences,
key: String,
defaultValue: Set<String>,
): Set<String> {
return preferences.getStringSet(key, defaultValue) ?: defaultValue
}

View File

@ -15,10 +15,9 @@ import tachiyomi.core.preference.AndroidPreference.StringSetPrimitive
class AndroidPreferenceStore(
context: Context,
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context),
) : PreferenceStore {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val keyFlow = sharedPreferences.keyFlow
override fun getString(key: String, defaultValue: String): Preference<String> {
@ -60,11 +59,19 @@ class AndroidPreferenceStore(
deserializer = deserializer,
)
}
override fun getAll(): Map<String, *> {
return sharedPreferences.all ?: emptyMap<String, Any>()
}
}
private val SharedPreferences.keyFlow
get() = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) }
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? ->
trySend(
key,
)
}
registerOnSharedPreferenceChangeListener(listener)
awaitClose {
unregisterOnSharedPreferenceChangeListener(listener)

View File

@ -21,9 +21,29 @@ interface Preference<T> {
fun changes(): Flow<T>
fun stateIn(scope: CoroutineScope): StateFlow<T>
val isPrivate: Boolean
get() = key().startsWith(PRIVATE_PREFIX)
companion object {
/**
* A preference that should not be exposed in places like backups.
*/
fun isPrivate(key: String): Boolean {
return key.startsWith(PRIVATE_PREFIX)
}
fun privateKey(key: String): String {
return "${PRIVATE_PREFIX}$key"
}
private const val PRIVATE_PREFIX = "__PRIVATE_"
}
}
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(block(get()))
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(
block(get()),
)
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
set(get() + item)

View File

@ -31,6 +31,8 @@ interface PreferenceStore {
serializer: (T) -> String,
deserializer: (String) -> T,
): Preference<T>
fun getAll(): Map<String, *>
}
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(

View File

@ -52,9 +52,15 @@ fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
launchIO { withContext(NonCancellable, block) }
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(
Dispatchers.Main,
block,
)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(
Dispatchers.IO,
block,
)
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
withContext(NonCancellable, block)

View File

@ -273,48 +273,6 @@ object ImageUtil {
private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg"
/**
* Check whether the image is a long Strip that needs splitting
* @return true if the image is not animated and it's height is greater than image width and screen height
*/
fun isStripSplitNeeded(imageStream: BufferedInputStream): Boolean {
if (isAnimatedAndSupported(imageStream)) return false
val options = extractImageOptions(imageStream)
val imageHeightIsBiggerThanWidth = options.outHeight > options.outWidth
val imageHeightBiggerThanScreenHeight = options.outHeight > optimalImageHeight
return imageHeightIsBiggerThanWidth && imageHeightBiggerThanScreenHeight
}
/**
* Split the imageStream according to the provided splitData
*/
fun splitStrip(splitData: SplitData, streamFn: () -> InputStream): InputStream {
val bitmapRegionDecoder = getBitmapRegionDecoder(streamFn())
?: throw Exception("Failed to create new instance of BitmapRegionDecoder")
logcat {
"WebtoonSplit #${splitData.index} with topOffset=${splitData.topOffset} " +
"splitHeight=${splitData.splitHeight} bottomOffset=${splitData.bottomOffset}"
}
try {
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, null)
val outputStream = ByteArrayOutputStream()
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
return ByteArrayInputStream(outputStream.toByteArray())
} catch (e: Throwable) {
throw e
} finally {
bitmapRegionDecoder.recycle()
}
}
fun getSplitDataForStream(imageStream: InputStream): List<SplitData> {
return extractImageOptions(imageStream).splitData
}
private val BitmapFactory.Options.splitData
get(): List<SplitData> {
val imageHeight = outHeight