Merge pull request #28 from fengyuecanzhu/dev

Merge dev to master
master
风月残烛 2 years ago committed by GitHub
commit d9963620a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      .github/workflows/build.yml
  2. 7
      .gitignore
  3. 1
      .idea/gradle.xml
  4. 2
      .idea/misc.xml
  5. 1
      .idea/modules.xml
  6. 38
      README.md
  7. 61
      app/build.gradle
  8. 9
      app/release.md
  9. 16
      app/src/main/assets/updatelog.fy
  10. 10
      app/src/main/java/xyz/fycz/myreader/application/App.java
  11. 2
      app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
  12. 30
      app/src/main/java/xyz/fycz/myreader/ui/activity/AboutActivity.java
  13. 48
      app/src/main/java/xyz/fycz/myreader/ui/dialog/UpdateDialog.java
  14. 29
      app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java
  15. 24
      app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt
  16. 53
      app/src/main/java/xyz/fycz/myreader/webapi/LanZouApi.kt
  17. 38
      app/src/main/java/xyz/fycz/myreader/webapi/LanZousApi.java
  18. 7
      app/src/main/java/xyz/fycz/myreader/widget/page/PageView.java
  19. 36
      app/src/main/res/layout/layout_about_content.xml
  20. 1
      app/src/main/res/values/strings.xml
  21. 3
      app/src/test/java/Test.java
  22. 4
      app/version_code.properties
  23. 2
      build.gradle
  24. 103
      dynamic/build.gradle
  25. 7
      dynamic/src/androidTest/java/xyz/fycz/dynamic/ExampleInstrumentedTest.kt
  26. 74
      dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt
  27. 24
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App243Fix.kt
  28. 14
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App244Fix.kt
  29. 14
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App244Fix2.kt
  30. 81
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App245Fix.kt
  31. 106
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix.kt
  32. 54
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix2.kt
  33. 139
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix3.kt
  34. 84
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix4.kt
  35. 208
      dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix5.kt
  36. 18
      dynamic/src/main/java/xyz/fycz/dynamic/fix/AppFixHandle.kt
  37. 57
      dynamic/src/main/java/xyz/fycz/dynamic/fix/AppSubSourceFix.kt
  38. 149
      dynamic/src/main/java/xyz/fycz/dynamic/utils/LanZouUtils.kt
  39. 1
      settings.gradle

@ -1,4 +1,4 @@
name: Auto build and delay name: build
on: on:
push: push:
@ -59,47 +59,33 @@ jobs:
if [[ ${{ steps.config.outputs.need_create_release }} != 'true' ]];then if [[ ${{ steps.config.outputs.need_create_release }} != 'true' ]];then
path="$GITHUB_WORKSPACE/app/build/outputs/apk/debug" path="$GITHUB_WORKSPACE/app/build/outputs/apk/debug"
fi fi
files=$(ls $path) echo ::set-output name=file_path::"$path"
for f in $files - name: Upload Lanzou
do
if [[ $f == *"apk" ]]; then
file=$f
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 文件:$file"
break
fi
done
echo ::set-output name=file_name::"$file"
echo ::set-output name=file_path::"$path/$file"
- name: Upload App To Artifact
uses: actions/upload-artifact@v2
with:
name: FYReader-apk
path: ${{ steps.get_path.outputs.file_path }}
- name: Upload App To Lanzou
run: | run: |
echo "上传APP至蓝奏云" echo "上传APP至蓝奏云"
python3 $GITHUB_WORKSPACE/keystore/scripts/lzy.py "${{ steps.get_path.outputs.file_path }}" "${{ steps.config.outputs.lanzou_folder_id }}" tag_name=${{ steps.config.outputs.version_name }}
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: ${{ steps.config.outputs.lanzou_share_url }}" if [ ${{ steps.config.outputs.need_create_release }} != 'true' -a ${{ github.ref }} != 'refs/heads/master' ];then
- name: Create Release gitHead=$(git rev-parse --short HEAD)
id: create_release tag_name=${{ steps.config.outputs.version_name }}-$gitHead
fi
python3 $GITHUB_WORKSPACE/keystore/scripts/lzy.py "${{ steps.get_path.outputs.file_path }}/风月读书$tag_name.apk" "${{ steps.config.outputs.lanzou_folder_id }}" "$tag_name"
# 发布正式版
- name: Release
if: ${{ steps.config.outputs.need_create_release == 'true' && github.ref == 'refs/heads/master' }} if: ${{ steps.config.outputs.need_create_release == 'true' && github.ref == 'refs/heads/master' }}
uses: actions/create-release@v1 uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TZ: Asia/Shanghai
with: with:
tag_name: ${{ steps.config.outputs.version_name }} body_path: ./app/release.md
release_name: FYReader_${{ steps.config.outputs.version_name }} prerelease: false
body_path: ${{ github.workspace }}/app/release.md
draft: false draft: false
- name: Upload Release Asset tag_name: ${{ steps.config.outputs.version_name }}
id: upload_release_asset name: FYReader_${{ steps.config.outputs.version_name }}
if: ${{ steps.config.outputs.need_create_release == 'true' && steps.create_release.outputs.upload_url }} files: |
uses: actions/upload-release-asset@v1 ${{ steps.get_path.outputs.file_path }}/风月读书${{ steps.config.outputs.version_name }}.apk
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 上传Artifact
- name: Upload Artifact
uses: actions/upload-artifact@v3
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} name: app
asset_path: ${{ steps.get_path.outputs.file_path }} path: ${{ steps.get_path.outputs.file_path }}/风月读书*.apk
asset_name: FYReader_${{ steps.config.outputs.version_name }}.apk
asset_content_type: application/vnd.android.package-archive

7
.gitignore vendored

@ -29,10 +29,3 @@ keystore
*.zip *.zip
*.tar.gz *.tar.gz
*.rar *.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
<<<<<<< HEAD
>>>>>>> 2c33f0b33a0411efd93a4810de5061a473835ff5
=======
>>>>>>> eee131cea39d27218fc7d1c3d641d617d377af42

@ -14,6 +14,7 @@
<option value="$PROJECT_DIR$/DialogX" /> <option value="$PROJECT_DIR$/DialogX" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/dynamic" /> <option value="$PROJECT_DIR$/dynamic" />
<option value="$PROJECT_DIR$/plugin" />
<option value="$PROJECT_DIR$/user" /> <option value="$PROJECT_DIR$/user" />
</set> </set>
</option> </option>

@ -3,6 +3,8 @@
<component name="DesignSurface"> <component name="DesignSurface">
<option name="filePathToZoomLevelMap"> <option name="filePathToZoomLevelMap">
<map> <map>
<entry key="..\:/android/FYReader/app/src/main/res/layout/activity_about.xml" value="0.2296195652173913" />
<entry key="..\:/android/FYReader/app/src/main/res/layout/activity_ad_setting.xml" value="0.12132725430597771" />
<entry key="..\:/android/FYReader/app/src/main/res/layout/activity_donate.xml" value="0.264" /> <entry key="..\:/android/FYReader/app/src/main/res/layout/activity_donate.xml" value="0.264" />
<entry key="..\:/android/FYReader/app/src/main/res/layout/activity_more_setting.xml" value="0.2" /> <entry key="..\:/android/FYReader/app/src/main/res/layout/activity_more_setting.xml" value="0.2" />
<entry key="..\:/android/FYReader/app/src/main/res/layout/activity_read.xml" value="0.12132725430597771" /> <entry key="..\:/android/FYReader/app/src/main/res/layout/activity_read.xml" value="0.12132725430597771" />

@ -15,6 +15,7 @@
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.androidTest.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.main.iml" filepath="$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.main.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.main.iml" filepath="$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.unitTest.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/dynamic/FYReader.dynamic.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/plugin/FYReader.plugin.iml" filepath="$PROJECT_DIR$/.idea/modules/plugin/FYReader.plugin.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/user/FYReader.user.iml" filepath="$PROJECT_DIR$/.idea/modules/user/FYReader.user.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/user/FYReader.user.iml" filepath="$PROJECT_DIR$/.idea/modules/user/FYReader.user.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/user/FYReader.user.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/user/FYReader.user.androidTest.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/user/FYReader.user.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/user/FYReader.user.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/user/FYReader.user.main.iml" filepath="$PROJECT_DIR$/.idea/modules/user/FYReader.user.main.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/user/FYReader.user.main.iml" filepath="$PROJECT_DIR$/.idea/modules/user/FYReader.user.main.iml" />

@ -14,10 +14,10 @@
<table> <table>
<tr> <tr>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/p1.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/p1.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/p2.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/p2.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/p3.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/p3.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/p4.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/p4.png" ></center></td>
</tr> </tr>
</table> </table>
@ -62,9 +62,9 @@
<table> <table>
<tr> <tr>
<td><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/Donate/wx_zsm.jpg" width=300>微信赞赏码</center></td> <td><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/Donate/wx_zsm.jpg" width=300>微信赞赏码</center></td>
<td><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/Donate/zfb_skm.jpg" width=300>支付宝收款码</center></td> <td><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/Donate/zfb_skm.jpg" width=300>支付宝收款码</center></td>
<td><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/Donate/qq_skm.jpg" width=300>QQ收款码</center></td> <td><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/Donate/qq_skm.jpg" width=300>QQ收款码</center></td>
</tr> </tr>
</table> </table>
@ -74,21 +74,21 @@
<table> <table>
<tr> <tr>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/1.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/1.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/2.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/2.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/3.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/3.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/4.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/4.png" ></center></td>
</tr> </tr>
<tr> <tr>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/5.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/5.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/6.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/6.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/7.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/7.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/8.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/8.png" ></center></td>
</tr> </tr>
<tr> <tr>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/9.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/9.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/10.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/10.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/11.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/11.png" ></center></td>
<td ><center><img src="https://fyreader.coding.net/p/img/d/img/git/raw/master/FYReader/12.png" ></center></td> <td ><center><img src="https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/img/FYReader/12.png" ></center></td>
</tr> </tr>
</table> </table>

