Merge remote-tracking branch 'origin/master' into master

# Conflicts:
#	app/src/main/java/io/legado/app/data/entities/BookChapter.kt
#	app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
#	app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
#	app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt
#	app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
#	app/src/main/java/io/legado/app/utils/HtmlFormatter.kt
pull/1123/head
bushixuanqi 3 years ago
commit 53f9e4ca42
  1. 23
      app/build.gradle
  2. 7
      app/proguard-rules.pro
  3. 1
      app/src/main/assets/help/ruleHelp.md
  4. 10
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  5. 1
      app/src/main/java/io/legado/app/help/AppConfig.kt
  6. 6
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  7. 65
      app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt
  8. 20
      app/src/main/java/io/legado/app/help/http/cronet/CronetInterceptor.kt
  9. 207
      app/src/main/java/io/legado/app/help/http/cronet/CronetUrlRequestCallback.kt
  10. 16
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  11. 30
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  12. 2
      build.gradle

@ -35,6 +35,9 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true
ndk {
abiFilters 'arm64-v8a'
}
javaCompileOptions {
annotationProcessorOptions {
arguments += [
@ -84,6 +87,11 @@ android {
applicationId "io.legado.play"
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
}
cronet {
dimension "mode"
applicationId "io.legado.cronet"
manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"]
}
}
compileOptions {
// Flag to enable support for the new language APIs
@ -115,10 +123,10 @@ kapt {
dependencies {
implementation project(path: ':epublib')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.multidex:multidex:2.0.1'
//kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@ -129,7 +137,7 @@ dependencies {
//androidX
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.core:core-ktx:1.6.0'
implementation "androidx.activity:activity-ktx:1.2.3"
implementation "androidx.fragment:fragment-ktx:1.3.5"
implementation 'androidx.preference:preference-ktx:1.1.1'
@ -170,7 +178,7 @@ dependencies {
//
implementation 'org.jsoup:jsoup:1.14.1'
//noinspection GradleDependency
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'cn.wanghaomiao:JsoupXpath:2.4.3'
implementation 'com.jayway.jsonpath:json-path:2.6.0'
//JS rhino
@ -178,6 +186,11 @@ dependencies {
//
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
def cronet_version = '92.0.4509.1'
compileOnly "org.microg:cronet-api:$cronet_version"
cronetImplementation "org.microg:cronet-native:$cronet_version"
cronetImplementation "org.microg:cronet-api:$cronet_version"
cronetImplementation "org.microg:cronet-common:$cronet_version"
//Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'

@ -222,3 +222,10 @@
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Keep all of Cronet API as it's used by the Cronet module.
-keep public class org.chromium.net.* {
!private *;
*;
}

@ -2,6 +2,7 @@
* [书源帮助文档](https://alanskycn.gitee.io/teachme/Rule/source.html)
* [订阅源帮助文档](https://alanskycn.gitee.io/teachme/Rule/rss.html)
* [js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt)
* 辅助键盘❓中可插入URL参数模板,打开帮助,选择文件
* 规则标志, {{......}}内使用规则必须有明显的规则标志,没有规则标志当作js执行
```

@ -5,14 +5,14 @@ import java.util.regex.Pattern
@Suppress("RegExpRedundantEscape")
object AppPattern {
val JS_PATTERN: Pattern =
Pattern.compile("<js>([\\w\\W]+?)</js>|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE)
Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
//匹配格式化后的图片格式
val imgPattern: Pattern = Pattern.compile("<img src=\"([^>]+)\">")
//图片有data-开头的数据属性时优先用数据属性作为src,没有数据属性时才匹配src
val imgPattern: Pattern =
Pattern.compile("<img(?:(?![^>]*data-)[^>]*src|[^>]*data-)[^=]*= *\"([^\"{]+(?:\\{(?:[^{}]|\\{[^{}]*\\})*\\})?)\"[^>]*>", Pattern.CASE_INSENSITIVE)
val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著")
val authorRegex = Regex("^.*?作\\s*者[::\\s]*|\\s+著")
val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
val splitGroupRegex = Regex("[,;,;]")
}
}

@ -10,6 +10,7 @@ import splitties.init.appCtx
@Suppress("MemberVisibilityCanBePrivate")
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val isGooglePlay = appCtx.channel == "google"
val isCronet= appCtx.channel=="cronet"
var userAgent: String = getPrefUserAgent()
var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2)

@ -1,5 +1,7 @@
package io.legado.app.help.http
import io.legado.app.help.AppConfig
import io.legado.app.help.http.cronet.CronetInterceptor
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ConnectionSpec
import okhttp3.Credentials
@ -41,6 +43,10 @@ val okHttpClient: OkHttpClient by lazy {
.build()
chain.proceed(request)
})
if (AppConfig.isCronet){
builder.addInterceptor(CronetInterceptor())
}
builder.build()
}

@ -0,0 +1,65 @@
package io.legado.app.help.http.cronet
import io.legado.app.help.http.CookieStore
import okhttp3.Headers
import okhttp3.MediaType
import okhttp3.Request
import okio.Buffer
import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK
import org.chromium.net.ExperimentalCronetEngine
import org.chromium.net.UploadDataProviders
import org.chromium.net.UrlRequest
import splitties.init.appCtx
import java.util.concurrent.Executor
import java.util.concurrent.Executors
val executor: Executor by lazy { Executors.newSingleThreadExecutor() }
val cronetEngine: ExperimentalCronetEngine by lazy {
val builder = ExperimentalCronetEngine.Builder(appCtx)
.setStoragePath(appCtx.externalCacheDir?.absolutePath)
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))
.enableQuic(true)
.enablePublicKeyPinningBypassForLocalTrustAnchors(true)
.enableHttp2(true)
//Brotli压缩
builder.enableBrotli(true)
return@lazy builder.build()
}
fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest {
val url = request.url.toString()
val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executor)
requestBuilder.setHttpMethod(request.method)
val cookie = CookieStore.getCookie(url)
if (cookie.length > 1) {
requestBuilder.addHeader("Cookie", cookie)
}
val headers: Headers = request.headers
headers.forEachIndexed { index, pair ->
requestBuilder.addHeader(headers.name(index), headers.value(index))
}
val requestBody = request.body
if (requestBody != null) {
val contentType: MediaType? = requestBody.contentType()
if (contentType != null) {
requestBuilder.addHeader("Content-Type", contentType.toString())
}
val buffer = Buffer()
requestBody.writeTo(buffer)
requestBuilder.setUploadDataProvider(
UploadDataProviders.create(buffer.readByteArray()),
executor
);
}
return requestBuilder.build()
}

@ -0,0 +1,20 @@
package io.legado.app.help.http.cronet
import okhttp3.*
import java.io.IOException
class CronetInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
return proceedWithCronet(chain.request(), chain.call())
}
@Throws(IOException::class)
private fun proceedWithCronet(request: Request, call: Call): Response {
val callback = CronetUrlRequestCallback(request, call)
val urlRequest = buildRequest(request, callback)
urlRequest.start()
return callback.waitForDone()
}
}

@ -0,0 +1,207 @@
package io.legado.app.help.http.cronet
import android.os.ConditionVariable
import android.util.Log
import io.legado.app.help.http.okHttpClient
import okhttp3.*
import okhttp3.EventListener
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.toResponseBody
import org.chromium.net.CronetException
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.util.*
class CronetUrlRequestCallback @JvmOverloads internal constructor(
private val originalRequest: Request,
private val mCall: Call,
eventListener: EventListener? = null,
responseCallback: Callback? = null
) : UrlRequest.Callback() {
private val eventListener: EventListener?
private val responseCallback: Callback?
private var followCount = 0
private var mResponse: Response
private var mException: IOException? = null
private val mResponseCondition = ConditionVariable()
private val mBytesReceived = ByteArrayOutputStream()
private val mReceiveChannel = Channels.newChannel(mBytesReceived)
@Throws(IOException::class)
fun waitForDone(): Response {
mResponseCondition.block()
if (mException != null) {
throw mException as IOException
}
return mResponse
}
override fun onRedirectReceived(
request: UrlRequest,
info: UrlResponseInfo,
newLocationUrl: String
) {
if (followCount > MAX_FOLLOW_COUNT) {
request.cancel()
}
followCount += 1
val client = okHttpClient
if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) {
request.followRedirect()
} else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) {
request.followRedirect()
} else if (client.followRedirects) {
request.followRedirect()
} else {
request.cancel()
}
}
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
mResponse = responseFromResponse(mResponse, info)
// val sb: StringBuilder = StringBuilder(info.url).append("\r\n")
// sb.append("[Cached:").append(info.wasCached()).append("][StatusCode:")
// .append(info.httpStatusCode).append("][StatusText:").append(info.httpStatusText)
// .append("][Protocol:").append(info.negotiatedProtocol).append("][ByteCount:")
// .append(info.receivedByteCount).append("]\r\n");
// val httpHeaders=info.allHeadersAsList
// httpHeaders.forEach { h ->
// sb.append("[").append(h.key).append("]").append(h.value).append("\r\n");
// }
// Log.e("Cronet", sb.toString())
if (eventListener != null) {
eventListener.responseHeadersEnd(mCall, mResponse)
eventListener.responseBodyStart(mCall)
}
request.read(ByteBuffer.allocateDirect(32 * 1024))
}
@Throws(Exception::class)
override fun onReadCompleted(
request: UrlRequest,
info: UrlResponseInfo,
byteBuffer: ByteBuffer
) {
byteBuffer.flip()
try {
mReceiveChannel.write(byteBuffer)
} catch (e: IOException) {
Log.i(TAG, "IOException during ByteBuffer read. Details: ", e)
throw e
}
byteBuffer.clear()
request.read(byteBuffer)
}
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
eventListener?.responseBodyEnd(mCall, info.receivedByteCount)
val contentType: MediaType? = (mResponse.header("content-type")
?: "text/plain; charset=\"utf-8\"").toMediaTypeOrNull()
val responseBody: ResponseBody =
mBytesReceived.toByteArray().toResponseBody(contentType)
val newRequest = originalRequest.newBuilder().url(info.url).build()
mResponse = mResponse.newBuilder().body(responseBody).request(newRequest).build()
mResponseCondition.open()
eventListener?.callEnd(mCall)
if (responseCallback != null) {
try {
responseCallback.onResponse(mCall, mResponse)
} catch (e: IOException) {
// Pass?
}
}
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo, error: CronetException) {
val e = IOException("Cronet Exception Occurred", error)
mException = e
mResponseCondition.open()
eventListener?.callFailed(mCall, e)
responseCallback?.onFailure(mCall, e)
}
override fun onCanceled(request: UrlRequest, info: UrlResponseInfo) {
mResponseCondition.open()
eventListener?.callEnd(mCall)
}
companion object {
private const val TAG = "Callback"
private const val MAX_FOLLOW_COUNT = 20
private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol {
val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault())
// Log.e("Cronet", responseInfo.url)
// Log.e("Cronet", negotiatedProtocol)
return when {
negotiatedProtocol.contains("h3") -> {
return Protocol.QUIC
}
negotiatedProtocol.contains("quic") -> {
Protocol.QUIC
}
negotiatedProtocol.contains("spdy") -> {
Protocol.SPDY_3
}
negotiatedProtocol.contains("h2") -> {
Protocol.HTTP_2
}
negotiatedProtocol.contains("1.1") -> {
Protocol.HTTP_1_1
}
else -> {
Protocol.HTTP_1_0
}
}
}
private fun headersFromResponse(responseInfo: UrlResponseInfo): Headers {
val headers = responseInfo.allHeadersAsList
val headerBuilder = Headers.Builder()
for ((key, value) in headers) {
try {
if (key.equals("content-encoding", ignoreCase = true)) {
// Strip all content encoding headers as decoding is done handled by cronet
continue
}
headerBuilder.add(key, value)
} catch (e: Exception) {
Log.w(TAG, "Invalid HTTP header/value: $key$value")
// Ignore that header
}
}
return headerBuilder.build()
}
private fun responseFromResponse(
response: Response,
responseInfo: UrlResponseInfo
): Response {
val protocol = protocolFromNegotiatedProtocol(responseInfo)
val headers = headersFromResponse(responseInfo)
return response.newBuilder()
.receivedResponseAtMillis(System.currentTimeMillis())
.protocol(protocol)
.code(responseInfo.httpStatusCode)
.message(responseInfo.httpStatusText)
.headers(headers)
.build()
}
}
init {
mResponse = Response.Builder()
.sentRequestAtMillis(System.currentTimeMillis())
.request(originalRequest)
.protocol(Protocol.HTTP_1_0)
.code(0)
.message("")
.build()
this.responseCallback = responseCallback
this.eventListener = eventListener
}
}

@ -139,19 +139,19 @@ class EpubFile(var book: Book) {
/*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/
var tag = Book.hTag
if (book.getDelTag(tag)) {
body.getElementsByTag("h1")?.remove()
body.getElementsByTag("h2")?.remove()
body.getElementsByTag("h3")?.remove()
body.getElementsByTag("h4")?.remove()
body.getElementsByTag("h5")?.remove()
body.getElementsByTag("h6")?.remove()
body.getElementsByTag("h1").remove()
body.getElementsByTag("h2").remove()
body.getElementsByTag("h3").remove()
body.getElementsByTag("h4").remove()
body.getElementsByTag("h5").remove()
body.getElementsByTag("h6").remove()
//body.getElementsMatchingOwnText(chapter.title)?.remove()
}
/*选择去除正文中的img标签,目前图片支持效果待优化*/
tag = Book.imgTag
if (book.getDelTag(tag)) {
body.getElementsByTag("img")?.remove()
body.getElementsByTag("img").remove()
}
val elements = body.children()
@ -204,7 +204,7 @@ class EpubFile(var book: Book) {
val doc =
Jsoup.parse(String(resource.data, mCharset))
val elements = doc.getElementsByTag("title")
if (elements != null && elements.size > 0) {
if (elements.size > 0) {
title = elements[0].text()
}
} catch (e: IOException) {

@ -415,22 +415,30 @@ object ReadBook {
else -> chapter.title
}
val contents = contentProcessor!!.getContent(book, chapter.title, content)
val textChapter = ChapterProvider.getTextChapter(book, chapter,contents,chapterSize)
val offset = chapter.index - durChapterIndex
if (upContent) callBack?.upContent(offset ,resetPageOffset)
when (offset) {
0 -> {
curTextChapter = textChapter
when (chapter.index) {
durChapterIndex -> {
curTextChapter =
ChapterProvider.getTextChapter(
book, chapter, contents, chapterSize
)
if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset)
callBack?.upView()
curPageChanged()
callBack?.contentLoadFinish()
}
- 1 -> {
prevTextChapter = textChapter
durChapterIndex - 1 -> {
prevTextChapter =
ChapterProvider.getTextChapter(
book, chapter, contents, chapterSize
)
if (upContent) callBack?.upContent(-1, resetPageOffset)
}
1 -> {
nextTextChapter = textChapter
durChapterIndex + 1 -> {
nextTextChapter =
ChapterProvider.getTextChapter(
book, chapter, contents, chapterSize
)
if (upContent) callBack?.upContent(1, resetPageOffset)
}
}
}

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.20'
ext.kotlin_version = '1.5.21'
repositories {
google()
mavenCentral()

Loading…
Cancel
Save