更新依赖

pull/21/head
fengyuecanzhu 3 years ago
parent acaed246cc
commit 055796fceb
  1. 31
      app/build.gradle
  2. 7
      app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
  3. 2
      app/src/main/java/xyz/fycz/myreader/entity/sourceedit/EditEntityUtil.kt
  4. 14
      app/src/main/java/xyz/fycz/myreader/greendao/entity/Book.java
  5. 54
      app/src/main/java/xyz/fycz/myreader/greendao/entity/Cache.java
  6. 70
      app/src/main/java/xyz/fycz/myreader/greendao/entity/Chapter.java
  7. 21
      app/src/main/java/xyz/fycz/myreader/greendao/entity/rule/BookSource.java
  8. 83
      app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt
  9. 31
      app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeRule.kt
  10. 48
      app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeUrl.kt
  11. 39
      app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt
  12. 2
      app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/RuleDataInterface.kt
  13. 4
      app/src/main/java/xyz/fycz/myreader/model/third3/http/BackstageWebView.kt
  14. 91
      app/src/main/java/xyz/fycz/myreader/model/third3/http/HttpHelper.kt
  15. 23
      app/src/main/java/xyz/fycz/myreader/model/third3/http/OkHttpUtils.kt
  16. 2
      app/src/main/java/xyz/fycz/myreader/model/third3/http/RequestMethod.kt
  17. 2
      app/src/main/java/xyz/fycz/myreader/model/third3/http/SSLHelper.kt
  18. 2
      app/src/main/java/xyz/fycz/myreader/model/third3/http/StrResponse.kt
  19. 1
      app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookContent.kt
  20. 5
      app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookInfo.kt
  21. 1
      app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookList.kt
  22. 11
      app/src/main/java/xyz/fycz/myreader/model/third3/webBook/WebBook.kt
  23. 785
      app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt
  24. 19
      app/src/main/java/xyz/fycz/myreader/util/utils/EncoderUtils.kt
  25. 26
      app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java
  26. 62
      app/src/main/java/xyz/fycz/myreader/util/utils/HtmlFormatter.kt
  27. 176
      app/src/main/java/xyz/fycz/myreader/util/utils/RealPathUtil.kt
  28. 81
      app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt
  29. 28
      app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java
  30. 1
      app/src/main/res/values/strings.xml
  31. 5
      build.gradle

@ -148,12 +148,12 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar']) implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(path: ':DialogX') implementation project(path: ':DialogX')
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
api 'androidx.core:core-ktx:1.3.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//anko //anko
def anko_version = '0.10.8' def anko_version = '0.10.8'
@ -161,12 +161,11 @@ dependencies {
implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.8.0' implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.7' implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.journeyapps:zxing-android-embedded:3.5.0' implementation 'com.journeyapps:zxing-android-embedded:3.5.0'
@ -174,9 +173,9 @@ dependencies {
implementation 'com.github.yuweiguocn:GreenDaoUpgradeHelper:v2.2.1' implementation 'com.github.yuweiguocn:GreenDaoUpgradeHelper:v2.2.1'
//JSoup //JSoup
implementation 'org.jsoup:jsoup:1.11.3' implementation 'org.jsoup:jsoup:1.14.3'
implementation 'cn.wanghaomiao:JsoupXpath:2.4.3' implementation 'cn.wanghaomiao:JsoupXpath:2.5.0'
implementation 'com.jayway.jsonpath:json-path:2.5.0' implementation 'com.jayway.jsonpath:json-path:2.6.0'
//SmartRefreshLayout //SmartRefreshLayout
implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2' implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2'
@ -187,9 +186,9 @@ dependencies {
implementation 'uk.co.chrisjenx:calligraphy:2.3.0' implementation 'uk.co.chrisjenx:calligraphy:2.3.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.4.0'
//Scroller //Scroller
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5' implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
@ -200,7 +199,7 @@ dependencies {
// //
implementation 'net.ricecode:string-similarity:1.0.0' implementation 'net.ricecode:string-similarity:1.0.0'
implementation 'com.jayway.jsonpath:json-path:2.4.0' implementation 'com.jayway.jsonpath:json-path:2.6.0'
//RxAndroid //RxAndroid
implementation 'io.reactivex.rxjava2:rxjava:2.2.19' implementation 'io.reactivex.rxjava2:rxjava:2.2.19'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
@ -257,7 +256,7 @@ dependencies {
} }
greendao { greendao {
schemaVersion 32 schemaVersion 33
daoPackage 'xyz.fycz.myreader.greendao.gen' daoPackage 'xyz.fycz.myreader.greendao.gen'
// targetGenDir 'src/main/java' // targetGenDir 'src/main/java'
} }

@ -1,6 +1,7 @@
package xyz.fycz.myreader.common; package xyz.fycz.myreader.common;
import android.os.Environment; import android.os.Environment;
import android.provider.Settings;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.hjq.permissions.Permission; import com.hjq.permissions.Permission;
@ -133,4 +134,10 @@ public class APPCONST {
public static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4168.3 Safari/537.36"; public static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4168.3 Safari/537.36";
public static final String UA_NAME = "User-Agent"; public static final String UA_NAME = "User-Agent";
public static final String androidId = getAndroidId();
public static String getAndroidId(){
return Settings.System.getString(App.getmContext().getContentResolver(), Settings.Secure.ANDROID_ID);
}
} }

@ -34,6 +34,7 @@ object EditEntityUtil {
) )
add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url, "")) add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url, ""))
add(EditEntity("sourceComment", source?.sourceComment, R.string.comment, "这是您留给使用者的说明")) add(EditEntity("sourceComment", source?.sourceComment, R.string.comment, "这是您留给使用者的说明"))
add(EditEntity("concurrentRate", source?.concurrentRate, R.string.comment, ""))
} }
return sourceEntities return sourceEntities
} }
@ -195,6 +196,7 @@ object EditEntityUtil {
"sourceHeaders" -> source.sourceHeaders = it.value "sourceHeaders" -> source.sourceHeaders = it.value
"loginUrl" -> source.loginUrl = it.value "loginUrl" -> source.loginUrl = it.value
"sourceComment" -> source.sourceComment = it.value "sourceComment" -> source.sourceComment = it.value
"concurrentRate" -> source.concurrentRate = it.value
} }
} }
return source return source

