Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,18 @@ dependencies {
implementation libs.material
implementation libs.androidx.lifecycle.common.java8
implementation libs.androidx.webkit
implementation libs.injekt.core

implementation libs.androidx.work.runtime
implementation libs.guava
implementation libs.quickjs.kt

// Foldable/Window layout
implementation libs.androidx.window

implementation libs.rxjava
implementation libs.rxandroid

implementation libs.androidx.room.runtime
implementation libs.androidx.room.ktx
ksp libs.androidx.room.compiler
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
Expand Down Expand Up @@ -287,6 +288,9 @@
<activity
android:name="io.github.landwarderer.futon.settings.sources.catalog.SourcesCatalogActivity"
android:label="@string/sources_catalog" />
<activity
android:name="io.github.landwarderer.futon.settings.sources.extension.ExtensionDownloaderActivity"
android:label="@string/extensions_manager" />
<activity
android:name="io.github.landwarderer.futon.scrobbling.kitsu.ui.KitsuAuthActivity"
android:exported="false"
Expand Down
35 changes: 35 additions & 0 deletions app/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package eu.kanade.tachiyomi

import io.github.landwarderer.futon.BuildConfig

/**
* Stub class for Mihon extensions that reference AppInfo.
* Extensions may call these methods for User-Agent strings or version checks.
*
* @since extension-lib 1.3
*/
@Suppress("UNUSED")
object AppInfo {
/**
* Version code of the host application.
*/
fun getVersionCode(): Int = BuildConfig.VERSION_CODE

/**
* Version name of the host application.
*/
fun getVersionName(): String = BuildConfig.VERSION_NAME

/**
* Supported image MIME types by the reader.
*/
fun getSupportedImageMimeTypes(): List<String> = listOf(
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/avif",
"image/heif",
"image/jxl",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.network

import android.content.Context
import com.dokar.quickjs.QuickJs
import kotlinx.coroutines.Dispatchers

/**
* Util for evaluating JavaScript in sources.
*
* Uses QuickJS (with Rhino fallback) to execute JavaScript code.
* This provides compatibility with Mihon extensions that use JavaScriptEngine.
*
* @since extensions-lib 1.4
*/
class JavaScriptEngine(private val context: Context) {

/**
* Evaluate arbitrary JavaScript code and get the result as a primitive type
* (e.g., String, Int).
*
* @param script JavaScript to execute.
* @return Result of JavaScript code as a primitive type.
*/
@Suppress("UNCHECKED_CAST")
suspend fun <T> evaluate(script: String): T {
return QuickJs.create(jobDispatcher = Dispatchers.Default).use { qjs ->
qjs.maxStackSize = 1L shl 20 // 1MB
qjs.memoryLimit = 64L shl 20 // 64MB soft limit
val result = qjs.evaluate<Any?>(script)
result as T
}
}
}
29 changes: 29 additions & 0 deletions app/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.network

import okhttp3.OkHttpClient

/**
* Mihon-compatible NetworkHelper interface.
* Provides access to OkHttpClient for extensions.
*
* This will be implemented by app to bridge with its existing network stack.
*/
abstract class NetworkHelper {

/**
* The default OkHttpClient with CloudFlare bypassing.
*/
abstract val client: OkHttpClient

/**
* @deprecated Since extension-lib 1.5
*/
@Deprecated("The regular client handles Cloudflare by default")
open val cloudflareClient: OkHttpClient
get() = client

/**
* Returns the default user agent string.
*/
abstract fun defaultUserAgentProvider(): String
}
131 changes: 131 additions & 0 deletions app/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.network

import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException

/**
* OkHttp extension functions for Mihon compatibility.
*/

val jsonMime = "application/json; charset=utf-8".toMediaType()

fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber ->
val call = clone()

val requestArbiter = object : Producer, Subscription {
val boolean = AtomicBoolean(false)
override fun request(n: Long) {
if (n == 0L || !boolean.compareAndSet(false, true)) return

try {
val response = call.execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (e: Exception) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(e)
}
}
}

override fun unsubscribe() {
call.cancel()
}

override fun isUnsubscribed(): Boolean {
return call.isCanceled()
}
}

subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
}
}

fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response ->
if (!response.isSuccessful) {
response.close()
throw HttpException(response.code)
}
}
}

suspend fun Call.await(): Response {
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
return suspendCancellableCoroutine { continuation ->
val callback = object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response) {
response.body.close()
}
}

override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return
val exception = IOException(e.message, e).apply { stackTrace = callStack }
continuation.resumeWithException(exception)
}
}

enqueue(callback)

continuation.invokeOnCancellation {
try {
cancel()
} catch (ex: Throwable) {
// Ignore cancel exception
}
}
}
}

/**
* @since extensions-lib 1.5
*/
suspend fun Call.awaitSuccess(): Response {
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
val response = await()
if (!response.isSuccessful) {
response.close()
throw HttpException(response.code).apply { stackTrace = callStack }
}
return response
}

fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
.build()

return progressClient.newCall(request)
}

/**
* Exception that handles HTTP codes considered not successful by OkHttp.
* Use it to have a standardized error message in the app across the extensions.
*
* @since extensions-lib 1.5
* @param code [Int] the HTTP status code
*/
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.network

/**
* Progress listener interface for tracking download progress.
*/
interface ProgressListener {
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.network

import okhttp3.MediaType
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.Source
import okio.buffer

/**
* ResponseBody wrapper that reports download progress.
*/
class ProgressResponseBody(
private val responseBody: ResponseBody,
private val progressListener: ProgressListener,
) : ResponseBody() {

private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer()
}

override fun contentType(): MediaType? {
return responseBody.contentType()
}

override fun contentLength(): Long {
return responseBody.contentLength()
}

override fun source(): BufferedSource {
return bufferedSource
}

private fun source(source: Source): Source {
return object : ForwardingSource(source) {
var totalBytesRead = 0L

override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
progressListener.update(
totalBytesRead,
responseBody.contentLength(),
bytesRead == -1L
)
return bytesRead
}
}
}
}
Loading
Loading