@ -20,54 +20,28 @@ apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' apply plugin: 'org.greenrobot.greendao'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
import com.android.build.OutputFile
def releaseTime() { def releaseTime() {
return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+08:00")) return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+08:00"))
} }
def commitId = 'git rev-parse --short HEAD'.execute().text.trim() def commitId = 'git rev-parse --short HEAD'.execute().text.trim()
def getVersionCode() { def getVersionC() {
def versionCodeFile = file('version_code.properties') def versionCodeFile = file('version_code.properties')
if (versionCodeFile.canRead()) { if (versionCodeFile.canRead()) {
Properties properties = new Properties() Properties properties = new Properties()
properties.load(new FileInputStream(versionCodeFile)) properties.load(new FileInputStream(versionCodeFile))
def versionCode = properties['VERSION_CODE'].toInteger()//version_code.properties文件存放的版本号 return properties['VERSION_CODE'].toInteger()
def runTasks = gradle.startParameter.taskNames
def task = ':app:assembleRelease'
if (task in runTasks) {
properties['VERSION_CODE'] = (++versionCode).toString()
properties.store(versionCodeFile.newWriter(), null)
}
return versionCode
} else { } else {
throw new FileNotFoundException("无法读取version_code.properties文件!") return 200
}
}
def getVersionCodeNotIncrease() {
def versionCodeFile = file('version_code.properties')
if (versionCodeFile.canRead()) {
Properties properties = new Properties()
properties.load(new FileInputStream(versionCodeFile))
def versionCode = properties['VERSION_CODE'].toInteger()//version_code.properties文件存放的版本号
return versionCode
} else {
throw new FileNotFoundException("无法读取version_code.properties文件!")
} }
} }
def name = "风月读书" def name = "风月读书"
def version = getVersionCode() def versionC = getVersionC()
def versionN = versionC.toString()
def getVersionName(){ versionN = versionN[0] + "." + versionN[1] + "." + versionN[2]
def version = getVersionCodeNotIncrease()
def hun = (version / 100).toInteger()
def ten = (version / 10).toInteger() % 10
def one = version % 10
return hun + "." + ten + "." + one
}
def versionN = getVersionName()
// keystore.properties // keystore.properties
def keyProps = new Properties() def keyProps = new Properties()
@ -84,7 +58,7 @@ android {
applicationId "xyz.fycz.myreader" applicationId "xyz.fycz.myreader"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode version versionCode versionC
versionName versionN versionName versionN
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
@ -104,6 +78,15 @@ android {
viewBinding true viewBinding true
} }
/*splits {
abi {
reset()
enable true
universalApk true // If true, also generate a universal APK
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}*/
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
@ -136,6 +119,14 @@ android {
variant.outputs.all { variant.outputs.all {
outputFileName = fileName outputFileName = fileName
} }
/*variant.outputs.each { output ->
def abi = output.getFilter(OutputFile.ABI)
if (abi == null) {
abi = "universal"
}
def fileName = "${name}v${defaultConfig.versionName}-${abi}.apk"
output.outputFileName = fileName
}*/
} }
} }
sourceSets.main { sourceSets.main {
@ -302,7 +293,7 @@ dependencies {
} }
//https://github.com/fengyuecanzhu/Maple //https://github.com/fengyuecanzhu/Maple
implementation("me.fycz.maple:maple:1.8") implementation("me.fycz.maple:maple:1.9")
} }
greendao { greendao {

@ -1 +1,8 @@
* 1、修复书源过多时搜索界面卡死的问题 * 1、修复阅读界面概率性闪退的问题
* 2、关于界面新增插件加载结果
* 3、修复书源订阅失败的问题
* 4、修复字体下载失败的问题
* 5、\[设置-缓存设置\]新增清除广告文件
* 6、修复检查更新失败的问题
* 7、修复获取更新链接失败的问题
* 8、更新订阅书源链接

@ -1,3 +1,19 @@
2022.08.02
风月读书v2.4.7
1、修复阅读界面概率性闪退的问题
2、关于界面新增插件加载结果
3、修复书源订阅失败的问题
4、修复字体下载失败的问题
5、[设置-缓存设置]新增清除广告文件
6、修复检查更新失败的问题
7、修复获取更新链接失败的问题
8、更新订阅书源链接
2022.05.28
风月读书v2.4.6
更新内容:
1、修复书源过多时搜索界面卡死的问题
2022.05.22 2022.05.22
风月读书v2.4.5 风月读书v2.4.5
更新内容: 更新内容:

@ -282,15 +282,9 @@ public class App extends Application {
public static void checkVersionByServer(final AppCompatActivity activity, final boolean isManualCheck) { public static void checkVersionByServer(final AppCompatActivity activity, final boolean isManualCheck) {
App.getApplication().newThread(() -> { App.getApplication().newThread(() -> {
try { try {
String url = "https://shimo.im/docs/cqkgjPRRydYYhQKt/read"; String content = OkHttpUtils.getUpdateInfo();
if (debug) {
url = "https://shimo.im/docs/zfzpda7MUGskOC9v/read";
}
String html = OkHttpUtils.getHtml(url);
Document doc = Jsoup.parse(html);
String content = doc.getElementsByClass("ql-editor").text();
if (StringHelper.isEmpty(content)) { if (StringHelper.isEmpty(content)) {
content = OkHttpUtils.getUpdateInfo(); content = OkHttpUtils.getBakUpdateInfo();
if (StringHelper.isEmpty(content)) { if (StringHelper.isEmpty(content)) {
if (isManualCheck || NetworkUtils.isNetWorkAvailable()) { if (isManualCheck || NetworkUtils.isNetWorkAvailable()) {
ToastUtils.showError("检查更新失败!"); ToastUtils.showError("检查更新失败!");

@ -56,7 +56,7 @@ public class URLCONST {
public static final String QUOTATION = "https://v1.hitokoto.cn/?encode=json&charset=utf-8"; public static final String QUOTATION = "https://v1.hitokoto.cn/?encode=json&charset=utf-8";
public static final String DEFAULT_PLUGIN_CONFIG_URL = "https://fyreader.coding.net/p/img/d/Plugin/git/raw/master/release/config_FYReader.json"; public static final String DEFAULT_PLUGIN_CONFIG_URL = "https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/Plugin/release/config_FYReader.json";
public static String getDefaultDomain() { public static String getDefaultDomain() {
return SharedPreUtils.getInstance().getString("domain", "fycz.me"); return SharedPreUtils.getInstance().getString("domain", "fycz.me");

@ -23,6 +23,7 @@ import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -42,6 +43,7 @@ import xyz.fycz.myreader.base.observer.MyObserver;
import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.common.URLCONST; import xyz.fycz.myreader.common.URLCONST;
import xyz.fycz.myreader.databinding.ActivityAboutBinding; import xyz.fycz.myreader.databinding.ActivityAboutBinding;
import xyz.fycz.myreader.entity.PluginConfig;
import xyz.fycz.myreader.model.user.UserService; import xyz.fycz.myreader.model.user.UserService;
import xyz.fycz.myreader.ui.dialog.DialogCreator; import xyz.fycz.myreader.ui.dialog.DialogCreator;
import xyz.fycz.myreader.ui.dialog.LoadingDialog; import xyz.fycz.myreader.ui.dialog.LoadingDialog;
@ -53,6 +55,7 @@ import xyz.fycz.myreader.util.ZipUtils;
import xyz.fycz.myreader.util.utils.AdUtils; import xyz.fycz.myreader.util.utils.AdUtils;
import xyz.fycz.myreader.util.utils.FileUtils; import xyz.fycz.myreader.util.utils.FileUtils;
import xyz.fycz.myreader.util.utils.OkHttpUtils; import xyz.fycz.myreader.util.utils.OkHttpUtils;
import xyz.fycz.myreader.util.utils.PluginUtils;
import xyz.fycz.myreader.util.utils.RxUtils; import xyz.fycz.myreader.util.utils.RxUtils;
import xyz.fycz.myreader.webapi.LanZouApi; import xyz.fycz.myreader.webapi.LanZouApi;
@ -62,6 +65,8 @@ import xyz.fycz.myreader.webapi.LanZouApi;
*/ */
public class AboutActivity extends BaseActivity<ActivityAboutBinding> { public class AboutActivity extends BaseActivity<ActivityAboutBinding> {
private PluginConfig pluginConfig;
@Override @Override
protected void bindView() { protected void bindView() {
binding = ActivityAboutBinding.inflate(getLayoutInflater()); binding = ActivityAboutBinding.inflate(getLayoutInflater());
@ -75,11 +80,21 @@ public class AboutActivity extends BaseActivity<ActivityAboutBinding> {
getSupportActionBar().setTitle("关于"); getSupportActionBar().setTitle("关于");
} }
@Override
protected void initData(Bundle savedInstanceState) {
super.initData(savedInstanceState);
pluginConfig = PluginUtils.INSTANCE.getConfig();
}
@Override @Override
protected void initWidget() { protected void initWidget() {
super.initWidget(); super.initWidget();
binding.il.tvVersionName.setText(String.format("风月读书v%s", App.getStrVersionName())); binding.il.tvVersionName.setText(String.format("风月读书v%s", App.getStrVersionName()));
binding.il.tvPlugin.setText(PluginUtils.INSTANCE.getLoadSuccess() ?
getString(R.string.plugin_version,
pluginConfig != null ? pluginConfig.getVersion() : "插件加载失败")
: "插件加载失败"
);
binding.il.rlLanZou.setVisibility(App.isDebug() ? View.VISIBLE : View.GONE); binding.il.rlLanZou.setVisibility(App.isDebug() ? View.VISIBLE : View.GONE);
binding.il.rlResetPangle.setVisibility(App.isDebug() ? View.VISIBLE : View.GONE); binding.il.rlResetPangle.setVisibility(App.isDebug() ? View.VISIBLE : View.GONE);
} }
@ -87,6 +102,21 @@ public class AboutActivity extends BaseActivity<ActivityAboutBinding> {
@Override @Override
protected void initClick() { protected void initClick() {
super.initClick(); super.initClick();
binding.il.rlPlugin.setOnClickListener(v -> {
String tip = "";
if (pluginConfig == null) {
tip = "插件配置读取失败";
} else {
if (PluginUtils.INSTANCE.getLoadSuccess()) {
tip = PluginUtils.INSTANCE.getPluginLoadInfo();
} else {
tip = PluginUtils.INSTANCE.getErrorMsg();
}
}
DialogCreator.createTipDialog(this, binding.il.tvPlugin.getText().toString(), tip);
});
ClipboardManager mClipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager mClipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
binding.il.rlUpdate.setOnClickListener(v -> App.checkVersionByServer(this, true)); binding.il.rlUpdate.setOnClickListener(v -> App.checkVersionByServer(this, true));
binding.il.rlUpdateLog.setOnClickListener(v -> DialogCreator.createAssetTipDialog(this, "更新日志", "updatelog.fy")); binding.il.rlUpdateLog.setOnClickListener(v -> DialogCreator.createAssetTipDialog(this, "更新日志", "updatelog.fy"));

@ -18,10 +18,8 @@
package xyz.fycz.myreader.ui.dialog; package xyz.fycz.myreader.ui.dialog;
import android.Manifest;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -37,14 +35,12 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import com.hjq.permissions.OnPermissionCallback; import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions; import com.hjq.permissions.XXPermissions;
import com.liulishuo.filedownloader.BaseDownloadTask; import com.liulishuo.filedownloader.BaseDownloadTask;
import com.liulishuo.filedownloader.FileDownloadLargeFileListener; import com.liulishuo.filedownloader.FileDownloadLargeFileListener;
@ -60,11 +56,12 @@ import java.util.List;
import xyz.fycz.myreader.R; import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.App; import xyz.fycz.myreader.application.App;
import xyz.fycz.myreader.base.observer.MyObserver;
import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.databinding.FragmentUpdateBinding; import xyz.fycz.myreader.databinding.FragmentUpdateBinding;
import xyz.fycz.myreader.util.ToastUtils; import xyz.fycz.myreader.util.ToastUtils;
import xyz.fycz.myreader.webapi.LanZousApi; import xyz.fycz.myreader.util.utils.RxUtils;
import xyz.fycz.myreader.webapi.ResultCallback; import xyz.fycz.myreader.webapi.LanZouApi;
public class UpdateDialog extends Fragment { public class UpdateDialog extends Fragment {
@ -290,13 +287,13 @@ public class UpdateDialog extends Fragment {
.setListener(new FileDownloadLargeFileListener() { .setListener(new FileDownloadLargeFileListener() {
@Override @Override
protected void pending(BaseDownloadTask task, long soFarBytes, long totalBytes) { protected void pending(BaseDownloadTask task, long soFarBytes, long totalBytes) {
if (debug) Log.e("downloadApk", "pending-------"); if (debug) Log.d("downloadApk", "pending-------");
} }
@Override @Override
protected void progress(BaseDownloadTask task, long soFarBytes, long totalBytes) { protected void progress(BaseDownloadTask task, long soFarBytes, long totalBytes) {
float percent = 1f * soFarBytes / totalBytes * 100; float percent = 1f * soFarBytes / totalBytes * 100;
if (debug) Log.e("downloadApk", "progress-------" + percent); if (debug) Log.d("downloadApk", "progress-------" + percent);
if (percent >= 3) { if (percent >= 3) {
binding.barPercentView.setPercentage(percent); binding.barPercentView.setPercentage(percent);
binding.tvProgress.setText((int) percent + "%"); binding.tvProgress.setText((int) percent + "%");
@ -305,12 +302,12 @@ public class UpdateDialog extends Fragment {
@Override @Override
protected void paused(BaseDownloadTask task, long soFarBytes, long totalBytes) { protected void paused(BaseDownloadTask task, long soFarBytes, long totalBytes) {
if (debug) Log.e("downloadApk", "paused-------"); if (debug) Log.d("downloadApk", "paused-------");
} }
@Override @Override
protected void completed(BaseDownloadTask task) { protected void completed(BaseDownloadTask task) {
if (debug) Log.e("downloadApk", "completed-------"); if (debug) Log.d("downloadApk", "completed-------");
binding.barPercentView.setPercentage(100); binding.barPercentView.setPercentage(100);
binding.tvProgress.setText("100%"); binding.tvProgress.setText("100%");
install(new File(path), mActivity); install(new File(path), mActivity);
@ -375,22 +372,23 @@ public class UpdateDialog extends Fragment {
private void downloadWithLanzous(String apkUrl) { private void downloadWithLanzous(String apkUrl) {
binding.tvProgress.setText("正在获取下载链接..."); binding.tvProgress.setText("正在获取下载链接...");
binding.barPercentView.setPercentage(0); binding.barPercentView.setPercentage(0);
LanZousApi.getUrl(apkUrl, new ResultCallback() { LanZouApi.INSTANCE.getFileUrl(apkUrl)
@Override .compose(RxUtils::toSimpleSingle)
public void onFinish(Object o, int code) { .subscribe(new MyObserver<String>() {
String downloadUrl = (String) o; @Override
if (downloadUrl == null) { public void onNext(String directUrl) {
App.runOnUiThread(() -> error()); if (directUrl == null) {
return; error();
} }else {
App.runOnUiThread(() -> downloadApkNormal(downloadUrl)); downloadApkNormal(directUrl);
} }
}
@Override @Override
public void onError(Exception e) { public void onError(Throwable e) {
App.runOnUiThread(() -> error()); error();
} }
}); });
} }
private void error() { private void error() {

@ -176,7 +176,7 @@ public class OkHttpUtils {
return response.body().byteStream(); return response.body().byteStream();
} }
public static String getUpdateInfo() throws IOException, JSONException { public static String getUpdateInfo() throws IOException {
String key = "ryvwiq"; String key = "ryvwiq";
if (App.isDebug()) { if (App.isDebug()) {
key = "sgak2h"; key = "sgak2h";
@ -197,17 +197,28 @@ public class OkHttpUtils {
if (body == null) { if (body == null) {
return ""; return "";
} else { } else {
String bodyStr = new String(body.bytes(), StandardCharsets.UTF_8); try {
JSONObject jsonObj = new JSONObject(bodyStr); String bodyStr = new String(body.bytes(), StandardCharsets.UTF_8);
jsonObj = jsonObj.getJSONObject("data"); JSONObject jsonObj = new JSONObject(bodyStr);
String content = jsonObj.getString("content"); jsonObj = jsonObj.getJSONObject("data");
Document doc = Jsoup.parse(content); String content = jsonObj.getString("content");
content = doc.text(); Document doc = Jsoup.parse(content);
Log.d("Http -> UpdateInfo", content); content = doc.text();
return content; Log.d("Http -> UpdateInfo", content);
return content;
}catch (JSONException e){
e.printStackTrace();
return "";
}
} }
} }
public static String getBakUpdateInfo() throws IOException {
return OkHttpUtils.getHtml("https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/FYReader-Update/" +
(App.isDebug() ? "debug" : "release") +
"/content.txt");
}
public static Observable<StrResponse> getStrResponse(AnalyzeUrl analyzeUrl) { public static Observable<StrResponse> getStrResponse(AnalyzeUrl analyzeUrl) {
return Observable.create(emitter -> { return Observable.create(emitter -> {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

@ -21,10 +21,8 @@ package xyz.fycz.myreader.util.utils
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import dalvik.system.DexClassLoader import dalvik.system.DexClassLoader
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import me.fycz.maple.MapleUtils
import xyz.fycz.dynamic.AppParam import xyz.fycz.dynamic.AppParam
import xyz.fycz.dynamic.IAppLoader import xyz.fycz.dynamic.IAppLoader
import xyz.fycz.myreader.application.App import xyz.fycz.myreader.application.App
@ -33,11 +31,9 @@ import xyz.fycz.myreader.common.URLCONST.DEFAULT_PLUGIN_CONFIG_URL
import xyz.fycz.myreader.entity.PluginConfig import xyz.fycz.myreader.entity.PluginConfig
import xyz.fycz.myreader.model.third3.Coroutine import xyz.fycz.myreader.model.third3.Coroutine
import xyz.fycz.myreader.model.third3.http.getProxyClient import xyz.fycz.myreader.model.third3.http.getProxyClient
import xyz.fycz.myreader.model.third3.http.newCallResponse
import xyz.fycz.myreader.model.third3.http.newCallResponseBody import xyz.fycz.myreader.model.third3.http.newCallResponseBody
import xyz.fycz.myreader.model.third3.http.text import xyz.fycz.myreader.model.third3.http.text
import xyz.fycz.myreader.util.SharedPreUtils import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.ToastUtils
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -49,12 +45,15 @@ import java.util.*
object PluginUtils { object PluginUtils {
val TAG = PluginUtils.javaClass.simpleName val TAG = PluginUtils.javaClass.simpleName
private var appLoader: IAppLoader? = null
var config: PluginConfig? = null
var hasLoad = false var hasLoad = false
var loadSuccess = false
var errorMsg = ""
fun init() { fun init() {
val pluginConfigUrl = val pluginConfigUrl =
SharedPreUtils.getInstance().getString("pluginConfigUrl", DEFAULT_PLUGIN_CONFIG_URL) SharedPreUtils.getInstance().getString("pluginConfigUrl", DEFAULT_PLUGIN_CONFIG_URL)
var config: PluginConfig? = null
Coroutine.async { Coroutine.async {
val oldConfig = GSON.fromJsonObject<PluginConfig>( val oldConfig = GSON.fromJsonObject<PluginConfig>(
SharedPreUtils.getInstance().getString("pluginConfig") SharedPreUtils.getInstance().getString("pluginConfig")
@ -108,7 +107,7 @@ object PluginUtils {
) )
try { try {
val libClazz = dexClassLoader.loadClass("xyz.fycz.dynamic.AppLoadImpl") val libClazz = dexClassLoader.loadClass("xyz.fycz.dynamic.AppLoadImpl")
val appLoader = libClazz.newInstance() as IAppLoader? appLoader = libClazz.newInstance() as IAppLoader?
appLoader?.run { appLoader?.run {
val appParam = AppParam() val appParam = AppParam()
appParam.classLoader = context.classLoader appParam.classLoader = context.classLoader
@ -116,13 +115,24 @@ object PluginUtils {
appParam.appInfo = context.applicationInfo appParam.appInfo = context.applicationInfo
onLoad(appParam) onLoad(appParam)
hasLoad = true hasLoad = true
loadSuccess = true
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
errorMsg = e.stackTraceToString()
} }
} else { } else {
errorMsg = pluginPath + "文件不存在"
Log.d(TAG, pluginPath + "文件不存在") Log.d(TAG, pluginPath + "文件不存在")
} }
} }
} }
fun getPluginLoadInfo(): String {
return try {
MapleUtils.callMethod(appLoader, "getPluginLoadInfo") as String
} catch (e: Exception) {
""
}
}
} }

@ -97,9 +97,17 @@ object LanZouApi {
): Coroutine<List<LanZouFile>?> { ): Coroutine<List<LanZouFile>?> {
return Coroutine.async { return Coroutine.async {
val params = if (page == 1) { val params = if (page == 1) {
getFoldParams(getProxyClient().newCallResponseBody { url(foldUrl) }.text(), page, pwd) getFoldParams(
getProxyClient().newCallResponseBody { url(foldUrl) }.text(),
page,
pwd
)
} else { } else {
paramCathe[foldUrl] ?: getFoldParams(getProxyClient().newCallResponseBody { url(foldUrl) }.text(), page, pwd) paramCathe[foldUrl] ?: getFoldParams(getProxyClient().newCallResponseBody {
url(
foldUrl
)
}.text(), page, pwd)
} }
params["pg"] = page params["pg"] = page
paramCathe[foldUrl] = params paramCathe[foldUrl] = params
@ -158,12 +166,17 @@ object LanZouApi {
* @param url * @param url
* @param password * @param password
*/ */
private fun getFileUrl(url: String, password: String = ""): Observable<String> { fun getFileUrl(url: String, password: String = ""): Observable<String> {
return Observable.create { return Observable.create {
val html = OkHttpUtils.getHtml(url) var html = OkHttpUtils.getHtml(url)
val url2 = if (password.isEmpty()) { val url2 = if (password.isEmpty()) {
val url1 = getUrl1(html) val url1 = getUrl1(html)
val key = getKey(OkHttpUtils.getHtml(url1)) html = OkHttpUtils.getHtml(url1)
val data = getDataString(html)
Log.d("LanZouUtils", "data:$data")
val key = getKeyValueByKey(html, data, "sign") +
"&" + getKeyValueByKey(html, data, "websignkey")
Log.d("LanZouUtils", "key:$key")
getUrl2(key, url1) getUrl2(key, url1)
} else { } else {
getUrl2(StringHelper.getSubString(html, "sign=", "&"), url, password) getUrl2(StringHelper.getSubString(html, "sign=", "&"), url, password)
@ -177,27 +190,31 @@ object LanZouApi {
} }
} }
private fun getUrl1(html: String): String { fun getDataString(html: String): String {
val start = html.lastIndexOf("data :") + "data :".length
val end = html.indexOf("},", start) + 1
return html.substring(start, end)
}
fun getUrl1(html: String): String {
val doc = Jsoup.parse(html) val doc = Jsoup.parse(html)
return URLCONST.LAN_ZOU_URL + doc.getElementsByTag("iframe").attr("src") return URLCONST.LAN_ZOU_URL + doc.getElementsByTag("iframe").attr("src")
} }
private fun getKey(html: String): String { fun getKeyValueByKey(html: String, data: String, key: String): String {
var lanzousKeyStart = "var pposturl = '" val keyName = StringHelper.getSubString(data, "'$key':", ",")
val keyName = StringHelper.getSubString(html, "'sign':", ",") return if (keyName.endsWith("'")) {
lanzousKeyStart = if (keyName.endsWith("'")) { key + "=" + keyName.replace("'", "")
"'sign':'"
} else { } else {
"var $keyName = '" val lanzousKeyStart = "var $keyName = '"
key + "=" + StringHelper.getSubString(html, lanzousKeyStart, "'")
} }
return StringHelper.getSubString(html, lanzousKeyStart, "'")
} }
fun getUrl2(key: String, referer: String, password: String = ""): String {
private fun getUrl2(key: String, referer: String, password: String = ""): String {
val mediaType = "application/x-www-form-urlencoded".toMediaTypeOrNull() val mediaType = "application/x-www-form-urlencoded".toMediaTypeOrNull()
val body = if (password.isEmpty()) { val body = if (password.isEmpty()) {
"action=downprocess&sign=$key&ves=1" "action=downprocess&signs=?ctdf&websign=&ves=1&$key"
} else { } else {
"action=downprocess&sign=$key&p=$password" "action=downprocess&sign=$key&p=$password"
} }
@ -213,7 +230,7 @@ object LanZouApi {
return getUrl2(html) return getUrl2(html)
} }
private fun getUrl2(o: String): String { fun getUrl2(o: String): String {
/*val info = o.split(",").toTypedArray() /*val info = o.split(",").toTypedArray()
val zt = info[0].substring(info[0].indexOf(":") + 1) val zt = info[0].substring(info[0].indexOf(":") + 1)
if (!"1".endsWith(zt)) { if (!"1".endsWith(zt)) {
@ -239,7 +256,7 @@ object LanZouApi {
* *
* @param path * @param path
*/ */
private fun getRedirectUrl(path: String): String { fun getRedirectUrl(path: String): String {
val conn = URL(path) val conn = URL(path)
.openConnection() as HttpURLConnection .openConnection() as HttpURLConnection
conn.instanceFollowRedirects = false conn.instanceFollowRedirects = false

@ -32,6 +32,7 @@ import io.reactivex.SingleOnSubscribe;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import xyz.fycz.myreader.application.App; import xyz.fycz.myreader.application.App;
import xyz.fycz.myreader.base.observer.MyObserver;
import xyz.fycz.myreader.base.observer.MySingleObserver; import xyz.fycz.myreader.base.observer.MySingleObserver;
import xyz.fycz.myreader.common.URLCONST; import xyz.fycz.myreader.common.URLCONST;
import xyz.fycz.myreader.util.help.StringHelper; import xyz.fycz.myreader.util.help.StringHelper;
@ -51,40 +52,23 @@ public class LanZousApi {
* @param callback * @param callback
*/ */
public static void getUrl(final String lanZouUrl, final ResultCallback callback) { public static void getUrl(final String lanZouUrl, final ResultCallback callback) {
LanZousApi.getUrl1(lanZouUrl, new ResultCallback() { getUrl(lanZouUrl, "", callback);
@Override }
public void onFinish(final Object o, int code) {
LanZousApi.getKey((String) o, new ResultCallback() {
final String referer = (String) o;
public static void getUrl(final String lanZouUrl, String password, final ResultCallback callback) {
LanZouApi.INSTANCE.getFileUrl(lanZouUrl, "")
.compose(RxUtils::toSimpleSingle)
.subscribe(new MyObserver<String>() {
@Override @Override
public void onFinish(Object o, int code) { public void onNext(String s) {
LanZousApi.getUrl2((String) o, new ResultCallback() { callback.onFinish(s, 1);
@Override
public void onFinish(Object o, int code) {
LanZousApi.getRedirectUrl((String) o, callback);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
}, referer);
} }
@Override @Override
public void onError(Exception e) { public void onError(Throwable e) {
callback.onError(e); callback.onError((Exception) e);
} }
}); });
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
} }

@ -491,9 +491,10 @@ public class PageView extends View {
@Override @Override
protected void onDetachedFromWindow() { protected void onDetachedFromWindow() {
super.onDetachedFromWindow(); super.onDetachedFromWindow();
mPageAnim.abortAnim(); if (mPageAnim != null) {
mPageAnim.clear(); mPageAnim.abortAnim();
mPageAnim.clear();
}
mPageLoader = null; mPageLoader = null;
mPageAnim = null; mPageAnim = null;
} }

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
~ This file is part of FYReader. ~ This file is part of FYReader.
~ FYReader is free software: you can redistribute it and/or modify ~ FYReader is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by ~ it under the terms of the GNU General Public License as published by
@ -55,6 +54,37 @@
</LinearLayout> </LinearLayout>
<RelativeLayout
android:id="@+id/rl_plugin"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/selector_common_bg"
android:gravity="center"
android:paddingStart="20dp"
android:paddingEnd="10dp">
<TextView
android:id="@+id/tv_plugin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="@string/plugin_version"
android:textColor="@color/textPrimary"
android:textSize="@dimen/text_normal_size" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
app:srcCompat="@drawable/ic_right_arrow"
app:tint="@color/textSecondary" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp" />
<RelativeLayout <RelativeLayout
android:id="@+id/rl_update" android:id="@+id/rl_update"
@ -355,6 +385,7 @@
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="10dp" /> android:layout_height="10dp" />
<RelativeLayout <RelativeLayout
android:id="@+id/rl_lan_zou" android:id="@+id/rl_lan_zou"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -380,6 +411,7 @@
app:srcCompat="@drawable/ic_right_arrow" app:srcCompat="@drawable/ic_right_arrow"
app:tint="@color/textSecondary" /> app:tint="@color/textSecondary" />
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/rl_reset_pangle" android:id="@+id/rl_reset_pangle"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -123,6 +123,7 @@
<string name="git_hub">GitHub</string> <string name="git_hub">GitHub</string>
<string name="share_log">分享崩溃日志</string> <string name="share_log">分享崩溃日志</string>
<string name="this_github_url">https://github.com/fengyuecanzhu/FYReader</string> <string name="this_github_url">https://github.com/fengyuecanzhu/FYReader</string>
<string name="plugin_version">插件版本:%s</string>
<string name="update">检查更新</string> <string name="update">检查更新</string>
<string name="thanks">致谢名单</string> <string name="thanks">致谢名单</string>
<string name="join_qq_channel">加入QQ频道</string> <string name="join_qq_channel">加入QQ频道</string>

@ -21,6 +21,7 @@ import org.seimicrawler.xpath.JXDocument;
import java.io.File; import java.io.File;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import xyz.fycz.myreader.model.sourceAnalyzer.BaseAnalyzer; import xyz.fycz.myreader.model.sourceAnalyzer.BaseAnalyzer;
@ -135,7 +136,7 @@ public class Test {
@org.junit.Test @org.junit.Test
public void md5(){ public void md5(){
File file = new File("D:\\Java\\AndroidSdk\\build-tools\\29.0.3\\dynamic_v1.0.5.dex"); File file = new File("D:\\Java\\AndroidSdk\\build-tools\\29.0.3\\dynamic_v1.0.6.dex");
System.out.println(MD5Utils.INSTANCE.getFileMD5s(file, 32)); System.out.println(MD5Utils.INSTANCE.getFileMD5s(file, 32));
} }
} }

@ -17,5 +17,5 @@
# #
#Fri Jun 18 21:45:31 CST 2021 #Fri Jun 18 21:45:31 CST 2021
VERSION_CODE=246 VERSION_CODE=247
CREATE_RELEASE=true CREATE_RELEASE=true

@ -33,7 +33,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.3' 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:1.6.21"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath 'com.github.megatronking.stringfog:gradle-plugin:3.0.0' classpath 'com.github.megatronking.stringfog:gradle-plugin:3.0.0'

@ -48,12 +48,111 @@ android {
} }
dependencies { dependencies {
compileOnly("androidx.core:core-ktx:$kotlin_version")
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
compileOnly("me.fycz.maple:maple:1.7") // androidx
compileOnly "androidx.core:core-ktx:$kotlin_version"
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compileOnly 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
compileOnly 'androidx.appcompat:appcompat:1.3.1'
compileOnly 'androidx.constraintlayout:constraintlayout:2.1.3'
//anko
def anko_version = '0.10.8'
compileOnly "org.jetbrains.anko:anko-sdk27:$anko_version"
compileOnly "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
//Glide
compileOnly 'com.github.bumptech.glide:glide:4.13.1'
compileOnly 'com.squareup.okhttp3:okhttp:4.9.3'
compileOnly 'com.google.code.gson:gson:2.9.0'
compileOnly 'com.journeyapps:zxing-android-embedded:3.5.0'
compileOnly 'org.greenrobot:greendao:3.3.0' compileOnly 'org.greenrobot:greendao:3.3.0'
compileOnly 'com.github.yuweiguocn:GreenDaoUpgradeHelper:v2.2.1'
//JSoup
compileOnly 'org.jsoup:jsoup:1.14.3'
compileOnly 'cn.wanghaomiao:JsoupXpath:2.5.1'
compileOnly 'com.jayway.jsonpath:json-path:2.7.0'
//SmartRefreshLayout
compileOnly 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2'
compileOnly 'com.scwang.smartrefresh:SmartRefreshHeader:1.1.2'
compileOnly 'com.google.android.material:material:1.4.0'
//Scroller
compileOnly 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
//Toasty
compileOnly 'com.github.GrenderG:Toasty:1.5.0'
//
compileOnly 'net.ricecode:string-similarity:1.0.0'
compileOnly 'com.jayway.jsonpath:json-path:2.7.0'
//RxAndroid
compileOnly 'io.reactivex.rxjava2:rxjava:2.2.19'
compileOnly 'io.reactivex.rxjava2:rxandroid:2.1.1'
//ImmersionBar
compileOnly 'com.gyf.immersionbar:immersionbar:3.0.0'
//
compileOnly 'com.luhuiguo:chinese-utils:1.0'
//
compileOnly 'com.jaredrummler:colorpicker:1.1.0'
//
compileOnly 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7'
//
compileOnly 'com.github.albfernandez:juniversalchardet:2.4.0'
// https://github.com/hongyangAndroid/FlowLayout
compileOnly 'com.hyman:flowlayout-lib:1.1.2'
compileOnly 'com.liulishuo.filedownloader:library:1.7.7'
//SwipeBackLayout
compileOnly 'me.imid.swipebacklayout.lib:library:1.1.0'
//JS
//noinspection GradleDependency
compileOnly 'com.github.gedoor:rhino-android:1.6'
//XXPermissions
compileOnly 'com.github.getActivity:XXPermissions:11.2'
//epub
compileOnly('com.positiondev.epublib:epublib-core:3.1') {
exclude group: 'org.slf4j'
exclude group: 'xmlpull'
}
compileOnly 'com.nshmura:recyclertablayout:1.5.0'
// https://mvnrepository.com/artifact/net.lingala.zip4j/zip4j
compileOnly group: 'net.lingala.zip4j', name: 'zip4j', version: '2.9.1'
//
def coroutines_version = '1.5.1'
compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")
//apache
compileOnly('org.apache.commons:commons-text:1.9')
//https://github.com/fengyuecanzhu/Maple
compileOnly("me.fycz.maple:maple:1.9")
compileOnly project(":app") compileOnly project(":app")
compileOnly project(":DialogX") compileOnly project(":DialogX")
} }

@ -18,14 +18,14 @@
package xyz.fycz.dynamic package xyz.fycz.dynamic
import android.util.Base64
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import me.fycz.maple.MapleUtils
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.fycz.dynamic.fix.App243Fix
import xyz.fycz.myreader.application.App
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
@ -37,5 +37,6 @@ class ExampleInstrumentedTest {
@Test @Test
fun testFix() { fun testFix() {
// Context of the app under test. // Context of the app under test.
Log.d("asdas", Base64.encodeToString("asdasd".toByteArray(), Base64.NO_WRAP))
} }
} }

@ -18,7 +18,6 @@
package xyz.fycz.dynamic package xyz.fycz.dynamic
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
@ -28,7 +27,11 @@ import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodHook import me.fycz.maple.MethodHook
import xyz.fycz.dynamic.fix.* import xyz.fycz.dynamic.fix.*
import xyz.fycz.myreader.application.App import xyz.fycz.myreader.application.App
import xyz.fycz.myreader.entity.PluginConfig
import xyz.fycz.myreader.ui.activity.MainActivity import xyz.fycz.myreader.ui.activity.MainActivity
import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.utils.GSON
import xyz.fycz.myreader.util.utils.fromJsonObject
/** /**
* @author fengyue * @author fengyue
@ -40,41 +43,40 @@ class AppLoadImpl : IAppLoader {
private const val spuName = "FYReader_plugin" private const val spuName = "FYReader_plugin"
val spu: SharedPreferences = val spu: SharedPreferences =
App.getmContext().getSharedPreferences(spuName, Context.MODE_PRIVATE) App.getmContext().getSharedPreferences(spuName, Context.MODE_PRIVATE)
val allFixInfoSb = StringBuilder()
} }
private val fixList = listOf( private val fixList = listOf(
App243Fix::class.java, App243Fix::class.java,
App244Fix::class.java, App244Fix::class.java,
App244Fix2::class.java, App244Fix2::class.java,
App245Fix::class.java, App246Fix::class.java,
AppSubSourceFix::class.java,
App246Fix2::class.java,
App246Fix3::class.java,
App246Fix4::class.java,
App246Fix5::class.java,
) )
override fun onLoad(appParam: AppParam) { override fun onLoad(appParam: AppParam) {
val sb = StringBuilder() val sb = StringBuilder()
fixList.forEach { fixList.forEach {
val annotation = it.getAnnotation(AppFix::class.java)!! val annotation = it.getAnnotation(AppFix::class.java)!!
for (version in annotation.versions) { if (annotation.versions.isEmpty()) {
if (App.getVersionCode() == version) { fix(sb, annotation, it)
val fix = it.newInstance() } else {
val fixResult = fix.onFix(annotation.date) for (version in annotation.versions) {
if (!spu.getBoolean(annotation.date, false)) { if (App.getVersionCode() == version) {
if (sb.isNotEmpty()) sb.append("\n") fix(sb, annotation, it)
sb.append("${annotation.date}\n") break
fixResult.forEachIndexed { i, b ->
sb.append("${i + 1}${annotation.fixLog[i]}${if (b) "成功" else "失败"}\n")
}
spu.edit().run {
putBoolean(annotation.date, true)
apply()
}
} }
break
} }
} }
} }
if (sb.isNotEmpty()) { if (sb.isNotEmpty()) {
if (sb.endsWith("\n")) sb.substring(0, sb.length - 1) if (sb.endsWith("\n")) sb.substring(0, sb.length - 1)
val key = "fix244-2" val key = "fix2022-07-02"
val hasRead = spu.getBoolean(key, false) val hasRead = spu.getBoolean(key, false)
if (!hasRead) { if (!hasRead) {
announce("插件更新", "更新内容:\n$sb") announce("插件更新", "更新内容:\n$sb")
@ -86,6 +88,33 @@ class AppLoadImpl : IAppLoader {
} }
} }
private fun fix(
sb: StringBuilder,
annotation: AppFix,
fixClz: Class<out AppFixHandle>
) {
val fix = fixClz.newInstance()
val fixResult = fix.onFix(annotation.date)
//所有修复结果信息
if (allFixInfoSb.isNotEmpty()) allFixInfoSb.append("\n")
allFixInfoSb.append("${annotation.date}\n")
fixResult.forEachIndexed { i, b ->
allFixInfoSb.append("${i + 1}${annotation.fixLog[i]}${if (b) "成功" else "失败"}\n")
}
//需要提示的修复结果信息
if (!spu.getBoolean(annotation.date, false)) {
if (sb.isNotEmpty()) sb.append("\n")
sb.append("${annotation.date}\n")
fixResult.forEachIndexed { i, b ->
sb.append("${i + 1}${annotation.fixLog[i]}${if (b) "成功" else "失败"}\n")
}
spu.edit().run {
putBoolean(annotation.date, true)
apply()
}
}
}
private fun announce(title: String, msg: String) { private fun announce(title: String, msg: String) {
try { try {
MapleUtils.findAndHookMethod( MapleUtils.findAndHookMethod(
@ -105,4 +134,11 @@ class AppLoadImpl : IAppLoader {
MapleUtils.log(e) MapleUtils.log(e)
} }
} }
}
fun getPluginLoadInfo(): String {
val pluginConfig = GSON.fromJsonObject<PluginConfig>(
SharedPreUtils.getInstance().getString("pluginConfig")
) ?: PluginConfig("dynamic.dex", 100)
return "当前版本更新日志:\n${pluginConfig.changelog}\n\n插件加载结果:\n" + allFixInfoSb.toString()
}
}

@ -39,25 +39,11 @@ import xyz.fycz.myreader.util.utils.AdUtils
class App243Fix : AppFixHandle { class App243Fix : AppFixHandle {
override fun onFix(key: String): BooleanArray { override fun onFix(key: String): BooleanArray {
var fx1 = false return handleFix(
var fx2 = false key,
try { "getAllNoLocalSource" to { fixGetAllNoLocalSource() },
fixGetAllNoLocalSource() "adTimeout" to { fixAdTimeout() },
fx1 = true )
fixResult(key, "getAllNoLocalSource", true)
} catch (e: Exception) {
MapleUtils.log(e)
fixResult(key, "getAllNoLocalSource", false)
}
try {
fixAdTimeout()
fx2 = true
fixResult(key, "adTimeout", true)
} catch (e: Exception) {
MapleUtils.log(e)
fixResult(key, "adTimeout", false)
}
return booleanArrayOf(fx1, fx2)
} }
private fun getAllNoLocalSource(): List<BookSource> { private fun getAllNoLocalSource(): List<BookSource> {

@ -36,16 +36,10 @@ import java.io.*
@AppFix([243, 244], ["修复书籍无法导出缓存的问题"], "2022-04-26") @AppFix([243, 244], ["修复书籍无法导出缓存的问题"], "2022-04-26")
class App244Fix: AppFixHandle { class App244Fix: AppFixHandle {
override fun onFix(key: String): BooleanArray { override fun onFix(key: String): BooleanArray {
var fx = false return handleFix(
try { key,
fixUnionChapterCathe() "unionChapterCathe" to { fixUnionChapterCathe() },
fx = true )
fixResult(key, "unionChapterCathe", true)
} catch (e: Exception) {
MapleUtils.log(e)
fixResult(key, "unionChapterCathe", false)
}
return booleanArrayOf(fx)
} }
private fun fixUnionChapterCathe() { private fun fixUnionChapterCathe() {

@ -32,16 +32,10 @@ import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler
@AppFix([243, 244], ["修复搜索时当前分组不存在时无法搜索的问题"], "2022-05-11") @AppFix([243, 244], ["修复搜索时当前分组不存在时无法搜索的问题"], "2022-05-11")
class App244Fix2 : AppFixHandle{ class App244Fix2 : AppFixHandle{
override fun onFix(key: String): BooleanArray { override fun onFix(key: String): BooleanArray {
var fx = false return handleFix(
try { key,
fixGetEnableReadCrawlers() "getEnableReadCrawlers" to { fixGetEnableReadCrawlers() },
fx = true )
fixResult(key, "getEnableReadCrawlers", true)
} catch (e: Exception) {
MapleUtils.log(e)
fixResult(key, "getEnableReadCrawlers", false)
}
return booleanArrayOf(fx)
} }
private fun fixGetEnableReadCrawlers() { private fun fixGetEnableReadCrawlers() {

@ -1,81 +0,0 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.fix
import me.fycz.maple.MapleBridge
import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodReplacement
import xyz.fycz.myreader.application.App
import xyz.fycz.myreader.model.user.UserService
import xyz.fycz.myreader.util.AppInfoUtils
import xyz.fycz.myreader.util.utils.EncoderUtils
/**
* @author fengyue
* @date 2022/5/23 18:20
*/
@AppFix([243, 244, 245], ["修改用户服务验证机制(beta)"], "2022-05-23")
class App245Fix : AppFixHandle {
override fun onFix(key: String): BooleanArray {
val result = try {
fixMakeAuth()
true
} catch (e: Exception) {
MapleUtils.log(e)
false
}
fixResult(key, "makeAuth", result)
return booleanArrayOf(result)
}
private fun fixMakeAuth() {
MapleUtils.findAndHookMethod(
UserService::class.java,
"makeAuth",
object : MethodReplacement() {
override fun replaceHookedMethod(param: MapleBridge.MethodHookParam): Any {
return makeAuth()
}
}
)
}
fun makeAuth(): String {
var auth = "signal=" + AppInfoUtils.getSingInfo(
App.getmContext(),
App.getApplication().packageName,
AppInfoUtils.SHA1
) + "&appVersion=" + App.getVersionCode()
auth = try {
EncoderUtils.encryptAES2Base64(
auth.toByteArray(), DO_FILTER_KEY,
"AES/ECB/PKCS5Padding"
)?.let { String(it) }.toString()
} catch (e: Exception) {
""
}
return "&auth=$auth" +
"&deviceId=" + UserService.getUUID() +
"&isDebug=" + App.isDebug()
}
val DO_FILTER_KEY = "79qdunN8534y44T3".toByteArray()
}

@ -0,0 +1,106 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.fix
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.get
import androidx.viewbinding.ViewBinding
import me.fycz.maple.MapleBridge
import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodHook
import xyz.fycz.myreader.R
import xyz.fycz.myreader.base.adapter2.onClick
import xyz.fycz.myreader.ui.activity.MoreSettingActivity
import xyz.fycz.myreader.util.ToastUtils
import xyz.fycz.myreader.util.utils.FileUtils
import xyz.fycz.myreader.util.utils.ScreenUtils
/**
* @author fengyue
* @date 2022/6/3 15:34
*/
@AppFix([243, 244, 245, 246], ["[设置-缓存设置]新增清除广告文件"], "2022-06-03")
class App246Fix : AppFixHandle {
override fun onFix(key: String): BooleanArray {
return handleFix(
key,
"adFile" to { fxAdFile() },
)
}
fun fxAdFile() {
MapleUtils.findAndHookMethod(
MoreSettingActivity::class.java,
"initWidget",
object : MethodHook() {
override fun afterHookedMethod(param: MapleBridge.MethodHookParam) {
val binding =
MapleUtils.getObjectField(param.thisObject, "binding") as ViewBinding
val rootLayout =
(MapleUtils.getObjectField(
binding,
"svContent"
) as ScrollView)[0] as ViewGroup
addDeleteAdFileView(rootLayout, rootLayout.childCount)
}
}
)
}
fun addDeleteAdFileView(rootLayout: ViewGroup, index: Int) {
val context = rootLayout.context
val resources = context.resources
val rlDeleteAdFile = RelativeLayout(context)
val layoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ScreenUtils.dpToPx(50)
)
rlDeleteAdFile.setPadding(
ScreenUtils.dpToPx(20),
0,
ScreenUtils.dpToPx(20),
0
)
rlDeleteAdFile.background =
ContextCompat.getDrawable(context, R.drawable.selector_common_bg)
rlDeleteAdFile.id = R.id.rl_delete_ad_file
val textview = TextView(context)
val textViewParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
addRule(RelativeLayout.CENTER_VERTICAL)
}
textview.setText(R.string.delete_ad_file)
textview.setTextColor(resources.getColor(R.color.textSecondary))
textview.textSize =
ScreenUtils.pxToSp(resources.getDimension(R.dimen.text_normal_size).toInt()).toFloat()
rlDeleteAdFile.addView(textview, textViewParams)
rlDeleteAdFile.onClick {
FileUtils.deleteFile(FileUtils.getFilePath())
ToastUtils.showSuccess("广告文件删除成功")
}
rootLayout.addView(rlDeleteAdFile, index, layoutParams)
}
}

@ -0,0 +1,54 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.fix
import me.fycz.maple.MapleBridge
import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodHook
import xyz.fycz.myreader.widget.page.PageView
/**
* @author fengyue
* @date 2022/6/23 20:51
*/
@AppFix([243, 244, 245, 246], ["修复阅读界面概率性闪退的问题"], "2022-06-23")
class App246Fix2 : AppFixHandle {
override fun onFix(key: String): BooleanArray {
return handleFix(
key,
"pageView" to { fxPageView() },
)
}
private fun fxPageView() {
MapleUtils.findAndHookMethod(
PageView::class.java,
"onDetachedFromWindow",
object : MethodHook(){
override fun beforeHookedMethod(param: MapleBridge.MethodHookParam) {
val mPageAnim = MapleUtils.getObjectField(param.thisObject, "mPageAnim")
if (mPageAnim == null){
MapleUtils.setObjectField(param.thisObject, "mPageLoader", null)
param.result = null
}
}
}
)
}
}

@ -0,0 +1,139 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.fix
import android.annotation.SuppressLint
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.core.view.get
import androidx.core.widget.NestedScrollView
import androidx.viewbinding.ViewBinding
import me.fycz.maple.MapleBridge
import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodHook
import xyz.fycz.dynamic.AppLoadImpl
import xyz.fycz.myreader.R
import xyz.fycz.myreader.base.adapter2.onClick
import xyz.fycz.myreader.entity.PluginConfig
import xyz.fycz.myreader.ui.activity.AboutActivity
import xyz.fycz.myreader.ui.dialog.DialogCreator
import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.utils.GSON
import xyz.fycz.myreader.util.utils.ScreenUtils
import xyz.fycz.myreader.util.utils.fromJsonObject
/**
* @author fengyue
* @date 2022/6/28 19:22
*/
@AppFix([243, 244, 245, 246], ["关于界面新增插件加载结果"], "2022-06-28")
class App246Fix3 : AppFixHandle {
override fun onFix(key: String): BooleanArray {
return handleFix(
key,
"pluginView" to { fxPluginView() },
)
}
private fun fxPluginView() {
MapleUtils.findAndHookMethod(
AboutActivity::class.java,
"initWidget",
object : MethodHook() {
override fun afterHookedMethod(param: MapleBridge.MethodHookParam) {
val binding =
MapleUtils.getObjectField(param.thisObject, "binding") as ViewBinding
val ilBinding = MapleUtils.getObjectField(binding, "il") as ViewBinding
val rootLayout = (ilBinding.root as NestedScrollView)[0] as ViewGroup
addPluginView(rootLayout, 1)
}
}
)
}
@SuppressLint("SetTextI18n")
fun addPluginView(rootLayout: ViewGroup, index: Int) {
val pluginConfig = GSON.fromJsonObject<PluginConfig>(
SharedPreUtils.getInstance().getString("pluginConfig")
) ?: PluginConfig("dynamic.dex", 100)
val context = rootLayout.context
val rlPlugin = RelativeLayout(context)
val pluginLayoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ScreenUtils.dpToPx(50)
)
rlPlugin.background = ContextCompat.getDrawable(context, R.drawable.selector_common_bg)
rlPlugin.gravity = Gravity.CENTER
rlPlugin.id = R.id.rl_update
rlPlugin.setPadding(
ScreenUtils.dpToPx(20),
0,
ScreenUtils.dpToPx(10),
0
)
val textview = TextView(context)
val textviewLayoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
addRule(RelativeLayout.CENTER_VERTICAL)
}
textview.text = "插件版本:${pluginConfig.version}"
textview.setTextColor(context.resources.getColor(R.color.textPrimary))
textview.textSize =
ScreenUtils.pxToSp(
context.resources.getDimension(R.dimen.text_normal_size).toInt()
).toFloat()
rlPlugin.addView(textview, textviewLayoutParams)
val imageView = AppCompatImageView(context)
val imageViewLayoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
addRule(RelativeLayout.ALIGN_PARENT_END)
addRule(RelativeLayout.CENTER_VERTICAL)
}
imageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_right_arrow))
imageView.drawable.setTint(context.resources.getColor(R.color.textPrimary))
rlPlugin.addView(imageView, imageViewLayoutParams)
rlPlugin.onClick {
DialogCreator.createTipDialog(
context,
"插件版本:${pluginConfig.version}",
"当前版本更新日志:\n${pluginConfig.changelog}\n\n插件加载结果:\n" +
AppLoadImpl.allFixInfoSb.toString()
)
}
rootLayout.addView(rlPlugin, index, pluginLayoutParams)
val view = View(context)
val layoutParams = ViewGroup.MarginLayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ScreenUtils.dpToPx(10)
)
rootLayout.addView(view, index + 1, layoutParams)
}
}

@ -0,0 +1,84 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.fix
import me.fycz.maple.MapleBridge
import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodReplacement
import xyz.fycz.dynamic.utils.LanZouUtils
import xyz.fycz.myreader.base.observer.MyObserver
import xyz.fycz.myreader.util.utils.RxUtils
import xyz.fycz.myreader.webapi.LanZouApi
import xyz.fycz.myreader.webapi.LanZousApi
import xyz.fycz.myreader.webapi.ResultCallback
import java.lang.Exception
/**
* @author fengyue
* @date 2022/6/30 20:40
*/
@AppFix([243, 244, 245, 246], ["修复书源订阅失败的问题", "修复字体下载失败的问题"], "2022-06-30")
class App246Fix4: AppFixHandle {
override fun onFix(key: String): BooleanArray {
return handleFix(
key,
"lanZouApi" to { fxLanZouApi() },
"fontLanZouApi" to { fxFontLanZouApi() },
)
}
private fun fxLanZouApi() {
MapleUtils.findAndHookMethod(
LanZouApi::class.java,
"getFileUrl",
String::class.java,
String::class.java,
object : MethodReplacement(){
override fun replaceHookedMethod(param: MapleBridge.MethodHookParam): Any {
return LanZouUtils.getFileUrl(param.args[0] as String, param.args[1] as String)
}
}
)
}
private fun fxFontLanZouApi(){
MapleUtils.findAndHookMethod(
LanZousApi::class.java,
"getUrl",
String::class.java,
ResultCallback::class.java,
object : MethodReplacement(){
override fun replaceHookedMethod(param: MapleBridge.MethodHookParam) {
val callback = param.args[1] as ResultCallback
LanZouUtils.getFileUrl(param.args[0] as String)
.compose { RxUtils.toSimpleSingle(it) }
.subscribe(object : MyObserver<String>(){
override fun onNext(t: String) {
callback.onFinish(t, 1)
}
override fun onError(e: Throwable) {
callback.onError(e as Exception)
}
})
}
}
)
}
}

@ -0,0 +1,208 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.fix
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import io.reactivex.Observable
import me.fycz.maple.MapleBridge
import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodReplacement
import xyz.fycz.myreader.R
import xyz.fycz.myreader.application.App
import xyz.fycz.myreader.application.SysManager
import xyz.fycz.myreader.base.observer.MyObserver
import xyz.fycz.myreader.common.URLCONST
import xyz.fycz.myreader.ui.dialog.UpdateDialog
import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.ToastUtils
import xyz.fycz.myreader.util.help.StringHelper
import xyz.fycz.myreader.util.utils.NetworkUtils
import xyz.fycz.myreader.util.utils.OkHttpUtils
import xyz.fycz.myreader.util.utils.RxUtils
import xyz.fycz.myreader.webapi.LanZouApi.getFileUrl
import xyz.fycz.myreader.widget.BarPercentView
import java.io.IOException
/**
* @author fengyue
* @date 2022/7/1 15:45
*/
@AppFix([243, 244, 245, 246], ["修复检查更新失败的问题", "修复获取更新链接失败的问题"], "2022-07-02")
class App246Fix5 : AppFixHandle {
override fun onFix(key: String): BooleanArray {
return handleFix(
key,
"checkUpdateUrl" to { fxCheckUpdateUrl() },
"downloadLanzou" to { fxDownloadLanzou() },
)
}
fun fxCheckUpdateUrl() {
MapleUtils.findAndHookMethod(
App::class.java,
"checkVersionByServer",
AppCompatActivity::class.java,
Boolean::class.java,
object : MethodReplacement() {
override fun replaceHookedMethod(param: MapleBridge.MethodHookParam) {
checkUpdate(param.args[0] as AppCompatActivity, param.args[1] as Boolean)
}
}
)
}
fun checkUpdate(activity: AppCompatActivity, isManualCheck: Boolean) {
App.getApplication().newThread {
try {
var content = OkHttpUtils.getUpdateInfo()
if (StringHelper.isEmpty(content)) {
content = getBakUpdateInfo()
if (StringHelper.isEmpty(content)) {
if (isManualCheck || NetworkUtils.isNetWorkAvailable()) {
ToastUtils.showError("检查更新失败!")
}
return@newThread
}
}
val contents = content.split(";".toRegex())
val newestVersion = contents[0].substring(contents[0].indexOf(":") + 1).toInt()
var isForceUpdate = contents[1].substring(contents[1].indexOf(":") + 1).toBoolean()
val downloadLink =
contents[2].substring(contents[2].indexOf(":") + 1).trim { it <= ' ' }
val updateContent = contents[3].substring(contents[3].indexOf(":") + 1)
SharedPreUtils.getInstance().putString(
App.getmContext().getString(R.string.lanzousKeyStart),
contents[4].substring(contents[4].indexOf(":") + 1)
)
val newSplashTime = contents[5].substring(contents[5].indexOf(":") + 1)
val oldSplashTime = SharedPreUtils.getInstance().getString("splashTime")
SharedPreUtils.getInstance()
.putBoolean("needUdSI", oldSplashTime != newSplashTime)
SharedPreUtils.getInstance().putString(
"splashTime",
contents[5].substring(contents[5].indexOf(":") + 1)
)
SharedPreUtils.getInstance().putString(
"splashImageUrl",
contents[6].substring(contents[6].indexOf(":") + 1)
)
SharedPreUtils.getInstance().putString(
"splashImageMD5",
contents[7].substring(contents[7].indexOf(":") + 1)
)
val forceUpdateVersion = contents[8].substring(contents[8].indexOf(":") + 1).toInt()
SharedPreUtils.getInstance().putInt("forceUpdateVersion", forceUpdateVersion)
val domain = contents[9].substring(contents[9].indexOf(":") + 1)
SharedPreUtils.getInstance().putString("domain", domain)
val pluginConfigUrl = contents[10].substring(contents[10].indexOf(":") + 1)
SharedPreUtils.getInstance().putString("pluginConfigUrl", pluginConfigUrl)
val versionCode = App.getVersionCode()
isForceUpdate = isForceUpdate && forceUpdateVersion > versionCode
if (!StringHelper.isEmpty(downloadLink)) {
SharedPreUtils.getInstance()
.putString(App.getmContext().getString(R.string.downloadLink), downloadLink)
} else {
SharedPreUtils.getInstance().putString(
App.getmContext().getString(R.string.downloadLink),
URLCONST.APP_DIR_URL
)
}
val updateContents = updateContent.split("/".toRegex())
val s = StringBuilder()
updateContents.forEach {
s.append(it)
s.append("<br>")
}
Log.i("检查更新,最新版本", newestVersion.toString() + "")
if (newestVersion > versionCode) {
val setting = SysManager.getSetting()
if (isManualCheck || setting.newestVersionCode < newestVersion || isForceUpdate) {
setting.newestVersionCode = newestVersion
SysManager.saveSetting(setting)
App.getApplication().updateApp2(
activity, downloadLink, newestVersion, s.toString(), isForceUpdate
)
}
} else if (isManualCheck) {
ToastUtils.showSuccess("已经是最新版本!")
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("检查更新失败!", "" + e.localizedMessage)
if (isManualCheck || NetworkUtils.isNetWorkAvailable()) {
ToastUtils.showError("检查更新失败!")
}
}
}
}
private fun fxDownloadLanzou() {
MapleUtils.findAndHookMethod(
UpdateDialog::class.java,
"downloadWithLanzous",
String::class.java,
object : MethodReplacement() {
override fun replaceHookedMethod(param: MapleBridge.MethodHookParam) {
val updateDialog = param.thisObject as UpdateDialog
val binding = MapleUtils.getObjectField(updateDialog, "binding")
as ViewBinding
val tvProgress = MapleUtils.getObjectField(binding, "tvProgress")
as TextView
val barPercentView = MapleUtils.getObjectField(binding, "barPercentView")
as BarPercentView
tvProgress.text = "正在获取下载链接..."
barPercentView.setPercentage(0F)
val apkUrl = param.args[0] as String
getFileUrl(apkUrl)
.compose { RxUtils.toSimpleSingle(it) }
.subscribe(object : MyObserver<String>() {
override fun onNext(directUrl: String) {
MapleUtils.callMethod(
updateDialog,
"downloadApkNormal",
arrayOf(String::class.java),
directUrl
)
}
override fun onError(e: Throwable) {
MapleUtils.callMethod(
updateDialog,
"error"
)
}
})
}
}
)
}
@Throws(IOException::class)
fun getBakUpdateInfo(): String {
return OkHttpUtils.getHtml(
"https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/FYReader-Update/" +
(if (App.isDebug()) "debug" else "release") +
"/content.txt"
)
}
}

@ -33,9 +33,25 @@ import xyz.fycz.myreader.util.utils.AdUtils
* @date 2022/4/25 21:49 * @date 2022/4/25 21:49
*/ */
interface AppFixHandle { interface AppFixHandle {
fun onFix(key: String): BooleanArray fun onFix(key: String): BooleanArray
fun handleFix(key: String, vararg fix: Pair<String, () -> Unit>): BooleanArray {
val results = mutableListOf<Boolean>()
fix.forEach {
val result = try {
it.second()
true
} catch (e: Exception) {
MapleUtils.log(e)
false
}
results.add(result)
fixResult(key, it.first, result)
}
return results.toBooleanArray()
}
fun fixResult(key: String, name: String, success: Boolean) { fun fixResult(key: String, name: String, success: Boolean) {
val res = if (success) "Success" else "Failed" val res = if (success) "Success" else "Failed"
if (!spu.getBoolean("$key-$name-$res", false)) { if (!spu.getBoolean("$key-$name-$res", false)) {

@ -0,0 +1,57 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.fix
import me.fycz.maple.MapleBridge
import me.fycz.maple.MapleUtils
import me.fycz.maple.MethodHook
import xyz.fycz.myreader.common.URLCONST
import xyz.fycz.myreader.webapi.LanZouApi
/**
* @author fengyue
* @date 2022/6/21 18:30
*/
@AppFix([], ["更新订阅书源链接,仅支持v2.4.3版本及以上版本"], "2022-06-21")
class AppSubSourceFix : AppFixHandle {
override fun onFix(key: String): BooleanArray {
return handleFix(
key,
"subSource" to { fxSubSource() },
)
}
private fun fxSubSource() {
MapleUtils.findAndHookMethod(
LanZouApi::class.java,
"getFoldFiles",
String::class.java,
Int::class.java,
String::class.java,
object : MethodHook() {
override fun beforeHookedMethod(param: MapleBridge.MethodHookParam) {
if (param.args[0] == URLCONST.SUB_SOURCE_URL) {
param.args[0] = "https://fycz.lanzoum.com/b00pucrch"
param.args[2] = "b0ox"
}
}
}
)
}
}

@ -0,0 +1,149 @@
/*
* This file is part of FYReader.
* FYReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FYReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FYReader. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 - 2022 fengyuecanzhu
*/
package xyz.fycz.dynamic.utils
import android.util.Log
import io.reactivex.Observable
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.Jsoup
import xyz.fycz.myreader.common.URLCONST
import xyz.fycz.myreader.entity.lanzou.LanZouParseBean
import xyz.fycz.myreader.util.help.StringHelper
import xyz.fycz.myreader.util.utils.GSON
import xyz.fycz.myreader.util.utils.OkHttpUtils
import xyz.fycz.myreader.util.utils.StringUtils
import xyz.fycz.myreader.util.utils.fromJsonObject
import java.net.HttpURLConnection
import java.net.URL
/**
* @author fengyue
* @date 2022/1/22 18:50
*/
object LanZouUtils {
/**
* 通过api获取蓝奏云可下载直链
*
* @param url
* @param password
*/
fun getFileUrl(url: String, password: String = ""): Observable<String> {
return Observable.create {
var html = OkHttpUtils.getHtml(url)
val url2 = if (password.isEmpty()) {
val url1 = getUrl1(html)
html = OkHttpUtils.getHtml(url1)
val data = getDataString(html)
Log.d("LanZouUtils", "data:$data")
val key = getKeyValueByKey(html, data, "sign") +
"&" + getKeyValueByKey(html, data, "websignkey")
Log.d("LanZouUtils", "key:$key")
getUrl2(key, url1)
} else {
getUrl2(StringHelper.getSubString(html, "sign=", "&"), url, password)
}
if (url2.contains("file")) {
it.onNext(getRedirectUrl(url2))
} else {
it.onError(Throwable(url2))
}
it.onComplete()
}
}
fun getUrl1(html: String): String {
val doc = Jsoup.parse(html)
return URLCONST.LAN_ZOU_URL + doc.getElementsByTag("iframe").attr("src")
}
fun getKeyValueByKey(html: String, data: String, key: String): String {
val keyName = StringHelper.getSubString(data, "'$key':", ",")
return if (keyName.endsWith("'")) {
key + "=" + keyName.replace("'", "")
} else {
val lanzousKeyStart = "var $keyName = '"
key + "=" + StringHelper.getSubString(html, lanzousKeyStart, "'")
}
}
fun getUrl2(key: String, referer: String, password: String = ""): String {
val mediaType = "application/x-www-form-urlencoded".toMediaTypeOrNull()
val body = if (password.isEmpty()) {
"action=downprocess&signs=?ctdf&websign=&ves=1&$key"
} else {
"action=downprocess&sign=$key&p=$password"
}
val requestBody = body.toRequestBody(mediaType)
val headers = HashMap<String, String>()
headers["Referer"] = referer
val html = OkHttpUtils.getHtml(
URLCONST.LAN_ZOU_URL + "/ajaxm.php", requestBody,
"UTF-8", headers
)
return getUrl2(html)
}
private fun getUrl2(o: String): String {
val lanZouBean = GSON.fromJsonObject<LanZouParseBean>(o)
lanZouBean?.run {
return if (zt == 1) {
"$dom/file/$url"
} else {
"解析失败\n信息:$inf"
}
}
return ""
}
/**
* 获取重定向地址
*
* @param path
*/
fun getRedirectUrl(path: String): String {
val conn = URL(path)
.openConnection() as HttpURLConnection
conn.instanceFollowRedirects = false
conn.connectTimeout = 5000
conn.setRequestProperty(
"User-Agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"
)
conn.setRequestProperty("Accept-Language", "zh-cn")
conn.setRequestProperty("Connection", "Keep-Alive")
conn.setRequestProperty(
"Accept",
"image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/x-silverlight, */*"
)
conn.connect()
val redirectUrl = conn.getHeaderField("Location")
conn.disconnect()
return redirectUrl
}
fun getDataString(html: String): String {
val start = html.lastIndexOf("data :") + "data :".length
val end = html.indexOf("},", start) + 1
return html.substring(start, end)
}
}

@ -20,3 +20,4 @@ include ':app'
include ':DialogX' include ':DialogX'
include ':dynamic' include ':dynamic'
include ':user' include ':user'
include ':plugin'

Loading…
Cancel
Save