@ -3,6 +3,7 @@ package xyz.fycz.myreader.greendao.entity;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -13,6 +14,7 @@ import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.Transient; import org.greenrobot.greendao.annotation.Transient;
import xyz.fycz.myreader.greendao.service.BookService; import xyz.fycz.myreader.greendao.service.BookService;
import xyz.fycz.myreader.model.third3.analyzeRule.RuleDataInterface;
import xyz.fycz.myreader.util.SharedPreUtils; import xyz.fycz.myreader.util.SharedPreUtils;
import java.io.Serializable; import java.io.Serializable;
@ -23,7 +25,7 @@ import java.util.Objects;
import static xyz.fycz.myreader.common.APPCONST.MAP_STRING; import static xyz.fycz.myreader.common.APPCONST.MAP_STRING;
@Entity @Entity
public class Book implements Serializable { public class Book implements Serializable, RuleDataInterface {
@Transient @Transient
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -378,7 +380,7 @@ public class Book implements Serializable {
this.status = status; this.status = status;
} }
public void putVariable(String key, String value) { public void putVariable(@NonNull String key, String value) {
if (variableMap == null) { if (variableMap == null) {
variableMap = new HashMap<>(); variableMap = new HashMap<>();
} }
@ -386,6 +388,7 @@ public class Book implements Serializable {
variable = new Gson().toJson(variableMap); variable = new Gson().toJson(variableMap);
} }
@NonNull
public Map<String, String> getVariableMap() { public Map<String, String> getVariableMap() {
if (variableMap == null && !TextUtils.isEmpty(variable)) { if (variableMap == null && !TextUtils.isEmpty(variable)) {
variableMap = new Gson().fromJson(variable, MAP_STRING); variableMap = new Gson().fromJson(variable, MAP_STRING);
@ -398,7 +401,6 @@ public class Book implements Serializable {
return this.variable; return this.variable;
} }
public void setVariable(String variable) { public void setVariable(String variable) {
this.variable = variable; this.variable = variable;
} }
@ -430,4 +432,10 @@ public class Book implements Serializable {
public void setReSeg(boolean reSeg) { public void setReSeg(boolean reSeg) {
this.reSeg = reSeg; this.reSeg = reSeg;
} }
@Nullable
@Override
public String getVariable(@NonNull String key) {
return variableMap.get(key);
}
} }

@ -0,0 +1,54 @@
package xyz.fycz.myreader.greendao.entity;
import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.Generated;
/**
* @author fengyue
* @date 2022/1/18 10:20
*/
@Entity
public class Cache {
@Id
private String key;
private String value;
private long deadLine;
@Generated(hash = 1252535078)
public Cache(String key, String value, long deadLine) {
this.key = key;
this.value = value;
this.deadLine = deadLine;
}
@Generated(hash = 1305017356)
public Cache() {
}
public String getKey() {
return this.key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return this.value;
}
public void setValue(String value) {
this.value = value;
}
public long getDeadLine() {
return this.deadLine;
}
public void setDeadLine(long deadLine) {
this.deadLine = deadLine;
}
}

@ -1,14 +1,27 @@
package xyz.fycz.myreader.greendao.entity; package xyz.fycz.myreader.greendao.entity;
import static xyz.fycz.myreader.common.APPCONST.MAP_STRING;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.gson.Gson;
import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Generated;
import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.Transient;
import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.model.third3.analyzeRule.RuleDataInterface;
import xyz.fycz.myreader.util.utils.FileUtils; import xyz.fycz.myreader.util.utils.FileUtils;
import java.io.File; import java.io.File;
import java.util.HashMap;
import java.util.Map;
/** /**
* 章节 * 章节
@ -16,7 +29,7 @@ import java.io.File;
*/ */
@Entity @Entity
public class Chapter { public class Chapter implements RuleDataInterface {
@Id @Id
private String id; private String id;
@ -32,10 +45,13 @@ public class Chapter {
//章节内容在文章中的终止位置(本地) //章节内容在文章中的终止位置(本地)
private long end; private long end;
private String variable;
@Transient
private Map<String, String> variableMap;
@Generated(hash = 763230955) @Generated(hash = 1398484308)
public Chapter(String id, String bookId, int number, String title, String url, public Chapter(String id, String bookId, int number, String title, String url,
String content, long start, long end) { String content, long start, long end, String variable) {
this.id = id; this.id = id;
this.bookId = bookId; this.bookId = bookId;
this.number = number; this.number = number;
@ -44,52 +60,66 @@ public class Chapter {
this.content = content; this.content = content;
this.start = start; this.start = start;
this.end = end; this.end = end;
this.variable = variable;
} }
@Generated(hash = 393170288) @Generated(hash = 393170288)
public Chapter() { public Chapter() {
} }
public String getId() { public String getId() {
return this.id; return this.id;
} }
public void setId(String id) { public void setId(String id) {
this.id = id; this.id = id;
} }
public String getBookId() { public String getBookId() {
return this.bookId; return this.bookId;
} }
public void setBookId(String bookId) { public void setBookId(String bookId) {
this.bookId = bookId; this.bookId = bookId;
} }
public int getNumber() { public int getNumber() {
return this.number; return this.number;
} }
public void setNumber(int number) { public void setNumber(int number) {
this.number = number; this.number = number;
} }
public String getTitle() { public String getTitle() {
return this.title; return this.title;
} }
public void setTitle(String title) { public void setTitle(String title) {
this.title = title; this.title = title;
} }
public String getUrl() { public String getUrl() {
return this.url; return this.url;
} }
public void setUrl(String url) { public void setUrl(String url) {
this.url = url; this.url = url;
} }
public String getContent() { public String getContent() {
if (end > 0) return end + ""; if (end > 0) return end + "";
String filePath = APPCONST.BOOK_CACHE_PATH + bookId String filePath = APPCONST.BOOK_CACHE_PATH + bookId
+ File.separator + title + FileUtils.SUFFIX_FY; + File.separator + title + FileUtils.SUFFIX_FY;
File file = new File(filePath); File file = new File(filePath);
if (file.exists() && file.length() > 0){ if (file.exists() && file.length() > 0) {
this.content = filePath; this.content = filePath;
}else { } else {
this.content = null; this.content = null;
} }
return this.content; return this.content;
} }
public void setContent(String content) { public void setContent(String content) {
this.content = content; this.content = content;
} }
@ -109,4 +139,34 @@ public class Chapter {
public void setEnd(long end) { public void setEnd(long end) {
this.end = end; this.end = end;
} }
public void putVariable(String key, String value) {
if (variableMap == null) {
variableMap = new HashMap<>();
}
variableMap.put(key, value);
variable = new Gson().toJson(variableMap);
}
@NonNull
public Map<String, String> getVariableMap() {
if (variableMap == null && !TextUtils.isEmpty(variable)) {
variableMap = new Gson().fromJson(variable, MAP_STRING);
}
return variableMap;
}
@Nullable
@Override
public String getVariable(@NonNull String key) {
return variableMap.get(key);
}
public String getVariable() {
return this.variable;
}
public void setVariable(String variable) {
this.variable = variable;
}
} }

@ -44,6 +44,7 @@ public class BookSource implements Parcelable, Cloneable {
private String sourceHeaders; private String sourceHeaders;
private String loginUrl; private String loginUrl;
private String sourceComment; private String sourceComment;
private String concurrentRate;
private Long lastUpdateTime; private Long lastUpdateTime;
@OrderBy @OrderBy
@ -72,12 +73,12 @@ public class BookSource implements Parcelable, Cloneable {
@Convert(converter = FindRuleConvert.class, columnType = String.class) @Convert(converter = FindRuleConvert.class, columnType = String.class)
private FindRule findRule; private FindRule findRule;
@Generated(hash = 277037260) @Generated(hash = 906128088)
public BookSource(String sourceUrl, String sourceEName, String sourceName, String sourceGroup, public BookSource(String sourceUrl, String sourceEName, String sourceName, String sourceGroup,
String sourceCharset, String sourceType, String sourceHeaders, String loginUrl, String sourceCharset, String sourceType, String sourceHeaders, String loginUrl,
String sourceComment, Long lastUpdateTime, int orderNum, int weight, boolean enable, String sourceComment, String concurrentRate, Long lastUpdateTime, int orderNum, int weight,
SearchRule searchRule, InfoRule infoRule, TocRule tocRule, ContentRule contentRule, boolean enable, SearchRule searchRule, InfoRule infoRule, TocRule tocRule,
FindRule findRule) { ContentRule contentRule, FindRule findRule) {
this.sourceUrl = sourceUrl; this.sourceUrl = sourceUrl;
this.sourceEName = sourceEName; this.sourceEName = sourceEName;
this.sourceName = sourceName; this.sourceName = sourceName;
@ -87,6 +88,7 @@ public class BookSource implements Parcelable, Cloneable {
this.sourceHeaders = sourceHeaders; this.sourceHeaders = sourceHeaders;
this.loginUrl = loginUrl; this.loginUrl = loginUrl;
this.sourceComment = sourceComment; this.sourceComment = sourceComment;
this.concurrentRate = concurrentRate;
this.lastUpdateTime = lastUpdateTime; this.lastUpdateTime = lastUpdateTime;
this.orderNum = orderNum; this.orderNum = orderNum;
this.weight = weight; this.weight = weight;
@ -113,6 +115,7 @@ public class BookSource implements Parcelable, Cloneable {
sourceHeaders = in.readString(); sourceHeaders = in.readString();
loginUrl = in.readString(); loginUrl = in.readString();
sourceComment = in.readString(); sourceComment = in.readString();
concurrentRate = in.readString();
if (in.readByte() == 0) { if (in.readByte() == 0) {
lastUpdateTime = null; lastUpdateTime = null;
} else { } else {
@ -139,6 +142,7 @@ public class BookSource implements Parcelable, Cloneable {
dest.writeString(sourceHeaders); dest.writeString(sourceHeaders);
dest.writeString(loginUrl); dest.writeString(loginUrl);
dest.writeString(sourceComment); dest.writeString(sourceComment);
dest.writeString(concurrentRate);
if (lastUpdateTime == null) { if (lastUpdateTime == null) {
dest.writeByte((byte) 0); dest.writeByte((byte) 0);
} else { } else {
@ -187,6 +191,7 @@ public class BookSource implements Parcelable, Cloneable {
stringEquals(sourceHeaders, source.sourceHeaders) && stringEquals(sourceHeaders, source.sourceHeaders) &&
stringEquals(loginUrl, source.loginUrl) && stringEquals(loginUrl, source.loginUrl) &&
stringEquals(sourceComment, source.sourceComment) && stringEquals(sourceComment, source.sourceComment) &&
stringEquals(concurrentRate, source.concurrentRate) &&
Objects.equals(searchRule, source.searchRule) && Objects.equals(searchRule, source.searchRule) &&
Objects.equals(infoRule, source.infoRule) && Objects.equals(infoRule, source.infoRule) &&
Objects.equals(tocRule, source.tocRule) && Objects.equals(tocRule, source.tocRule) &&
@ -396,4 +401,12 @@ public class BookSource implements Parcelable, Cloneable {
public void setLoginUrl(String loginUrl) { public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl; this.loginUrl = loginUrl;
} }
public String getConcurrentRate() {
return this.concurrentRate;
}
public void setConcurrentRate(String concurrentRate) {
this.concurrentRate = concurrentRate;
}
} }

@ -0,0 +1,83 @@
package xyz.fycz.myreader.greendao.service
import android.database.Cursor
import xyz.fycz.myreader.application.App
import xyz.fycz.myreader.greendao.DbManager
import xyz.fycz.myreader.greendao.entity.Cache
import xyz.fycz.myreader.model.third3.analyzeRule.QueryTTF
import xyz.fycz.myreader.util.utils.ACache
import java.lang.Exception
@Suppress("unused")
object CacheManager {
private val queryTTFMap = hashMapOf<String, Pair<Long, QueryTTF>>()
/**
* saveTime 单位为秒
*/
@JvmOverloads
fun put(key: String, value: Any, saveTime: Int = 0) {
val deadline =
if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000
when (value) {
is QueryTTF -> queryTTFMap[key] = Pair(deadline, value)
is ByteArray -> ACache.get(App.getmContext()).put(key, value, saveTime)
else -> {
val cache = Cache(key, value.toString(), deadline)
DbManager.getDaoSession().cacheDao.insertOrReplace(cache)
}
}
}
fun get(key: String): String? {
var str: String? = null
try {
val sql = "select VALUE from CACHE where key = ? and (DEAD_LINE = 0 or DEAD_LINE > ?)"
val cursor: Cursor = DbManager.getDaoSession().database.rawQuery(
sql,
arrayOf(key, "" + System.currentTimeMillis())
) ?: return null
if (cursor.moveToNext()) {
str = cursor.getColumnName(0)
}
} catch (e: Exception) {
e.printStackTrace()
}
return str
}
fun getInt(key: String): Int? {
return get(key)?.toIntOrNull()
}
fun getLong(key: String): Long? {
return get(key)?.toLongOrNull()
}
fun getDouble(key: String): Double? {
return get(key)?.toDoubleOrNull()
}
fun getFloat(key: String): Float? {
return get(key)?.toFloatOrNull()
}
fun getByteArray(key: String): ByteArray? {
return ACache.get(App.getmContext()).getAsBinary(key)
}
fun getQueryTTF(key: String): QueryTTF? {
val cache = queryTTFMap[key] ?: return null
if (cache.first == 0L || cache.first > System.currentTimeMillis()) {
return cache.second
}
return null
}
fun delete(key: String) {
DbManager.getDaoSession().cacheDao.deleteByKey(key)
ACache.get(App.getmContext()).remove(key)
}
}

@ -1,20 +1,19 @@
package xyz.fycz.myreader.model.third3.analyzeRule package xyz.fycz.myreader.model.third3.analyzeRule
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.constant.AppConst.SCRIPT_ENGINE
import io.legado.app.constant.AppPattern.JS_PATTERN
import io.legado.app.data.entities.BaseBook
import io.legado.app.data.entities.BaseSource
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.CacheManager
import io.legado.app.help.JsExtensions
import io.legado.app.help.http.CookieStore
import io.legado.app.utils.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jsoup.nodes.Entities import org.jsoup.nodes.Entities
import org.mozilla.javascript.NativeObject import org.mozilla.javascript.NativeObject
import timber.log.Timber import xyz.fycz.myreader.common.APPCONST.JS_PATTERN
import xyz.fycz.myreader.common.APPCONST.SCRIPT_ENGINE
import xyz.fycz.myreader.greendao.entity.Book
import xyz.fycz.myreader.greendao.entity.Chapter
import xyz.fycz.myreader.greendao.entity.rule.BookSource
import xyz.fycz.myreader.greendao.service.CacheManager
import xyz.fycz.myreader.greendao.service.CookieStore
import xyz.fycz.myreader.util.utils.*
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@ -28,12 +27,12 @@ import kotlin.collections.HashMap
@Suppress("unused", "RegExpRedundantEscape", "MemberVisibilityCanBePrivate") @Suppress("unused", "RegExpRedundantEscape", "MemberVisibilityCanBePrivate")
class AnalyzeRule( class AnalyzeRule(
val ruleData: RuleDataInterface, val ruleData: RuleDataInterface,
private val source: BaseSource? = null private val source: BookSource? = null
) : JsExtensions { ) : JsExtensions {
var book = if (ruleData is BaseBook) ruleData else null var book = if (ruleData is Book) ruleData else null
var chapter: BookChapter? = null var chapter: Chapter? = null
var nextChapterUrl: String? = null var nextChapterUrl: String? = null
var content: Any? = null var content: Any? = null
private set private set
@ -651,7 +650,7 @@ class AnalyzeRule(
return SCRIPT_ENGINE.eval(jsStr, bindings) return SCRIPT_ENGINE.eval(jsStr, bindings)
} }
override fun getSource(): BaseSource? { override fun getSource(): BookSource? {
return source return source
} }
@ -665,9 +664,9 @@ class AnalyzeRule(
analyzeUrl.getStrResponseAwait().body analyzeUrl.getStrResponseAwait().body
}.onFailure { }.onFailure {
log("ajax(${urlStr}) error\n${it.stackTraceToString()}") log("ajax(${urlStr}) error\n${it.stackTraceToString()}")
Timber.e(it) Log.e(TAG, it.toString())
}.getOrElse { }.getOrElse {
it.msg it.message
} }
} }
} }

@ -4,20 +4,18 @@ import android.annotation.SuppressLint
import androidx.annotation.Keep import androidx.annotation.Keep
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders import com.bumptech.glide.load.model.LazyHeaders
import io.legado.app.constant.AppConst.SCRIPT_ENGINE
import io.legado.app.constant.AppConst.UA_NAME
import io.legado.app.constant.AppPattern.JS_PATTERN
import io.legado.app.data.entities.BaseSource
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.CacheManager
import io.legado.app.help.JsExtensions
import io.legado.app.help.http.*
import io.legado.app.model.ConcurrentException
import io.legado.app.utils.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Response import okhttp3.Response
import xyz.fycz.myreader.common.APPCONST.*
import xyz.fycz.myreader.greendao.entity.Book
import xyz.fycz.myreader.greendao.entity.Chapter
import xyz.fycz.myreader.greendao.entity.rule.BookSource
import xyz.fycz.myreader.greendao.service.CacheManager
import xyz.fycz.myreader.greendao.service.CookieStore
import xyz.fycz.myreader.model.third3.ConcurrentException
import xyz.fycz.myreader.model.third3.http.*
import xyz.fycz.myreader.util.utils.*
import xyz.fycz.myreader.util.utils.UrlEncoderUtils
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@ -38,9 +36,9 @@ class AnalyzeUrl(
val speakText: String? = null, val speakText: String? = null,
val speakSpeed: Int? = null, val speakSpeed: Int? = null,
var baseUrl: String = "", var baseUrl: String = "",
private val source: BaseSource? = null, private val source: BookSource? = null,
private val ruleData: RuleDataInterface? = null, private val ruleData: RuleDataInterface? = null,
private val chapter: BookChapter? = null, private val chapter: Chapter? = null,
headerMapF: Map<String, String>? = null, headerMapF: Map<String, String>? = null,
) : JsExtensions { ) : JsExtensions {
companion object { companion object {
@ -195,7 +193,7 @@ class AnalyzeUrl(
} }
} }
headerMap[UA_NAME] ?: let { headerMap[UA_NAME] ?: let {
headerMap[UA_NAME] = AppConfig.userAgent headerMap[UA_NAME] = DEFAULT_USER_AGENT
} }
urlNoQuery = url urlNoQuery = url
when (method) { when (method) {
@ -224,7 +222,7 @@ class AnalyzeUrl(
val queryM = query.splitNotBlank("=") val queryM = query.splitNotBlank("=")
val value = if (queryM.size > 1) queryM[1] else "" val value = if (queryM.size > 1) queryM[1] else ""
if (charset.isNullOrEmpty()) { if (charset.isNullOrEmpty()) {
if (NetworkUtils.hasUrlEncoded(value)) { if (UrlEncoderUtils.hasUrlEncoded(value)) {
fieldMap[queryM[0]] = value fieldMap[queryM[0]] = value
} else { } else {
fieldMap[queryM[0]] = URLEncoder.encode(value, "UTF-8") fieldMap[queryM[0]] = URLEncoder.encode(value, "UTF-8")
@ -286,10 +284,10 @@ class AnalyzeUrl(
return null return null
} }
val rateIndex = concurrentRate.indexOf("/") val rateIndex = concurrentRate.indexOf("/")
var fetchRecord = concurrentRecordMap[source.getKey()] var fetchRecord = concurrentRecordMap[source.sourceUrl]
if (fetchRecord == null) { if (fetchRecord == null) {
fetchRecord = ConcurrentRecord(rateIndex > 0, System.currentTimeMillis(), 1) fetchRecord = ConcurrentRecord(rateIndex > 0, System.currentTimeMillis(), 1)
concurrentRecordMap[source.getKey()] = fetchRecord concurrentRecordMap[source.sourceUrl] = fetchRecord
return fetchRecord return fetchRecord
} }
val waitTime: Int = synchronized(fetchRecord) { val waitTime: Int = synchronized(fetchRecord) {
@ -354,7 +352,7 @@ class AnalyzeUrl(
return StrResponse(url, StringUtils.byteToHexString(getByteArrayAwait())) return StrResponse(url, StringUtils.byteToHexString(getByteArrayAwait()))
} }
val concurrentRecord = fetchStart() val concurrentRecord = fetchStart()
setCookie(source?.getKey()) setCookie(source?.sourceUrl)
val strResponse: StrResponse val strResponse: StrResponse
if (this.useWebView && useWebView) { if (this.useWebView && useWebView) {
strResponse = when (method) { strResponse = when (method) {
@ -371,7 +369,7 @@ class AnalyzeUrl(
BackstageWebView( BackstageWebView(
url = url, url = url,
html = body, html = body,
tag = source?.getKey(), tag = source?.sourceUrl,
javaScript = webJs ?: jsStr, javaScript = webJs ?: jsStr,
sourceRegex = sourceRegex, sourceRegex = sourceRegex,
headerMap = headerMap headerMap = headerMap
@ -379,7 +377,7 @@ class AnalyzeUrl(
} }
else -> BackstageWebView( else -> BackstageWebView(
url = url, url = url,
tag = source?.getKey(), tag = source?.sourceUrl,
javaScript = webJs ?: jsStr, javaScript = webJs ?: jsStr,
sourceRegex = sourceRegex, sourceRegex = sourceRegex,
headerMap = headerMap headerMap = headerMap
@ -421,7 +419,7 @@ class AnalyzeUrl(
*/ */
suspend fun getResponseAwait(): Response { suspend fun getResponseAwait(): Response {
val concurrentRecord = fetchStart() val concurrentRecord = fetchStart()
setCookie(source?.getKey()) setCookie(source?.sourceUrl)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
val response = getProxyClient(proxy).newCallResponse(retry) { val response = getProxyClient(proxy).newCallResponse(retry) {
addHeaders(headerMap) addHeaders(headerMap)
@ -452,7 +450,7 @@ class AnalyzeUrl(
*/ */
suspend fun getByteArrayAwait(): ByteArray { suspend fun getByteArrayAwait(): ByteArray {
val concurrentRecord = fetchStart() val concurrentRecord = fetchStart()
setCookie(source?.getKey()) setCookie(source?.sourceUrl)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
val byteArray = getProxyClient(proxy).newCallResponseBody(retry) { val byteArray = getProxyClient(proxy).newCallResponseBody(retry) {
addHeaders(headerMap) addHeaders(headerMap)
@ -522,14 +520,14 @@ class AnalyzeUrl(
} }
fun getUserAgent(): String { fun getUserAgent(): String {
return headerMap[UA_NAME] ?: AppConfig.userAgent return headerMap[UA_NAME] ?: DEFAULT_USER_AGENT
} }
fun isPost(): Boolean { fun isPost(): Boolean {
return method == RequestMethod.POST return method == RequestMethod.POST
} }
override fun getSource(): BaseSource? { override fun getSource(): BookSource? {
return source return source
} }

@ -4,15 +4,18 @@ import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.help.http.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import nl.siegmann.epublib.epub.PackageDocumentBase.dateFormat import nl.siegmann.epublib.epub.PackageDocumentBase.dateFormat
import org.jsoup.Connection import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import xyz.fycz.myreader.application.App
import xyz.fycz.myreader.common.APPCONST
import xyz.fycz.myreader.greendao.entity.rule.BookSource import xyz.fycz.myreader.greendao.entity.rule.BookSource
import xyz.fycz.myreader.greendao.service.CacheManager
import xyz.fycz.myreader.greendao.service.CookieStore import xyz.fycz.myreader.greendao.service.CookieStore
import xyz.fycz.myreader.model.third3.http.*
import xyz.fycz.myreader.util.ZipUtils import xyz.fycz.myreader.util.ZipUtils
import xyz.fycz.myreader.util.utils.* import xyz.fycz.myreader.util.utils.*
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -34,7 +37,7 @@ import java.util.zip.ZipInputStream
@Suppress("unused") @Suppress("unused")
interface JsExtensions { interface JsExtensions {
private val TAG: String? val TAG: String?
get() = JsExtensions::class.simpleName get() = JsExtensions::class.simpleName
fun getSource(): BookSource? fun getSource(): BookSource?
@ -271,7 +274,8 @@ interface JsExtensions {
} }
fun htmlFormat(str: String): String { fun htmlFormat(str: String): String {
return HtmlFormatter.formatKeepImg(str) return HtmlFormatter.format(str)
// return HtmlFormatter.formatKeepImg(str)
} }
//****************文件操作******************// //****************文件操作******************//
@ -282,7 +286,7 @@ interface JsExtensions {
* @return File * @return File
*/ */
fun getFile(path: String): File { fun getFile(path: String): File {
val cachePath = appCtx.externalCache.absolutePath val cachePath = FileUtils.getCachePath()
val aPath = if (path.startsWith(File.separator)) { val aPath = if (path.startsWith(File.separator)) {
cachePath + path cachePath + path
} else { } else {
@ -330,13 +334,16 @@ interface JsExtensions {
*/ */
fun unzipFile(zipPath: String): String { fun unzipFile(zipPath: String): String {
if (zipPath.isEmpty()) return "" if (zipPath.isEmpty()) return ""
val unzipPath = FileUtils.getCachePath() + File.separator + FileUtils.getNameExcludeExtension(zipPath) val unzipPath = FileUtils.getPath(
FileUtils.getFile(FileUtils.getCachePath()),
FileUtils.getNameExcludeExtension(zipPath)
)
FileUtils.deleteFile(unzipPath) FileUtils.deleteFile(unzipPath)
val zipFile = FileUtils.getFile(zipPath) val zipFile = getFile(zipPath)
val unzipFolder = FileUtils.getFolder(unzipPath) val unzipFolder = FileUtils.getFile(unzipPath)
ZipUtils.unzipFile(zipFile, unzipFolder) ZipUtils.unzipFile(zipFile, unzipFolder)
FileUtils.deleteFile(zipPath) FileUtils.deleteFile(zipFile.absolutePath)
return unzipPath return unzipPath.substring(FileUtils.getCachePath().length)
} }
/** /**
@ -437,7 +444,7 @@ interface JsExtensions {
} }
return@runBlocking x return@runBlocking x
} }
str.isContentScheme() -> Uri.parse(str).readBytes(appCtx) str.isContentScheme() -> DocumentUtil.readBytes(App.getmContext(), Uri.parse(str))
str.startsWith("/storage") -> File(str).readBytes() str.startsWith("/storage") -> File(str).readBytes()
else -> base64DecodeToByteArray(str) else -> base64DecodeToByteArray(str)
} }
@ -476,10 +483,10 @@ interface JsExtensions {
* 输出调试日志 * 输出调试日志
*/ */
fun log(msg: String): String { fun log(msg: String): String {
getSource()?.let { /*getSource()?.let {
Debug.log(it.getKey(), msg) Debug.log(it.sourceUrl, msg)
} ?: Debug.log(msg) } ?: Debug.log(msg)*/
if (BuildConfig.DEBUG) { if (App.isDebug()) {
Log.e(TAG, msg) Log.e(TAG, msg)
} }
return msg return msg
@ -549,7 +556,7 @@ interface JsExtensions {
iv.encodeToByteArray() iv.encodeToByteArray()
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, r.toString()) Log.e(TAG, e.toString())
log(e.localizedMessage ?: "aesDecodeToByteArrayERROR") log(e.localizedMessage ?: "aesDecodeToByteArrayERROR")
null null
} }
@ -644,7 +651,7 @@ interface JsExtensions {
} }
fun android(): String { fun android(): String {
return AppConst.androidId return APPCONST.androidId
} }
} }

@ -2,7 +2,7 @@ package xyz.fycz.myreader.model.third3.analyzeRule
interface RuleDataInterface { interface RuleDataInterface {
val variableMap: HashMap<String, String> val variableMap: Map<String, String>
fun putVariable(key: String, value: String?) fun putVariable(key: String, value: String?)

@ -1,4 +1,4 @@
package io.legado.app.help.http package xyz.fycz.myreader.model.third3.http
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Handler import android.os.Handler
@ -40,7 +40,7 @@ class BackstageWebView(
destroy() destroy()
} }
} }
callback = object : BackstageWebView.Callback() { callback = object : Callback() {
override fun onResult(response: StrResponse) { override fun onResult(response: StrResponse) {
if (!block.isCompleted) if (!block.isCompleted)
block.resume(response) block.resume(response)

@ -0,0 +1,91 @@
package xyz.fycz.myreader.model.third3.http
import okhttp3.ConnectionSpec
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
ConcurrentHashMap()
}
val okHttpClient: OkHttpClient by lazy {
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.callTimeout(60,TimeUnit.SECONDS)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager)
.retryOnConnectionFailure(true)
.hostnameVerifier(SSLHelper.unsafeHostnameVerifier)
.connectionSpecs(specs)
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(Interceptor { chain ->
val request = chain.request()
.newBuilder()
.addHeader("Keep-Alive", "300")
.addHeader("Connection", "Keep-Alive")
.addHeader("Cache-Control", "no-cache")
.build()
chain.proceed(request)
})
/*if (AppConfig.isCronet && CronetLoader.install() && !AppConfig.isGooglePlay) {
builder.addInterceptor(CronetInterceptor(null))
}*/
builder.build()
}
/**
* 缓存代理okHttp
*/
fun getProxyClient(proxy: String? = null): OkHttpClient {
if (proxy.isNullOrBlank()) {
return okHttpClient
}
proxyClientCache[proxy]?.let {
return it
}
val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?")
val ms = r.findAll(proxy)
val group = ms.first()
var username = "" //代理服务器验证用户名
var password = "" //代理服务器验证密码
val type = if (group.groupValues[1] == "http") "http" else "socks"
val host = group.groupValues[2]
val port = group.groupValues[3].toInt()
if (group.groupValues[4] != "") {
username = group.groupValues[4].split("@")[1]
password = group.groupValues[4].split("@")[2]
}
if (type != "direct" && host != "") {
val builder = okHttpClient.newBuilder()
if (type == "http") {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))
} else {
builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)))
}
if (username != "" && password != "") {
builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码
val credential: String = Credentials.basic(username, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
}
val proxyClient = builder.build()
proxyClientCache[proxy] = proxyClient
return proxyClient
}
return okHttpClient
}

@ -1,10 +1,5 @@
package io.legado.app.help.http package xyz.fycz.myreader.model.third3.http
import io.legado.app.constant.AppConst
import io.legado.app.help.AppConfig
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.GSON
import io.legado.app.utils.UTF8BOMFighter
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -13,6 +8,10 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import xyz.fycz.myreader.common.APPCONST
import xyz.fycz.myreader.util.help.UTF8BOMFighter
import xyz.fycz.myreader.util.utils.EncodingDetect
import xyz.fycz.myreader.util.utils.GSON
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.charset.Charset import java.nio.charset.Charset
@ -25,7 +24,7 @@ suspend fun OkHttpClient.newCallResponse(
): Response { ): Response {
return withContext(IO) { return withContext(IO) {
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) requestBuilder.header(APPCONST.UA_NAME, APPCONST.DEFAULT_USER_AGENT)
requestBuilder.apply(builder) requestBuilder.apply(builder)
var response: Response? = null var response: Response? = null
for (i in 0..retry) { for (i in 0..retry) {
@ -44,7 +43,7 @@ suspend fun OkHttpClient.newCallResponseBody(
): ResponseBody { ): ResponseBody {
return withContext(IO) { return withContext(IO) {
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) requestBuilder.header(APPCONST.UA_NAME, APPCONST.DEFAULT_USER_AGENT)
requestBuilder.apply(builder) requestBuilder.apply(builder)
var response: Response? = null var response: Response? = null
for (i in 0..retry) { for (i in 0..retry) {
@ -63,7 +62,7 @@ suspend fun OkHttpClient.newCallStrResponse(
): StrResponse { ): StrResponse {
return withContext(IO) { return withContext(IO) {
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) requestBuilder.header(APPCONST.UA_NAME, APPCONST.DEFAULT_USER_AGENT)
requestBuilder.apply(builder) requestBuilder.apply(builder)
var response: Response? = null var response: Response? = null
for (i in 0..retry) { for (i in 0..retry) {
@ -108,15 +107,15 @@ fun ResponseBody.text(encode: String? = null): String {
} }
//根据内容判断 //根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes) charsetName = EncodingDetect.getEncodeInHtml(responseBytes)
return String(responseBytes, Charset.forName(charsetName)) return String(responseBytes, Charset.forName(charsetName))
} }
fun Request.Builder.addHeaders(headers: Map<String, String>) { fun Request.Builder.addHeaders(headers: Map<String, String>) {
headers.forEach { headers.forEach {
if (it.key == AppConst.UA_NAME) { if (it.key == APPCONST.UA_NAME) {
//防止userAgent重复 //防止userAgent重复
removeHeader(AppConst.UA_NAME) removeHeader(APPCONST.UA_NAME)
} }
addHeader(it.key, it.value) addHeader(it.key, it.value)
} }

@ -1,4 +1,4 @@
package io.legado.app.help.http package xyz.fycz.myreader.model.third3.http
enum class RequestMethod { enum class RequestMethod {
GET, POST GET, POST

@ -1,4 +1,4 @@
package io.legado.app.help.http package xyz.fycz.myreader.model.third3.http
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log import android.util.Log

@ -1,4 +1,4 @@
package io.legado.app.help.http package xyz.fycz.myreader.model.third3.http
import okhttp3.* import okhttp3.*
import okhttp3.Response.Builder import okhttp3.Response.Builder

@ -12,7 +12,6 @@ import io.legado.app.model.Debug
import io.legado.app.model.NoStackTraceException import io.legado.app.model.NoStackTraceException
import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule
import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO

@ -7,12 +7,13 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.NoStackTraceException import io.legado.app.model.NoStackTraceException
import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.StringUtils.wordCountFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import splitties.init.appCtx import splitties.init.appCtx
import xyz.fycz.myreader.greendao.entity.Book
import xyz.fycz.myreader.greendao.entity.rule.BookSource
/** /**
* 获取详情 * 获取详情
@ -50,7 +51,7 @@ object BookInfo {
redirectUrl: String, redirectUrl: String,
canReName: Boolean, canReName: Boolean,
) { ) {
val infoRule = bookSource.getBookInfoRule() val infoRule = bookSource.infoRule
infoRule.init?.let { infoRule.init?.let {
if (it.isNotBlank()) { if (it.isNotBlank()) {
scope.ensureActive() scope.ensureActive()

@ -10,7 +10,6 @@ import io.legado.app.model.Debug
import io.legado.app.model.NoStackTraceException import io.legado.app.model.NoStackTraceException
import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule
import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.StringUtils.wordCountFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

@ -1,17 +1,12 @@
package xyz.fycz.myreader.model.third3.webBook package xyz.fycz.myreader.model.third3.webBook
import io.legado.app.data.entities.Book import xyz.fycz.myreader.model.third3.http.StrResponse
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.http.StrResponse
import io.legado.app.model.Debug
import io.legado.app.model.NoStackTraceException
import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import xyz.fycz.myreader.greendao.entity.Book
import xyz.fycz.myreader.greendao.entity.rule.BookSource
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")

@ -0,0 +1,785 @@
//Copyright (c) 2017. 章钦豪. All rights reserved.
package xyz.fycz.myreader.util.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.PixelFormat
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import xyz.fycz.myreader.application.App
import java.io.*
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.min
/**
* 本地缓存
*/
@Suppress("unused", "MemberVisibilityCanBePrivate")
class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) {
val TAG = ACache::class.simpleName
companion object {
const val TIME_HOUR = 60 * 60
const val TIME_DAY = TIME_HOUR * 24
private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb
private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量
private val mInstanceMap = HashMap<String, ACache>()
@JvmOverloads
fun get(
ctx: Context,
cacheName: String = "ACache",
maxSize: Long = MAX_SIZE.toLong(),
maxCount: Int = MAX_COUNT,
cacheDir: Boolean = true
): ACache {
val f = if (cacheDir) File(ctx.cacheDir, cacheName) else File(ctx.filesDir, cacheName)
return get(f, maxSize, maxCount)
}
@JvmOverloads
fun get(
cacheDir: File,
maxSize: Long = MAX_SIZE.toLong(),
maxCount: Int = MAX_COUNT
): ACache {
synchronized(this) {
var manager = mInstanceMap[cacheDir.absoluteFile.toString() + myPid()]
if (manager == null) {
manager = ACache(cacheDir, maxSize, maxCount)
mInstanceMap[cacheDir.absolutePath + myPid()] = manager
}
return manager
}
}
private fun myPid(): String {
return "_" + android.os.Process.myPid()
}
}
private var mCache: ACacheManager? = null
init {
try {
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
Log.i(TAG, "can't make dirs in %s" + cacheDir.absolutePath)
}
mCache = ACacheManager(cacheDir, max_size, max_count)
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
}
}
// =======================================
// ============ String数据 读写 ==============
// =======================================
/**
* 保存 String数据 缓存中
*
* @param key 保存的key
* @param value 保存的String数据
*/
fun put(key: String, value: String) {
mCache?.let { mCache ->
try {
val file = mCache.newFile(key)
file.writeText(value)
mCache.put(file)
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
}
}
}
/**
* 保存 String数据 缓存中
*
* @param key 保存的key
* @param value 保存的String数据
* @param saveTime 保存的时间单位
*/
fun put(key: String, value: String, saveTime: Int) {
put(key, Utils.newStringWithDateInfo(saveTime, value))
}
/**
* 读取 String数据
*
* @return String 数据
*/
fun getAsString(key: String): String? {
mCache?.let { mCache ->
val file = mCache[key]
if (!file.exists())
return null
var removeFile = false
try {
val text = file.readText()
if (!Utils.isDue(text)) {
return Utils.clearDateInfo(text)
} else {
removeFile = true
}
} catch (e: IOException) {
Log.e(TAG, "" + e.localizedMessage)
} finally {
if (removeFile)
remove(key)
}
}
return null
}
// =======================================
// ========== JSONObject 数据 读写 =========
// =======================================
/**
* 保存 JSONObject数据 缓存中
*
* @param key 保存的key
* @param value 保存的JSON数据
*/
fun put(key: String, value: JSONObject) {
put(key, value.toString())
}
/**
* 保存 JSONObject数据 缓存中
*
* @param key 保存的key
* @param value 保存的JSONObject数据
* @param saveTime 保存的时间单位
*/
fun put(key: String, value: JSONObject, saveTime: Int) {
put(key, value.toString(), saveTime)
}
/**
* 读取JSONObject数据
*
* @return JSONObject数据
*/
fun getAsJSONObject(key: String): JSONObject? {
val json = getAsString(key) ?: return null
return try {
JSONObject(json)
} catch (e: Exception) {
null
}
}
// =======================================
// ============ JSONArray 数据 读写 =============
// =======================================
/**
* 保存 JSONArray数据 缓存中
*
* @param key 保存的key
* @param value 保存的JSONArray数据
*/
fun put(key: String, value: JSONArray) {
put(key, value.toString())
}
/**
* 保存 JSONArray数据 缓存中
*
* @param key 保存的key
* @param value 保存的JSONArray数据
* @param saveTime 保存的时间单位
*/
fun put(key: String, value: JSONArray, saveTime: Int) {
put(key, value.toString(), saveTime)
}
/**
* 读取JSONArray数据
*
* @return JSONArray数据
*/
fun getAsJSONArray(key: String): JSONArray? {
val json = getAsString(key)
return try {
JSONArray(json)
} catch (e: Exception) {
null
}
}
// =======================================
// ============== byte 数据 读写 =============
// =======================================
/**
* 保存 byte数据 缓存中
*
* @param key 保存的key
* @param value 保存的数据
*/
fun put(key: String, value: ByteArray) {
mCache?.let { mCache ->
val file = mCache.newFile(key)
file.writeBytes(value)
mCache.put(file)
}
}
/**
* 保存 byte数据 缓存中
*
* @param key 保存的key
* @param value 保存的数据
* @param saveTime 保存的时间单位
*/
fun put(key: String, value: ByteArray, saveTime: Int) {
put(key, Utils.newByteArrayWithDateInfo(saveTime, value))
}
/**
* 获取 byte 数据
*
* @return byte 数据
*/
fun getAsBinary(key: String): ByteArray? {
mCache?.let { mCache ->
var removeFile = false
try {
val file = mCache[key]
if (!file.exists())
return null
val byteArray = file.readBytes()
return if (!Utils.isDue(byteArray)) {
Utils.clearDateInfo(byteArray)
} else {
removeFile = true
null
}
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
} finally {
if (removeFile)
remove(key)
}
}
return null
}
/**
* 保存 Serializable数据到 缓存中
*
* @param key 保存的key
* @param value 保存的value
* @param saveTime 保存的时间单位
*/
@JvmOverloads
fun put(key: String, value: Serializable, saveTime: Int = -1) {
try {
val byteArrayOutputStream = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOutputStream).use { oos ->
oos.writeObject(value)
val data = byteArrayOutputStream.toByteArray()
if (saveTime != -1) {
put(key, data, saveTime)
} else {
put(key, data)
}
}
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
}
}
/**
* 读取 Serializable数据
*
* @return Serializable 数据
*/
fun getAsObject(key: String): Any? {
val data = getAsBinary(key)
if (data != null) {
var bis: ByteArrayInputStream? = null
var ois: ObjectInputStream? = null
try {
bis = ByteArrayInputStream(data)
ois = ObjectInputStream(bis)
return ois.readObject()
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
} finally {
try {
bis?.close()
} catch (e: IOException) {
Log.e(TAG, "" + e.localizedMessage)
}
try {
ois?.close()
} catch (e: IOException) {
Log.e(TAG, "" + e.localizedMessage)
}
}
}
return null
}
// =======================================
// ============== bitmap 数据 读写 =============
// =======================================
/**
* 保存 bitmap 缓存中
*
* @param key 保存的key
* @param value 保存的bitmap数据
*/
fun put(key: String, value: Bitmap) {
put(key, Utils.bitmap2Bytes(value))
}
/**
* 保存 bitmap 缓存中
*
* @param key 保存的key
* @param value 保存的 bitmap 数据
* @param saveTime 保存的时间单位
*/
fun put(key: String, value: Bitmap, saveTime: Int) {
put(key, Utils.bitmap2Bytes(value), saveTime)
}
/**
* 读取 bitmap 数据
*
* @return bitmap 数据
*/
fun getAsBitmap(key: String): Bitmap? {
return if (getAsBinary(key) == null) {
null
} else Utils.bytes2Bitmap(getAsBinary(key)!!)
}
// =======================================
// ============= drawable 数据 读写 =============
// =======================================
/**
* 保存 drawable 缓存中
*
* @param key 保存的key
* @param value 保存的drawable数据
*/
fun put(key: String, value: Drawable) {
put(key, Utils.drawable2Bitmap(value))
}
/**
* 保存 drawable 缓存中
*
* @param key 保存的key
* @param value 保存的 drawable 数据
* @param saveTime 保存的时间单位
*/
fun put(key: String, value: Drawable, saveTime: Int) {
put(key, Utils.drawable2Bitmap(value), saveTime)
}
/**
* 读取 Drawable 数据
*
* @return Drawable 数据
*/
fun getAsDrawable(key: String): Drawable? {
return if (getAsBinary(key) == null) {
null
} else Utils.bitmap2Drawable(
Utils.bytes2Bitmap(
getAsBinary(key)!!
)
)
}
/**
* 获取缓存文件
*
* @return value 缓存的文件
*/
fun file(key: String): File? {
mCache?.let { mCache ->
try {
val f = mCache.newFile(key)
if (f.exists()) {
return f
} else {
return null
}
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
}
}
return null
}
/**
* 移除某个key
*
* @return 是否移除成功
*/
fun remove(key: String): Boolean {
return mCache?.remove(key) == true
}
/**
* 清除所有数据
*/
fun clear() {
mCache?.clear()
}
/**
* @author 杨福海michael www.yangfuhai.com
* @version 1.0
* title 时间计算工具类
*/
private object Utils {
private const val mSeparator = ' '
/**
* 判断缓存的String数据是否到期
*
* @return true到期了 false还没有到期
*/
fun isDue(str: String): Boolean {
return isDue(str.toByteArray())
}
/**
* 判断缓存的byte数据是否到期
*
* @return true到期了 false还没有到期
*/
fun isDue(data: ByteArray): Boolean {
try {
val text = getDateInfoFromDate(data)
if (text != null && text.size == 2) {
var saveTimeStr = text[0]
while (saveTimeStr.startsWith("0")) {
saveTimeStr = saveTimeStr
.substring(1)
}
val saveTime = java.lang.Long.valueOf(saveTimeStr)
val deleteAfter = java.lang.Long.valueOf(text[1])
if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) {
return true
}
}
} catch (e: Exception) {
Log.e("ACache", "" + e.localizedMessage)
}
return false
}
fun newStringWithDateInfo(second: Int, strInfo: String): String {
return createDateInfo(second) + strInfo
}
fun newByteArrayWithDateInfo(second: Int, data2: ByteArray): ByteArray {
val data1 = createDateInfo(second).toByteArray()
val retData = ByteArray(data1.size + data2.size)
System.arraycopy(data1, 0, retData, 0, data1.size)
System.arraycopy(data2, 0, retData, data1.size, data2.size)
return retData
}
fun clearDateInfo(strInfo: String?): String? {
strInfo?.let {
if (hasDateInfo(strInfo.toByteArray())) {
return strInfo.substring(strInfo.indexOf(mSeparator) + 1)
}
}
return strInfo
}
fun clearDateInfo(data: ByteArray): ByteArray {
return if (hasDateInfo(data)) {
copyOfRange(
data, indexOf(data, mSeparator) + 1,
data.size
)
} else data
}
fun hasDateInfo(data: ByteArray?): Boolean {
return (data != null && data.size > 15 && data[13] == '-'.code.toByte()
&& indexOf(data, mSeparator) > 14)
}
fun getDateInfoFromDate(data: ByteArray): Array<String>? {
if (hasDateInfo(data)) {
val saveDate = String(copyOfRange(data, 0, 13))
val deleteAfter = String(
copyOfRange(
data, 14,
indexOf(data, mSeparator)
)
)
return arrayOf(saveDate, deleteAfter)
}
return null
}
@Suppress("SameParameterValue")
private fun indexOf(data: ByteArray, c: Char): Int {
for (i in data.indices) {
if (data[i] == c.code.toByte()) {
return i
}
}
return -1
}
private fun copyOfRange(original: ByteArray, from: Int, to: Int): ByteArray {
val newLength = to - from
require(newLength >= 0) { "$from > $to" }
val copy = ByteArray(newLength)
System.arraycopy(
original, from, copy, 0,
min(original.size - from, newLength)
)
return copy
}
private fun createDateInfo(second: Int): String {
val currentTime = StringBuilder(System.currentTimeMillis().toString() + "")
while (currentTime.length < 13) {
currentTime.insert(0, "0")
}
return "$currentTime-$second$mSeparator"
}
/*
* Bitmap byte[]
*/
fun bitmap2Bytes(bm: Bitmap): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
bm.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
return byteArrayOutputStream.toByteArray()
}
/*
* byte[] Bitmap
*/
fun bytes2Bitmap(b: ByteArray): Bitmap? {
return if (b.isEmpty()) {
null
} else BitmapFactory.decodeByteArray(b, 0, b.size)
}
/*
* Drawable Bitmap
*/
fun drawable2Bitmap(drawable: Drawable): Bitmap {
// 取 drawable 的长宽
val w = drawable.intrinsicWidth
val h = drawable.intrinsicHeight
// 取 drawable 的颜色格式
@Suppress("DEPRECATION")
val config = if (drawable.opacity != PixelFormat.OPAQUE)
Bitmap.Config.ARGB_8888
else
Bitmap.Config.RGB_565
// 建立对应 bitmap
val bitmap = Bitmap.createBitmap(w, h, config)
// 建立对应 bitmap 的画布
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, w, h)
// 把 drawable 内容画到画布中
drawable.draw(canvas)
return bitmap
}
/*
* Bitmap Drawable
*/
fun bitmap2Drawable(bm: Bitmap?): Drawable? {
return if (bm == null) {
null
} else BitmapDrawable(App.getmContext().resources, bm)
}
}
/**
* @author 杨福海michael www.yangfuhai.com
* @version 1.0
* title 缓存管理器
*/
open inner class ACacheManager(
private var cacheDir: File,
private val sizeLimit: Long,
private val countLimit: Int
) {
private val cacheSize: AtomicLong = AtomicLong()
private val cacheCount: AtomicInteger = AtomicInteger()
private val lastUsageDates = Collections
.synchronizedMap(HashMap<File, Long>())
init {
calculateCacheSizeAndCacheCount()
}
/**
* 计算 cacheSize和cacheCount
*/
private fun calculateCacheSizeAndCacheCount() {
Thread {
try {
var size = 0
var count = 0
val cachedFiles = cacheDir.listFiles()
if (cachedFiles != null) {
for (cachedFile in cachedFiles) {
size += calculateSize(cachedFile).toInt()
count += 1
lastUsageDates[cachedFile] = cachedFile.lastModified()
}
cacheSize.set(size.toLong())
cacheCount.set(count)
}
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
}
}.start()
}
fun put(file: File) {
try {
var curCacheCount = cacheCount.get()
while (curCacheCount + 1 > countLimit) {
val freedSize = removeNext()
cacheSize.addAndGet(-freedSize)
curCacheCount = cacheCount.addAndGet(-1)
}
cacheCount.addAndGet(1)
val valueSize = calculateSize(file)
var curCacheSize = cacheSize.get()
while (curCacheSize + valueSize > sizeLimit) {
val freedSize = removeNext()
curCacheSize = cacheSize.addAndGet(-freedSize)
}
cacheSize.addAndGet(valueSize)
val currentTime = System.currentTimeMillis()
file.setLastModified(currentTime)
lastUsageDates[file] = currentTime
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
}
}
operator fun get(key: String): File {
val file = newFile(key)
val currentTime = System.currentTimeMillis()
file.setLastModified(currentTime)
lastUsageDates[file] = currentTime
return file
}
fun newFile(key: String): File {
return File(cacheDir, key.hashCode().toString() + "")
}
fun remove(key: String): Boolean {
val image = get(key)
return image.delete()
}
fun clear() {
try {
lastUsageDates.clear()
cacheSize.set(0)
val files = cacheDir.listFiles()
if (files != null) {
for (f in files) {
f.delete()
}
}
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
}
}
/**
* 移除旧的文件
*/
private fun removeNext(): Long {
try {
if (lastUsageDates.isEmpty()) {
return 0
}
var oldestUsage: Long? = null
var mostLongUsedFile: File? = null
val entries = lastUsageDates.entries
synchronized(lastUsageDates) {
for ((key, lastValueUsage) in entries) {
if (mostLongUsedFile == null) {
mostLongUsedFile = key
oldestUsage = lastValueUsage
} else {
if (lastValueUsage < oldestUsage!!) {
oldestUsage = lastValueUsage
mostLongUsedFile = key
}
}
}
}
var fileSize: Long = 0
if (mostLongUsedFile != null) {
fileSize = calculateSize(mostLongUsedFile!!)
if (mostLongUsedFile!!.delete()) {
lastUsageDates.remove(mostLongUsedFile)
}
}
return fileSize
} catch (e: Exception) {
Log.e(TAG, "" + e.localizedMessage)
return 0
}
}
private fun calculateSize(file: File): Long {
return file.length()
}
}
}

@ -8,6 +8,25 @@ import javax.crypto.spec.SecretKeySpec
@Suppress("unused") @Suppress("unused")
object EncoderUtils { object EncoderUtils {
fun escape(src: String): String {
val tmp = StringBuilder()
for (char in src) {
val charCode = char.code
if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) {
tmp.append(char)
continue
}
val prefix = when {
charCode < 16 -> "%0"
charCode < 256 -> "%"
else -> "%u"
}
tmp.append(prefix).append(charCode.toString(16))
}
return tmp.toString()
}
@JvmOverloads @JvmOverloads
fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String { fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String {
val bytes = Base64.decode(str, flags) val bytes = Base64.decode(str, flags)

@ -9,6 +9,7 @@ import android.os.Environment;
import android.os.StatFs; import android.os.StatFs;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.mozilla.universalchardet.UniversalDetector; import org.mozilla.universalchardet.UniversalDetector;
@ -904,4 +905,29 @@ public class FileUtils {
} }
} }
public static String getPath(String rootPath, String... subDirFiles){
StringBuilder path = new StringBuilder(rootPath);
for (String subPath : subDirFiles){
if (!TextUtils.isEmpty(subPath)){
if (!path.toString().endsWith(File.separator)){
path.append(File.separator);
}
path.append(subPath);
}
}
return path.toString();
}
public static String getPath(File root, String... subDirFiles){
StringBuilder path = new StringBuilder(root.getAbsolutePath());
for (String subPath : subDirFiles){
if (!TextUtils.isEmpty(subPath)){
if (!path.toString().endsWith(File.separator)){
path.append(File.separator);
}
path.append(subPath);
}
}
return path.toString();
}
} }

@ -0,0 +1,62 @@
package xyz.fycz.myreader.util.utils
import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl
import java.net.URL
import java.util.regex.Pattern
@Suppress("RegExpRedundantEscape")
object HtmlFormatter {
private val wrapHtmlRegex = "</?(?:div|p|br|hr|h\\d|article|dd|dl)[^>]*>".toRegex()
private val commentRegex = "<!--[^>]*-->".toRegex() //注释
private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val formatImagePattern = Pattern.compile(
"<img[^>]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|<img[^>]*data-[^=]*= *\"([^\"]*)\"[^>]*>|<img[^>]*src *= *\"([^\"]*)\"[^>]*>",
Pattern.CASE_INSENSITIVE
)
fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String {
html ?: return ""
return html.replace(wrapHtmlRegex, "\n")
.replace(commentRegex, "")
.replace(otherRegex, "")
.replace("\\s*\\n+\\s*".toRegex(), "\n  ")
.replace("^[\\n\\s]+".toRegex(), "  ")
.replace("[\\n\\s]+$".toRegex(), "")
}
fun formatKeepImg(html: String?, redirectUrl: URL? = null): String {
html ?: return ""
val keepImgHtml = format(html, notImgHtmlRegex)
//正则的“|”处于顶端而不处于()中时,具有类似||的熔断效果,故以此机制简化原来的代码
val matcher = formatImagePattern.matcher(keepImgHtml)
var appendPos = 0
val sb = StringBuffer()
while (matcher.find()) {
var param = ""
sb.append(
keepImgHtml.substring(appendPos, matcher.start()), "<img src=\"${
NetworkUtils.getAbsoluteURL(
redirectUrl,
matcher.group(1)?.let {
val urlMatcher = AnalyzeUrl.paramPattern.matcher(it)
if (urlMatcher.find()) {
param = ',' + it.substring(urlMatcher.end())
it.substring(0, urlMatcher.start())
} else it
} ?: matcher.group(2) ?: matcher.group(3)!!
) + param
}\">"
)
appendPos = matcher.end()
}
if (appendPos < keepImgHtml.length) sb.append(
keepImgHtml.substring(
appendPos,
keepImgHtml.length
)
)
return sb.toString()
}
}

@ -0,0 +1,176 @@
package xyz.fycz.myreader.util.utils
import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.util.Log
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
@Suppress("unused")
object RealPathUtil {
/**
* Method for return file path of Gallery image
* @return path of the selected image file from gallery
*/
private var filePathUri: Uri? = null
@Suppress("DEPRECATION")
fun getPath(context: Context, uri: Uri): String? {
//check here to KITKAT or new version
@SuppressLint("ObsoleteSdkInt")
val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
filePathUri = uri
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
}
} else if (isDownloadsDocument(uri)) {
val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
java.lang.Long.valueOf(id)
)
//return getDataColumn(context, uri, null, null);
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(
split[1]
)
return getDataColumn(context, contentUri, selection, selectionArgs)
}
} else if ("content".equals(
uri.scheme,
ignoreCase = true
)
) { // Return the remote address
return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
context,
uri,
null,
null
)
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
return uri.path
}
return null
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
private fun getDataColumn(
context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)
try {
cursor =
context.contentResolver.query(uri!!, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} catch (e: IllegalArgumentException) {
Log.e("RealPathUtil", "" + e.localizedMessage)
val file = File(context.cacheDir, "tmp")
val filePath = file.absolutePath
var input: FileInputStream? = null
var output: FileOutputStream? = null
try {
val pfd =
context.contentResolver.openFileDescriptor(filePathUri!!, "r")
?: return null
val fd = pfd.fileDescriptor
input = FileInputStream(fd)
output = FileOutputStream(filePath)
var read: Int
val bytes = ByteArray(4096)
while (input.read(bytes).also { read = it } != -1) {
output.write(bytes, 0, read)
}
return File(filePath).absolutePath
} catch (e: IOException) {
Log.e("RealPathUtil", "" + e.localizedMessage)
} finally {
input?.close()
output?.close()
}
} finally {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
}

@ -0,0 +1,81 @@
@file:Suppress("unused")
package xyz.fycz.myreader.util.utils
import android.icu.text.Collator
import android.icu.util.ULocale
import android.net.Uri
import java.io.File
import java.util.*
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
fun String.parseToUri(): Uri {
return if (isContentScheme()) {
Uri.parse(this)
} else {
Uri.fromFile(File(this))
}
}
fun String?.isAbsUrl() =
this?.let {
it.startsWith("http://", true) || it.startsWith("https://", true)
} ?: false
fun String?.isJson(): Boolean =
this?.run {
val str = this.trim()
when {
str.startsWith("{") && str.endsWith("}") -> true
str.startsWith("[") && str.endsWith("]") -> true
else -> false
}
} ?: false
fun String?.isJsonObject(): Boolean =
this?.run {
val str = this.trim()
str.startsWith("{") && str.endsWith("}")
} ?: false
fun String?.isJsonArray(): Boolean =
this?.run {
val str = this.trim()
str.startsWith("[") && str.endsWith("]")
} ?: false
fun String.splitNotBlank(vararg delimiter: String): Array<String> = run {
this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()
}
fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array<String> = run {
this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()
}
fun String.cnCompare(other: String): Int {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
Collator.getInstance(ULocale.SIMPLIFIED_CHINESE).compare(this, other)
} else {
java.text.Collator.getInstance(Locale.CHINA).compare(this, other)
}
}
/**
* 将字符串拆分为单个字符,包含emoji
*/
fun String.toStringArray(): Array<String> {
var codePointIndex = 0
return try {
Array(codePointCount(0, length)) {
val start = codePointIndex
codePointIndex = offsetByCodePoints(start, 1)
substring(start, codePointIndex)
}
} catch (e: Exception) {
split("").toTypedArray()
}
}

@ -635,4 +635,32 @@ public class StringUtils {
if (end < len) ++end; if (end < len) ++end;
return ((start > 0) || (end < len)) ? s.substring(start, end) : s; return ((start > 0) || (end < len)) ? s.substring(start, end) : s;
} }
public static String byteToHexString(byte[] bytes) {
if (bytes == null) return "";
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
int hex = 0xff & b;
if (hex < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(hex));
}
return sb.toString();
}
public static byte[] hexStringToByte(String hexString) {
String hexStr = hexString.replace(" ", "");
int len = hexStr.length();
byte[] bytes = new byte[len / 2];
int i = 0;
while (i < len) {
// 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节
bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) +
Character.digit(hexString.charAt(i+1), 16));
i += 2;
}
return bytes;
}
} }

@ -377,6 +377,7 @@
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL</string> <string name="login_url">登录URL</string>
<string name="comment">书源说明</string> <string name="comment">书源说明</string>
<string name="source_concurrent_rate">并发率</string>
<string name="r_search_url">搜索地址</string> <string name="r_search_url">搜索地址</string>
<string name="r_search_charset">搜索字符编码</string> <string name="r_search_charset">搜索字符编码</string>
<string name="r_find_url">发现地址规则</string> <string name="r_find_url">发现地址规则</string>

@ -3,17 +3,16 @@ ext {
support_library_version = '28.0.0' support_library_version = '28.0.0'
} }
buildscript { buildscript {
ext.kotlin_version = '1.4.20' ext.kotlin_version = '1.6.0'
repositories { repositories {
google() google()
jcenter()
mavenCentral() mavenCentral()
maven { url 'https://s3.amazonaws.com/fabric-artifacts/public' } maven { url 'https://s3.amazonaws.com/fabric-artifacts/public' }
maven { url 'https://plugins.gradle.org/m2/' } maven { url 'https://plugins.gradle.org/m2/' }
maven { url "https://maven.java.net/content/groups/public/" } maven { url "https://maven.java.net/content/groups/public/" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'

Loading…
Cancel
Save