diff --git a/app/build.gradle b/app/build.gradle index 99ba574..20b7047 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -148,12 +148,12 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(path: ':DialogX') - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - api 'androidx.core:core-ktx:1.3.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.core:core-ktx:1.7.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" //anko def anko_version = '0.10.8' @@ -161,12 +161,11 @@ dependencies { implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" //Glide - implementation 'com.github.bumptech.glide:glide:4.8.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0' + implementation 'com.github.bumptech.glide:glide:4.12.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' @@ -174,9 +173,9 @@ dependencies { implementation 'com.github.yuweiguocn:GreenDaoUpgradeHelper:v2.2.1' //JSoup - implementation 'org.jsoup:jsoup:1.11.3' - implementation 'cn.wanghaomiao:JsoupXpath:2.4.3' - implementation 'com.jayway.jsonpath:json-path:2.5.0' + implementation 'org.jsoup:jsoup:1.14.3' + implementation 'cn.wanghaomiao:JsoupXpath:2.5.0' + implementation 'com.jayway.jsonpath:json-path:2.6.0' //SmartRefreshLayout implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2' @@ -187,9 +186,9 @@ dependencies { 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 implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5' @@ -200,7 +199,7 @@ dependencies { //字符串比较 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 implementation 'io.reactivex.rxjava2:rxjava:2.2.19' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' @@ -257,7 +256,7 @@ dependencies { } greendao { - schemaVersion 32 + schemaVersion 33 daoPackage 'xyz.fycz.myreader.greendao.gen' // targetGenDir 'src/main/java' } diff --git a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java index 8ba6f3c..b74cf23 100644 --- a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java +++ b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java @@ -1,6 +1,7 @@ package xyz.fycz.myreader.common; import android.os.Environment; +import android.provider.Settings; import com.google.gson.reflect.TypeToken; import com.hjq.permissions.Permission; @@ -132,5 +133,11 @@ 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 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); + } } diff --git a/app/src/main/java/xyz/fycz/myreader/entity/sourceedit/EditEntityUtil.kt b/app/src/main/java/xyz/fycz/myreader/entity/sourceedit/EditEntityUtil.kt index 14b60b3..7366032 100644 --- a/app/src/main/java/xyz/fycz/myreader/entity/sourceedit/EditEntityUtil.kt +++ b/app/src/main/java/xyz/fycz/myreader/entity/sourceedit/EditEntityUtil.kt @@ -34,6 +34,7 @@ object EditEntityUtil { ) add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url, "")) add(EditEntity("sourceComment", source?.sourceComment, R.string.comment, "这是您留给使用者的说明")) + add(EditEntity("concurrentRate", source?.concurrentRate, R.string.comment, "")) } return sourceEntities } @@ -195,6 +196,7 @@ object EditEntityUtil { "sourceHeaders" -> source.sourceHeaders = it.value "loginUrl" -> source.loginUrl = it.value "sourceComment" -> source.sourceComment = it.value + "concurrentRate" -> source.concurrentRate = it.value } } return source diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/entity/Book.java b/app/src/main/java/xyz/fycz/myreader/greendao/entity/Book.java index 54cd9fc..c794003 100644 --- a/app/src/main/java/xyz/fycz/myreader/greendao/entity/Book.java +++ b/app/src/main/java/xyz/fycz/myreader/greendao/entity/Book.java @@ -3,6 +3,7 @@ package xyz.fycz.myreader.greendao.entity; import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.gson.Gson; @@ -13,6 +14,7 @@ import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.Transient; import xyz.fycz.myreader.greendao.service.BookService; +import xyz.fycz.myreader.model.third3.analyzeRule.RuleDataInterface; import xyz.fycz.myreader.util.SharedPreUtils; import java.io.Serializable; @@ -23,7 +25,7 @@ import java.util.Objects; import static xyz.fycz.myreader.common.APPCONST.MAP_STRING; @Entity -public class Book implements Serializable { +public class Book implements Serializable, RuleDataInterface { @Transient private static final long serialVersionUID = 1L; @@ -378,7 +380,7 @@ public class Book implements Serializable { this.status = status; } - public void putVariable(String key, String value) { + public void putVariable(@NonNull String key, String value) { if (variableMap == null) { variableMap = new HashMap<>(); } @@ -386,6 +388,7 @@ public class Book implements Serializable { variable = new Gson().toJson(variableMap); } + @NonNull public Map getVariableMap() { if (variableMap == null && !TextUtils.isEmpty(variable)) { variableMap = new Gson().fromJson(variable, MAP_STRING); @@ -398,7 +401,6 @@ public class Book implements Serializable { return this.variable; } - public void setVariable(String variable) { this.variable = variable; } @@ -430,4 +432,10 @@ public class Book implements Serializable { public void setReSeg(boolean reSeg) { this.reSeg = reSeg; } + + @Nullable + @Override + public String getVariable(@NonNull String key) { + return variableMap.get(key); + } } diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/entity/Cache.java b/app/src/main/java/xyz/fycz/myreader/greendao/entity/Cache.java new file mode 100644 index 0000000..eae5c9c --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/greendao/entity/Cache.java @@ -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; + } +} diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/entity/Chapter.java b/app/src/main/java/xyz/fycz/myreader/greendao/entity/Chapter.java index 4136ce9..d364723 100644 --- a/app/src/main/java/xyz/fycz/myreader/greendao/entity/Chapter.java +++ b/app/src/main/java/xyz/fycz/myreader/greendao/entity/Chapter.java @@ -1,14 +1,27 @@ 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 com.google.gson.Gson; + import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; +import org.greenrobot.greendao.annotation.Transient; + import xyz.fycz.myreader.common.APPCONST; +import xyz.fycz.myreader.model.third3.analyzeRule.RuleDataInterface; import xyz.fycz.myreader.util.utils.FileUtils; import java.io.File; +import java.util.HashMap; +import java.util.Map; /** * 章节 @@ -16,7 +29,7 @@ import java.io.File; */ @Entity -public class Chapter { +public class Chapter implements RuleDataInterface { @Id private String id; @@ -32,10 +45,13 @@ public class Chapter { //章节内容在文章中的终止位置(本地) private long end; + private String variable; + @Transient + private Map variableMap; - @Generated(hash = 763230955) + @Generated(hash = 1398484308) 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.bookId = bookId; this.number = number; @@ -44,52 +60,66 @@ public class Chapter { this.content = content; this.start = start; this.end = end; + this.variable = variable; } + @Generated(hash = 393170288) public Chapter() { } + public String getId() { return this.id; } + public void setId(String id) { this.id = id; } + public String getBookId() { return this.bookId; } + public void setBookId(String bookId) { this.bookId = bookId; } + public int getNumber() { return this.number; } + public void setNumber(int number) { this.number = number; } + public String getTitle() { return this.title; } + public void setTitle(String title) { this.title = title; } + public String getUrl() { return this.url; } + public void setUrl(String url) { this.url = url; } + public String getContent() { if (end > 0) return end + ""; String filePath = APPCONST.BOOK_CACHE_PATH + bookId + File.separator + title + FileUtils.SUFFIX_FY; File file = new File(filePath); - if (file.exists() && file.length() > 0){ + if (file.exists() && file.length() > 0) { this.content = filePath; - }else { + } else { this.content = null; } return this.content; } + public void setContent(String content) { this.content = content; } @@ -109,4 +139,34 @@ public class Chapter { public void setEnd(long 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 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; + } } diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/entity/rule/BookSource.java b/app/src/main/java/xyz/fycz/myreader/greendao/entity/rule/BookSource.java index 6f6b53d..54f7df7 100644 --- a/app/src/main/java/xyz/fycz/myreader/greendao/entity/rule/BookSource.java +++ b/app/src/main/java/xyz/fycz/myreader/greendao/entity/rule/BookSource.java @@ -44,6 +44,7 @@ public class BookSource implements Parcelable, Cloneable { private String sourceHeaders; private String loginUrl; private String sourceComment; + private String concurrentRate; private Long lastUpdateTime; @OrderBy @@ -72,12 +73,12 @@ public class BookSource implements Parcelable, Cloneable { @Convert(converter = FindRuleConvert.class, columnType = String.class) private FindRule findRule; - @Generated(hash = 277037260) + @Generated(hash = 906128088) public BookSource(String sourceUrl, String sourceEName, String sourceName, String sourceGroup, String sourceCharset, String sourceType, String sourceHeaders, String loginUrl, - String sourceComment, Long lastUpdateTime, int orderNum, int weight, boolean enable, - SearchRule searchRule, InfoRule infoRule, TocRule tocRule, ContentRule contentRule, - FindRule findRule) { + String sourceComment, String concurrentRate, Long lastUpdateTime, int orderNum, int weight, + boolean enable, SearchRule searchRule, InfoRule infoRule, TocRule tocRule, + ContentRule contentRule, FindRule findRule) { this.sourceUrl = sourceUrl; this.sourceEName = sourceEName; this.sourceName = sourceName; @@ -87,6 +88,7 @@ public class BookSource implements Parcelable, Cloneable { this.sourceHeaders = sourceHeaders; this.loginUrl = loginUrl; this.sourceComment = sourceComment; + this.concurrentRate = concurrentRate; this.lastUpdateTime = lastUpdateTime; this.orderNum = orderNum; this.weight = weight; @@ -113,6 +115,7 @@ public class BookSource implements Parcelable, Cloneable { sourceHeaders = in.readString(); loginUrl = in.readString(); sourceComment = in.readString(); + concurrentRate = in.readString(); if (in.readByte() == 0) { lastUpdateTime = null; } else { @@ -139,6 +142,7 @@ public class BookSource implements Parcelable, Cloneable { dest.writeString(sourceHeaders); dest.writeString(loginUrl); dest.writeString(sourceComment); + dest.writeString(concurrentRate); if (lastUpdateTime == null) { dest.writeByte((byte) 0); } else { @@ -187,6 +191,7 @@ public class BookSource implements Parcelable, Cloneable { stringEquals(sourceHeaders, source.sourceHeaders) && stringEquals(loginUrl, source.loginUrl) && stringEquals(sourceComment, source.sourceComment) && + stringEquals(concurrentRate, source.concurrentRate) && Objects.equals(searchRule, source.searchRule) && Objects.equals(infoRule, source.infoRule) && Objects.equals(tocRule, source.tocRule) && @@ -396,4 +401,12 @@ public class BookSource implements Parcelable, Cloneable { public void setLoginUrl(String loginUrl) { this.loginUrl = loginUrl; } + + public String getConcurrentRate() { + return this.concurrentRate; + } + + public void setConcurrentRate(String concurrentRate) { + this.concurrentRate = concurrentRate; + } } diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt b/app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt new file mode 100644 index 0000000..b8d7a42 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt @@ -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>() + + /** + * 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeRule.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeRule.kt index f884665..a866cbe 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeRule.kt @@ -1,20 +1,19 @@ package xyz.fycz.myreader.model.third3.analyzeRule import android.text.TextUtils +import android.util.Log 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 org.jsoup.nodes.Entities 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.util.* import java.util.regex.Pattern @@ -28,12 +27,12 @@ import kotlin.collections.HashMap @Suppress("unused", "RegExpRedundantEscape", "MemberVisibilityCanBePrivate") class AnalyzeRule( val ruleData: RuleDataInterface, - private val source: BaseSource? = null + private val source: BookSource? = null ) : 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 content: Any? = null private set @@ -651,7 +650,7 @@ class AnalyzeRule( return SCRIPT_ENGINE.eval(jsStr, bindings) } - override fun getSource(): BaseSource? { + override fun getSource(): BookSource? { return source } @@ -665,9 +664,9 @@ class AnalyzeRule( analyzeUrl.getStrResponseAwait().body }.onFailure { log("ajax(${urlStr}) error\n${it.stackTraceToString()}") - Timber.e(it) + Log.e(TAG, it.toString()) }.getOrElse { - it.msg + it.message } } } diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeUrl.kt index b66afa6..2889963 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/AnalyzeUrl.kt @@ -4,20 +4,18 @@ import android.annotation.SuppressLint import androidx.annotation.Keep import com.bumptech.glide.load.model.GlideUrl 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 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.util.* import java.util.regex.Pattern @@ -38,9 +36,9 @@ class AnalyzeUrl( val speakText: String? = null, val speakSpeed: Int? = null, var baseUrl: String = "", - private val source: BaseSource? = null, + private val source: BookSource? = null, private val ruleData: RuleDataInterface? = null, - private val chapter: BookChapter? = null, + private val chapter: Chapter? = null, headerMapF: Map? = null, ) : JsExtensions { companion object { @@ -195,7 +193,7 @@ class AnalyzeUrl( } } headerMap[UA_NAME] ?: let { - headerMap[UA_NAME] = AppConfig.userAgent + headerMap[UA_NAME] = DEFAULT_USER_AGENT } urlNoQuery = url when (method) { @@ -224,7 +222,7 @@ class AnalyzeUrl( val queryM = query.splitNotBlank("=") val value = if (queryM.size > 1) queryM[1] else "" if (charset.isNullOrEmpty()) { - if (NetworkUtils.hasUrlEncoded(value)) { + if (UrlEncoderUtils.hasUrlEncoded(value)) { fieldMap[queryM[0]] = value } else { fieldMap[queryM[0]] = URLEncoder.encode(value, "UTF-8") @@ -286,10 +284,10 @@ class AnalyzeUrl( return null } val rateIndex = concurrentRate.indexOf("/") - var fetchRecord = concurrentRecordMap[source.getKey()] + var fetchRecord = concurrentRecordMap[source.sourceUrl] if (fetchRecord == null) { fetchRecord = ConcurrentRecord(rateIndex > 0, System.currentTimeMillis(), 1) - concurrentRecordMap[source.getKey()] = fetchRecord + concurrentRecordMap[source.sourceUrl] = fetchRecord return fetchRecord } val waitTime: Int = synchronized(fetchRecord) { @@ -354,7 +352,7 @@ class AnalyzeUrl( return StrResponse(url, StringUtils.byteToHexString(getByteArrayAwait())) } val concurrentRecord = fetchStart() - setCookie(source?.getKey()) + setCookie(source?.sourceUrl) val strResponse: StrResponse if (this.useWebView && useWebView) { strResponse = when (method) { @@ -371,7 +369,7 @@ class AnalyzeUrl( BackstageWebView( url = url, html = body, - tag = source?.getKey(), + tag = source?.sourceUrl, javaScript = webJs ?: jsStr, sourceRegex = sourceRegex, headerMap = headerMap @@ -379,7 +377,7 @@ class AnalyzeUrl( } else -> BackstageWebView( url = url, - tag = source?.getKey(), + tag = source?.sourceUrl, javaScript = webJs ?: jsStr, sourceRegex = sourceRegex, headerMap = headerMap @@ -421,7 +419,7 @@ class AnalyzeUrl( */ suspend fun getResponseAwait(): Response { val concurrentRecord = fetchStart() - setCookie(source?.getKey()) + setCookie(source?.sourceUrl) @Suppress("BlockingMethodInNonBlockingContext") val response = getProxyClient(proxy).newCallResponse(retry) { addHeaders(headerMap) @@ -452,7 +450,7 @@ class AnalyzeUrl( */ suspend fun getByteArrayAwait(): ByteArray { val concurrentRecord = fetchStart() - setCookie(source?.getKey()) + setCookie(source?.sourceUrl) @Suppress("BlockingMethodInNonBlockingContext") val byteArray = getProxyClient(proxy).newCallResponseBody(retry) { addHeaders(headerMap) @@ -522,14 +520,14 @@ class AnalyzeUrl( } fun getUserAgent(): String { - return headerMap[UA_NAME] ?: AppConfig.userAgent + return headerMap[UA_NAME] ?: DEFAULT_USER_AGENT } fun isPost(): Boolean { return method == RequestMethod.POST } - override fun getSource(): BaseSource? { + override fun getSource(): BookSource? { return source } diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt index 83f0657..198929c 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt @@ -4,15 +4,18 @@ import android.net.Uri import android.util.Base64 import android.util.Log import androidx.annotation.Keep -import io.legado.app.help.http.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import nl.siegmann.epublib.epub.PackageDocumentBase.dateFormat import org.jsoup.Connection 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.service.CacheManager 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.utils.* import java.io.ByteArrayInputStream @@ -34,7 +37,7 @@ import java.util.zip.ZipInputStream @Suppress("unused") interface JsExtensions { - private val TAG: String? + val TAG: String? get() = JsExtensions::class.simpleName fun getSource(): BookSource? @@ -271,7 +274,8 @@ interface JsExtensions { } 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 */ fun getFile(path: String): File { - val cachePath = appCtx.externalCache.absolutePath + val cachePath = FileUtils.getCachePath() val aPath = if (path.startsWith(File.separator)) { cachePath + path } else { @@ -330,13 +334,16 @@ interface JsExtensions { */ fun unzipFile(zipPath: String): String { 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) - val zipFile = FileUtils.getFile(zipPath) - val unzipFolder = FileUtils.getFolder(unzipPath) + val zipFile = getFile(zipPath) + val unzipFolder = FileUtils.getFile(unzipPath) ZipUtils.unzipFile(zipFile, unzipFolder) - FileUtils.deleteFile(zipPath) - return unzipPath + FileUtils.deleteFile(zipFile.absolutePath) + return unzipPath.substring(FileUtils.getCachePath().length) } /** @@ -437,7 +444,7 @@ interface JsExtensions { } 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() else -> base64DecodeToByteArray(str) } @@ -476,10 +483,10 @@ interface JsExtensions { * 输出调试日志 */ fun log(msg: String): String { - getSource()?.let { - Debug.log(it.getKey(), msg) - } ?: Debug.log(msg) - if (BuildConfig.DEBUG) { + /*getSource()?.let { + Debug.log(it.sourceUrl, msg) + } ?: Debug.log(msg)*/ + if (App.isDebug()) { Log.e(TAG, msg) } return msg @@ -549,7 +556,7 @@ interface JsExtensions { iv.encodeToByteArray() ) } catch (e: Exception) { - Log.e(TAG, r.toString()) + Log.e(TAG, e.toString()) log(e.localizedMessage ?: "aesDecodeToByteArrayERROR") null } @@ -644,7 +651,7 @@ interface JsExtensions { } fun android(): String { - return AppConst.androidId + return APPCONST.androidId } } diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/RuleDataInterface.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/RuleDataInterface.kt index e866cfa..bda38b0 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/RuleDataInterface.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/RuleDataInterface.kt @@ -2,7 +2,7 @@ package xyz.fycz.myreader.model.third3.analyzeRule interface RuleDataInterface { - val variableMap: HashMap + val variableMap: Map fun putVariable(key: String, value: String?) diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/http/BackstageWebView.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/http/BackstageWebView.kt index dc4ce1d..0012af0 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/http/BackstageWebView.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/http/BackstageWebView.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.http +package xyz.fycz.myreader.model.third3.http import android.annotation.SuppressLint import android.os.Handler @@ -40,7 +40,7 @@ class BackstageWebView( destroy() } } - callback = object : BackstageWebView.Callback() { + callback = object : Callback() { override fun onResult(response: StrResponse) { if (!block.isCompleted) block.resume(response) diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/http/HttpHelper.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/http/HttpHelper.kt new file mode 100644 index 0000000..d967887 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/http/HttpHelper.kt @@ -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 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 +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/http/OkHttpUtils.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/http/OkHttpUtils.kt index c92d939..3e6c47e 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/http/OkHttpUtils.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/http/OkHttpUtils.kt @@ -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.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -13,6 +8,10 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody 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.IOException import java.nio.charset.Charset @@ -25,7 +24,7 @@ suspend fun OkHttpClient.newCallResponse( ): Response { return withContext(IO) { val requestBuilder = Request.Builder() - requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) + requestBuilder.header(APPCONST.UA_NAME, APPCONST.DEFAULT_USER_AGENT) requestBuilder.apply(builder) var response: Response? = null for (i in 0..retry) { @@ -44,7 +43,7 @@ suspend fun OkHttpClient.newCallResponseBody( ): ResponseBody { return withContext(IO) { val requestBuilder = Request.Builder() - requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) + requestBuilder.header(APPCONST.UA_NAME, APPCONST.DEFAULT_USER_AGENT) requestBuilder.apply(builder) var response: Response? = null for (i in 0..retry) { @@ -63,7 +62,7 @@ suspend fun OkHttpClient.newCallStrResponse( ): StrResponse { return withContext(IO) { val requestBuilder = Request.Builder() - requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) + requestBuilder.header(APPCONST.UA_NAME, APPCONST.DEFAULT_USER_AGENT) requestBuilder.apply(builder) var response: Response? = null 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)) } fun Request.Builder.addHeaders(headers: Map) { headers.forEach { - if (it.key == AppConst.UA_NAME) { + if (it.key == APPCONST.UA_NAME) { //防止userAgent重复 - removeHeader(AppConst.UA_NAME) + removeHeader(APPCONST.UA_NAME) } addHeader(it.key, it.value) } diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/http/RequestMethod.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/http/RequestMethod.kt index bba9f97..ef9c5cb 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/http/RequestMethod.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/http/RequestMethod.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.http +package xyz.fycz.myreader.model.third3.http enum class RequestMethod { GET, POST diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/http/SSLHelper.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/http/SSLHelper.kt index 55624c6..132bf03 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/http/SSLHelper.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/http/SSLHelper.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.http +package xyz.fycz.myreader.model.third3.http import android.annotation.SuppressLint import android.util.Log diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/http/StrResponse.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/http/StrResponse.kt index 0e11ea3..7000f67 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/http/StrResponse.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/http/StrResponse.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.http +package xyz.fycz.myreader.model.third3.http import okhttp3.* import okhttp3.Response.Builder diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookContent.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookContent.kt index 6966017..dfd7165 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookContent.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookContent.kt @@ -12,7 +12,6 @@ import io.legado.app.model.Debug import io.legado.app.model.NoStackTraceException import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl -import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.NetworkUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookInfo.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookInfo.kt index 2b67b8d..7de87d1 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookInfo.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookInfo.kt @@ -7,12 +7,13 @@ import io.legado.app.help.BookHelp import io.legado.app.model.Debug import io.legado.app.model.NoStackTraceException 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.StringUtils.wordCountFormat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ensureActive 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, canReName: Boolean, ) { - val infoRule = bookSource.getBookInfoRule() + val infoRule = bookSource.infoRule infoRule.init?.let { if (it.isNotBlank()) { scope.ensureActive() diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookList.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookList.kt index b8415ea..82949ea 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookList.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/BookList.kt @@ -10,7 +10,6 @@ import io.legado.app.model.Debug import io.legado.app.model.NoStackTraceException import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeRule 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.StringUtils.wordCountFormat import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/WebBook.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/WebBook.kt index 65386d5..ee46354 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/WebBook.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/webBook/WebBook.kt @@ -1,17 +1,12 @@ package xyz.fycz.myreader.model.third3.webBook -import io.legado.app.data.entities.Book -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.http.StrResponse import xyz.fycz.myreader.model.third3.analyzeRule.AnalyzeUrl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive +import xyz.fycz.myreader.greendao.entity.Book +import xyz.fycz.myreader.greendao.entity.rule.BookSource import kotlin.coroutines.CoroutineContext @Suppress("MemberVisibilityCanBePrivate") diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt new file mode 100644 index 0000000..a0c8960 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt @@ -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() + + @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? { + 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()) + + 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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/EncoderUtils.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/EncoderUtils.kt index 834ed4e..092fc5b 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/EncoderUtils.kt +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/EncoderUtils.kt @@ -8,6 +8,25 @@ import javax.crypto.spec.SecretKeySpec @Suppress("unused") 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 fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String { val bytes = Base64.decode(str, flags) diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java b/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java index be9255e..ef3da76 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java @@ -9,6 +9,7 @@ import android.os.Environment; import android.os.StatFs; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.text.TextUtils; import android.util.Log; 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(); + } } diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/HtmlFormatter.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/HtmlFormatter.kt new file mode 100644 index 0000000..63f250f --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/HtmlFormatter.kt @@ -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 = "]*>".toRegex() + private val commentRegex = "".toRegex() //注释 + private val notImgHtmlRegex = "])[^<>]*>".toRegex() + private val otherHtmlRegex = "])[^<>]*>".toRegex() + private val formatImagePattern = Pattern.compile( + "]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|]*data-[^=]*= *\"([^\"]*)\"[^>]*>|]*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()), "" + ) + appendPos = matcher.end() + } + if (appendPos < keepImgHtml.length) sb.append( + keepImgHtml.substring( + appendPos, + keepImgHtml.length + ) + ) + return sb.toString() + } +} diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/RealPathUtil.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/RealPathUtil.kt new file mode 100644 index 0000000..693505e --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/RealPathUtil.kt @@ -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? { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt new file mode 100644 index 0000000..6344e01 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt @@ -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 = run { + this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() +} + +fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = 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 { + var codePointIndex = 0 + return try { + Array(codePointCount(0, length)) { + val start = codePointIndex + codePointIndex = offsetByCodePoints(start, 1) + substring(start, codePointIndex) + } + } catch (e: Exception) { + split("").toTypedArray() + } +} + diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java b/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java index 99d43e2..0e6b014 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/StringUtils.java @@ -635,4 +635,32 @@ public class StringUtils { if (end < len) ++end; 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; + } + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f10c34..89f06d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,6 +377,7 @@ 分类Url 登录URL 书源说明 + 并发率 搜索地址 搜索字符编码 发现地址规则 diff --git a/build.gradle b/build.gradle index 40dbb61..19a4229 100644 --- a/build.gradle +++ b/build.gradle @@ -3,17 +3,16 @@ ext { support_library_version = '28.0.0' } buildscript { - ext.kotlin_version = '1.4.20' + ext.kotlin_version = '1.6.0' repositories { google() - jcenter() mavenCentral() maven { url 'https://s3.amazonaws.com/fabric-artifacts/public' } maven { url 'https://plugins.gradle.org/m2/' } maven { url "https://maven.java.net/content/groups/public/" } } 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.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'