From ac07c5d6705ea1a3912d4aa6f72d19029cc334e3 Mon Sep 17 00:00:00 2001 From: fengyuecanzhu <1021300691@qq.com> Date: Wed, 7 Oct 2020 18:36:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EWebDav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/caches/build_file_checksums.ser | Bin 537 -> 537 bytes README.md | 4 +- app/build.gradle | 8 + app/src/main/AndroidManifest.xml | 1 + app/src/main/assets/updatelog.fy | 8 +- .../myreader/application/MyApplication.java | 15 +- .../myreader/base/observer/MyObserver.java | 23 + .../base/observer/MySingleObserver.java | 17 + .../xyz/fycz/myreader/common/APPCONST.java | 3 + .../java/xyz/fycz/myreader/common/Common.java | 11 - .../greendao/service/BookService.java | 4 +- .../xyz/fycz/myreader/model/SearchEngine.java | 47 +- .../myreader/model/backup/UserService.java | 134 ++-- .../xyz/fycz/myreader/model/storage/Backup.kt | 178 ++++++ .../myreader/model/storage/BackupRestoreUi.kt | 239 +++++++ .../myreader/model/storage/Preferences.kt | 48 ++ .../fycz/myreader/model/storage/Restore.kt | 137 ++++ .../fycz/myreader/model/storage/WebDavHelp.kt | 121 ++++ .../myreader/ui/activity/AboutActivity.java | 3 +- .../ui/activity/BookDetailedActivity.java | 11 +- .../ui/activity/BookstoreActivity.java | 8 +- .../myreader/ui/activity/MainActivity.java | 4 +- .../ui/activity/MoreSettingActivity.java | 29 +- .../myreader/ui/activity/ReadActivity.java | 2 + .../ui/activity/SearchBookActivity.java | 8 +- .../ui/activity/WebDavSettingActivity.java | 131 ++++ .../myreader/ui/adapter/BookcaseAdapter.java | 3 +- .../ui/adapter/BookcaseDetailedAdapter.java | 8 +- .../ui/adapter/BookcaseDragAdapter.java | 8 +- .../ui/adapter/SearchBookAdapter.java | 9 +- .../adapter/holder/BookStoreBookHolder.java | 11 +- .../ui/adapter/holder/SearchBookHolder.java | 70 +- .../myreader/ui/dialog/MyAlertDialog.java | 76 ++- .../myreader/ui/fragment/MineFragment.java | 113 +++- .../ui/presenter/BookcasePresenter.java | 53 +- .../myreader/ui/presenter/ReadPresenter.java | 6 +- .../java/xyz/fycz/myreader/util/HttpUtil.java | 49 +- .../fycz/myreader/util/SharedPreUtils.java | 40 +- .../xyz/fycz/myreader/util/StringHelper.java | 6 +- .../myreader/util/utils/DocumentUtil.java | 387 +++++++++++ .../fycz/myreader/util/utils/FileUtils.java | 601 +++++++++++++++++- .../myreader/util/utils/GsonExtensions.kt | 43 ++ .../fycz/myreader/util/utils/ImageLoader.kt | 50 ++ .../xyz/fycz/myreader/util/webdav/README.md | 1 + .../xyz/fycz/myreader/util/webdav/WebDav.kt | 250 ++++++++ .../fycz/myreader/util/webdav/http/Handler.kt | 16 + .../myreader/util/webdav/http/HttpAuth.kt | 9 + .../xyz/fycz/myreader/webapi/CommonApi.java | 56 +- .../xyz/fycz/myreader/webapi/LanZousApi.java | 3 +- .../webapi/crawler/ReadCrawlerUtil.java | 10 +- .../webapi/crawler/find/QiDianMobileRank.java | 3 +- .../fycz/myreader/widget/CoverImageView.kt | 158 +++++ .../myreader/widget/page/LocalPageLoader.java | 416 ++++++------ .../main/res/layout/activity_more_setting.xml | 22 + .../main/res/layout/activity_search_book.xml | 3 +- .../res/layout/activity_webdav_setting.xml | 105 +++ app/src/main/res/layout/edit_dialog.xml | 3 +- .../layout/gridview_book_detailed_item.xml | 5 +- .../main/res/layout/gridview_book_item.xml | 12 +- .../res/layout/layout_book_detail_header.xml | 15 +- .../layout/listview_book_store_book_item.xml | 6 +- .../res/layout/listview_search_book_item.xml | 30 +- app/src/main/res/values/strings.xml | 20 + app/version_code.properties | 4 +- build.gradle | 8 +- 65 files changed, 3392 insertions(+), 490 deletions(-) create mode 100644 app/src/main/java/xyz/fycz/myreader/base/observer/MyObserver.java create mode 100644 app/src/main/java/xyz/fycz/myreader/base/observer/MySingleObserver.java delete mode 100644 app/src/main/java/xyz/fycz/myreader/common/Common.java create mode 100644 app/src/main/java/xyz/fycz/myreader/model/storage/Backup.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/model/storage/BackupRestoreUi.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/model/storage/Preferences.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/model/storage/Restore.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/model/storage/WebDavHelp.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/ui/activity/WebDavSettingActivity.java create mode 100644 app/src/main/java/xyz/fycz/myreader/util/utils/DocumentUtil.java create mode 100644 app/src/main/java/xyz/fycz/myreader/util/utils/GsonExtensions.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/util/utils/ImageLoader.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/util/webdav/README.md create mode 100644 app/src/main/java/xyz/fycz/myreader/util/webdav/WebDav.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/util/webdav/http/Handler.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/util/webdav/http/HttpAuth.kt create mode 100644 app/src/main/java/xyz/fycz/myreader/widget/CoverImageView.kt create mode 100644 app/src/main/res/layout/activity_webdav_setting.xml diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index c297635d9a49daed509d771626e059759d257615..f4879e1ffd628365a746394a72e69805107266b8 100644 GIT binary patch delta 56 zcmV-80LTBC1epYom;~{jjZTrAcMxQIs!C^b^>2wxU!@vanM#u*0g4a|VkiDgrE~^Vk;RiE0g4cIsYgB65Eij9 OJ>DS;5oha@y#ZVkT^cO_ diff --git a/README.md b/README.md index fa52d82..9120666 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,12 @@ 发现界面:排行榜、分类、书城 - 详细功能可查看图片或下载自行体验 + 详细功能可查看图片或下载自行体验或自行编译 demo下载:https://fycz.lanzoui.com/iBofFh42pxg +如有问题请加QQ群:1085028304 + ![Image](https://github.com/fengyuecanzhu/FYReader/tree/master/img/1.png) ![Image](https://github.com/fengyuecanzhu/FYReader/tree/master/img/2.png) ![Image](https://github.com/fengyuecanzhu/FYReader/tree/master/img/3.png) diff --git a/app/build.gradle b/app/build.gradle index 637f1a4..2c03329 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'org.greenrobot.greendao'//greendao插件dependencies +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' def releaseTime() { return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+08:00")) @@ -78,7 +80,13 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') + api 'androidx.core:core-ktx:1.3.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + //anko + def anko_version = '0.10.8' + implementation "org.jetbrains.anko:anko-sdk27:$anko_version" + implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" implementation 'com.jakewharton:butterknife:10.0.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dd10e23..e66becc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,6 +122,7 @@ + diff --git a/app/src/main/assets/updatelog.fy b/app/src/main/assets/updatelog.fy index ae88e67..e998b38 100644 --- a/app/src/main/assets/updatelog.fy +++ b/app/src/main/assets/updatelog.fy @@ -1,5 +1,11 @@ +2020.10.03 +风月读书v1.20.100315 +1、优化搜索书籍高频刷新的问题 +2、优化书籍默认封面显示 +3、修复换源对话框书源重复的问题 + 2020.10.02 -风月读书v1.20.100120 +风月读书v1.20.100220 1、新增书源:搜小说网、全小说网、奇奇小说 2、新增停止搜索按钮 3、优化搜索书籍显示 diff --git a/app/src/main/java/xyz/fycz/myreader/application/MyApplication.java b/app/src/main/java/xyz/fycz/myreader/application/MyApplication.java index 4deef83..24976bb 100644 --- a/app/src/main/java/xyz/fycz/myreader/application/MyApplication.java +++ b/app/src/main/java/xyz/fycz/myreader/application/MyApplication.java @@ -40,6 +40,7 @@ import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import xyz.fycz.myreader.R; import xyz.fycz.myreader.base.BaseActivity; import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.common.URLCONST; @@ -95,7 +96,7 @@ public class MyApplication extends Application { } public boolean isNightFS() { - return SharedPreUtils.getInstance().getBoolean("isNightFS", false); + return SharedPreUtils.getInstance().getBoolean(getString(R.string.isNightFS), false); } /** @@ -103,7 +104,7 @@ public class MyApplication extends Application { * @param isNightMode */ public void setNightTheme(boolean isNightMode) { - SharedPreUtils.getInstance().putBoolean("isNightFS", false); + SharedPreUtils.getInstance().putBoolean(getmContext().getString(R.string.isNightFS), false); Setting setting = SysManager.getSetting(); setting.setDayStyle(!isNightMode); SysManager.saveSetting(setting); @@ -181,6 +182,10 @@ public class MyApplication extends Application { handler.post(runnable); } + public static Handler getHandler(){ + return handler; + } + public static MyApplication getApplication() { return application; } @@ -313,11 +318,11 @@ public class MyApplication extends Application { downloadLink = contents[2].substring(contents[2].indexOf(":") + 1).trim(); updateContent = contents[3].substring(contents[3].indexOf(":") + 1); SharedPreUtils spu = SharedPreUtils.getInstance(); - spu.putString("lanzousKeyStart", contents[4].substring(contents[4].indexOf(":") + 1)); + spu.putString(getmContext().getString(R.string.lanzousKeyStart), contents[4].substring(contents[4].indexOf(":") + 1)); if (!StringHelper.isEmpty(downloadLink)) { - spu.putString("downloadLink", downloadLink); + spu.putString(getmContext().getString(R.string.downloadLink), downloadLink); } else { - spu.putString("downloadLink", URLCONST.APP_DIR_UR); + spu.putString(getmContext().getString(R.string.downloadLink), URLCONST.APP_DIR_UR); } String[] updateContents = updateContent.split("/"); for (String string : updateContents) { diff --git a/app/src/main/java/xyz/fycz/myreader/base/observer/MyObserver.java b/app/src/main/java/xyz/fycz/myreader/base/observer/MyObserver.java new file mode 100644 index 0000000..633e8a8 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/base/observer/MyObserver.java @@ -0,0 +1,23 @@ +//Copyright (c) 2017. 章钦豪. All rights reserved. +package xyz.fycz.myreader.base.observer; + +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; + +public abstract class MyObserver implements Observer { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } +} diff --git a/app/src/main/java/xyz/fycz/myreader/base/observer/MySingleObserver.java b/app/src/main/java/xyz/fycz/myreader/base/observer/MySingleObserver.java new file mode 100644 index 0000000..154b9cb --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/base/observer/MySingleObserver.java @@ -0,0 +1,17 @@ +package xyz.fycz.myreader.base.observer; + +import io.reactivex.SingleObserver; +import io.reactivex.disposables.Disposable; + +public abstract class MySingleObserver implements SingleObserver { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + + } +} 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 c893364..e0843dd 100644 --- a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java +++ b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java @@ -84,4 +84,7 @@ public class APPCONST { public static final String FORMAT_FILE_DATE = "yyyy-MM-dd"; public final static String channelIdDownload = "channel_download"; + + public static final String DEFAULT_WEB_DAV_URL = "https://dav.jianguoyun.com/dav/"; + } diff --git a/app/src/main/java/xyz/fycz/myreader/common/Common.java b/app/src/main/java/xyz/fycz/myreader/common/Common.java deleted file mode 100644 index ea1cb64..0000000 --- a/app/src/main/java/xyz/fycz/myreader/common/Common.java +++ /dev/null @@ -1,11 +0,0 @@ -package xyz.fycz.myreader.common; - - - -public interface Common { - - int SUCCESS = 1; - int INSUCCESS = 0; - int LOGINEXIT = 2; - int MESSAGE_HINT = 3; -} diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/service/BookService.java b/app/src/main/java/xyz/fycz/myreader/greendao/service/BookService.java index d725ba1..ac1eb27 100644 --- a/app/src/main/java/xyz/fycz/myreader/greendao/service/BookService.java +++ b/app/src/main/java/xyz/fycz/myreader/greendao/service/BookService.java @@ -6,6 +6,8 @@ import android.text.TextUtils; import net.ricecode.similarity.JaroWinklerStrategy; import net.ricecode.similarity.StringSimilarityService; import net.ricecode.similarity.StringSimilarityServiceImpl; +import xyz.fycz.myreader.R; +import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.application.SysManager; import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.greendao.entity.Chapter; @@ -101,7 +103,7 @@ public class BookService extends BaseService { book.setSortCode(0); book.setGroupSort(0); - book.setGroupId(SharedPreUtils.getInstance().getString("curBookGroupId", "")); + book.setGroupId(SharedPreUtils.getInstance().getString(MyApplication.getmContext().getString(R.string.curBookGroupId), "")); if (StringHelper.isEmpty(book.getId())) { book.setId(StringHelper.getStringRandom(25)); } diff --git a/app/src/main/java/xyz/fycz/myreader/model/SearchEngine.java b/app/src/main/java/xyz/fycz/myreader/model/SearchEngine.java index c6d040a..3e97682 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/SearchEngine.java +++ b/app/src/main/java/xyz/fycz/myreader/model/SearchEngine.java @@ -7,13 +7,19 @@ import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; +import xyz.fycz.myreader.R; +import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.entity.SearchBookBean; import xyz.fycz.myreader.greendao.entity.Book; +import xyz.fycz.myreader.greendao.entity.Chapter; +import xyz.fycz.myreader.greendao.service.BookMarkService; import xyz.fycz.myreader.model.mulvalmap.ConcurrentMultiValueMap; import xyz.fycz.myreader.util.SharedPreUtils; import xyz.fycz.myreader.util.ToastUtils; import xyz.fycz.myreader.webapi.CommonApi; +import xyz.fycz.myreader.webapi.crawler.base.BookInfoCrawler; import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler; import java.io.UnsupportedEncodingException; @@ -44,7 +50,7 @@ public class SearchEngine { private OnSearchListener searchListener; public SearchEngine() { - threadsNum = SharedPreUtils.getInstance().getInt("threadNum", 8); + threadsNum = SharedPreUtils.getInstance().getInt(MyApplication.getmContext().getString(R.string.threadNum), 8); } public void setOnSearchListener(OnSearchListener searchListener) { @@ -237,6 +243,33 @@ public class SearchEngine { } + public synchronized void getBookInfo(Book book, BookInfoCrawler bic, OnGetBookInfoListener listener){ + CommonApi.getBookInfo(book, bic) + .subscribeOn(scheduler) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { + + } + + @Override + public void onNext(@io.reactivex.annotations.NonNull Book book) { + listener.loadFinish(true); + } + + @Override + public void onError(@io.reactivex.annotations.NonNull Throwable e) { + listener.loadFinish(false); + } + + @Override + public void onComplete() { + + } + }); + } + /************************************************************************/ public interface OnSearchListener { @@ -250,4 +283,16 @@ public class SearchEngine { void searchBookError(Throwable throwable); } + + public interface OnGetBookInfoListener{ + void loadFinish(Boolean isSuccess); + } + + public interface OnGetBookChaptersListener{ + void loadFinish(List chapters, Boolean isSuccess); + } + + public interface OnGetChapterContentListener{ + void loadFinish(String content, Boolean isSuccess); + } } diff --git a/app/src/main/java/xyz/fycz/myreader/model/backup/UserService.java b/app/src/main/java/xyz/fycz/myreader/model/backup/UserService.java index c7aa256..be9c073 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/backup/UserService.java +++ b/app/src/main/java/xyz/fycz/myreader/model/backup/UserService.java @@ -1,6 +1,9 @@ package xyz.fycz.myreader.model.backup; +import io.reactivex.annotations.NonNull; import xyz.fycz.myreader.application.MyApplication; +import xyz.fycz.myreader.model.storage.Backup; +import xyz.fycz.myreader.model.storage.Restore; import xyz.fycz.myreader.webapi.callback.ResultCallback; import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.common.URLCONST; @@ -190,68 +193,80 @@ public class UserService { * 网络备份 * @return */ - public static boolean webBackup(){ + public static void webBackup(ResultCallback rc){ Map userInfo = readConfig(); if (userInfo == null){ - return false; - } - BackupAndRestore bar = new BackupAndRestore(); - bar.backup("webBackup"); - File inputFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup"); - if (!inputFile.exists()) { - return false; + rc.onFinish(false, 0); } - File zipFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup.zip"); - FileInputStream fis = null; - HttpURLConnection conn = null; - try { - //压缩文件 - ZipUtils.zipFile(inputFile, zipFile); - fis = new FileInputStream(zipFile); - URL url = new URL(URLCONST.APP_WEB_URL + "bak?username=" + userInfo.get("userName") + - makeSignalParam()); - conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setRequestProperty("Content-type", "multipart/form-data"); - conn.setDoInput(true); - conn.setDoOutput(true); - OutputStream out = conn.getOutputStream(); - byte[] bytes = new byte[1024]; - int len = -1; - while ((len = fis.read(bytes)) != -1){ - out.write(bytes, 0, len); + Backup.INSTANCE.backup(MyApplication.getmContext(), APPCONST.FILE_DIR + "webBackup/", new Backup.CallBack() { + @Override + public void backupSuccess() { + MyApplication.getApplication().newThread(() ->{ + File inputFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup"); + if (!inputFile.exists()) { + rc.onFinish(false, 0); + } + File zipFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup.zip"); + FileInputStream fis = null; + HttpURLConnection conn = null; + try { + //压缩文件 + ZipUtils.zipFile(inputFile, zipFile); + fis = new FileInputStream(zipFile); + URL url = new URL(URLCONST.APP_WEB_URL + "bak?username=" + userInfo.get("userName") + + makeSignalParam()); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-type", "multipart/form-data"); + conn.setDoInput(true); + conn.setDoOutput(true); + OutputStream out = conn.getOutputStream(); + byte[] bytes = new byte[1024]; + int len = -1; + while ((len = fis.read(bytes)) != -1){ + out.write(bytes, 0, len); + } + out.flush(); + zipFile.delete(); + BufferedReader bw = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); + StringBuilder sb = new StringBuilder(); + String line = bw.readLine(); + while (line != null) { + sb.append(line); + line = bw.readLine(); + } + String[] info = sb.toString().split(":"); + int code = Integer.parseInt(info[0].trim()); + rc.onFinish(code == 104, 0); + } catch (Exception e) { + e.printStackTrace(); + rc.onError(e); + } finally { + IOUtils.close(fis); + if (conn != null) { + conn.disconnect(); + } + } + }); } - out.flush(); - zipFile.delete(); - BufferedReader bw = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); - StringBuilder sb = new StringBuilder(); - String line = bw.readLine(); - while (line != null) { - sb.append(line); - line = bw.readLine(); - } - String[] info = sb.toString().split(":"); - int code = Integer.parseInt(info[0].trim()); - return code == 104; - } catch (Exception e) { - e.printStackTrace(); - return false; - } finally { - IOUtils.close(fis); - if (conn != null) { - conn.disconnect(); + + @Override + public void backupError(@NonNull String msg) { + ToastUtils.showError(msg); + rc.onFinish(false, 0); } - } + }, false); + } /** * 网络恢复 * @return */ - public static boolean webRestore(){ + public static void webRestore(ResultCallback rc){ Map userInfo = readConfig(); if (userInfo == null){ - return false; + rc.onFinish(false, 0); } FileOutputStream fos = null; File zipFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup.zip"); @@ -274,16 +289,25 @@ public class UserService { fos.flush(); if (zipFile.length() == 0){ zipFile.delete(); - return false; + rc.onFinish(false, 0); } ZipUtils.unzipFile(zipFile.getAbsolutePath(), APPCONST.FILE_DIR); - BackupAndRestore bar = new BackupAndRestore(); - bar.restore("webBackup"); - zipFile.delete(); - return true; + Restore.INSTANCE.restore(APPCONST.FILE_DIR + "webBackup/", new Restore.CallBack() { + @Override + public void restoreSuccess() { + zipFile.delete(); + rc.onFinish(true, 0); + } + + @Override + public void restoreError(@NonNull String msg) { + ToastUtils.showError(msg); + rc.onFinish(false, 0); + } + }); } catch (Exception e) { e.printStackTrace(); - return false; + rc.onError(e); }finally { IOUtils.close(fos); if (conn != null) { diff --git a/app/src/main/java/xyz/fycz/myreader/model/storage/Backup.kt b/app/src/main/java/xyz/fycz/myreader/model/storage/Backup.kt new file mode 100644 index 0000000..5193400 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/model/storage/Backup.kt @@ -0,0 +1,178 @@ +package xyz.fycz.myreader.model.storage + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile + +import io.reactivex.Single +import io.reactivex.SingleOnSubscribe +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import xyz.fycz.myreader.application.MyApplication +import xyz.fycz.myreader.application.SysManager +import xyz.fycz.myreader.base.observer.MySingleObserver +import xyz.fycz.myreader.common.APPCONST +import xyz.fycz.myreader.greendao.GreenDaoManager +import xyz.fycz.myreader.greendao.service.BookMarkService +import xyz.fycz.myreader.greendao.service.BookService +import xyz.fycz.myreader.greendao.service.SearchHistoryService +import xyz.fycz.myreader.util.SharedPreUtils +import xyz.fycz.myreader.util.utils.DocumentUtil +import xyz.fycz.myreader.util.utils.FileUtils +import xyz.fycz.myreader.util.utils.GSON +import java.io.File +import java.util.concurrent.TimeUnit + + +object Backup { + + val backupPath = MyApplication.getApplication().filesDir.absolutePath + File.separator + "backup" + + val defaultPath by lazy { + APPCONST.BACKUP_FILE_DIR + } + + val backupFileNames by lazy { + arrayOf( + "myBooks.json", + "mySearchHistory.json", + "myBookMark.json", + "myBookGroup.json", + "setting.json", + "config.xml" + ) + } + + fun autoBack() { + val lastBackup = SharedPreUtils.getInstance().getLong("lastBackup", 0) + if (System.currentTimeMillis() - lastBackup < TimeUnit.DAYS.toMillis(1)) { + return + } + val path = SharedPreUtils.getInstance().getString("backupPath", defaultPath) + if (path == null) { + backup(MyApplication.getmContext(), defaultPath, null, true) + } else { + backup(MyApplication.getmContext(), path, null, true) + } + } + + fun backup(context: Context, path: String, callBack: CallBack?, isAuto: Boolean = false) { + SharedPreUtils.getInstance().putLong("lastBackup", System.currentTimeMillis()) + Single.create(SingleOnSubscribe { e -> + BookService.getInstance().allBooks.let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + FileUtils.getFile(backupPath + File.separator + "myBooks.json").writeText(json) + } + } + SearchHistoryService.getInstance().findAllSearchHistory().let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + FileUtils.getFile(backupPath + File.separator + "mySearchHistory.json") + .writeText(json) + } + } + GreenDaoManager.getInstance().session.bookMarkDao.queryBuilder().list().let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + FileUtils.getFile(backupPath + File.separator + "myBookMark.json") + .writeText(json) + } + } + GreenDaoManager.getInstance().session.bookGroupDao.queryBuilder().list().let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + FileUtils.getFile(backupPath + File.separator + "myBookGroup.json") + .writeText(json) + } + } + val json = GSON.toJson(SysManager.getSetting()) + FileUtils.getFile(backupPath + File.separator + "setting.json") + .writeText(json) + Preferences.getSharedPreferences(context, backupPath, "config")?.let { sp -> + val edit = sp.edit() + SharedPreUtils.getInstance().all.map { + when (val value = it.value) { + is Int -> edit.putInt(it.key, value) + is Boolean -> edit.putBoolean(it.key, value) + is Long -> edit.putLong(it.key, value) + is Float -> edit.putFloat(it.key, value) + is String -> edit.putString(it.key, value) + else -> Unit + } + } + edit.commit() + } + WebDavHelp.backUpWebDav(backupPath) + if (path.isContentPath()) { + copyBackup(context, Uri.parse(path), isAuto) + } else { + copyBackup(path, isAuto) + } + e.onSuccess(true) + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : MySingleObserver() { + override fun onSuccess(t: Boolean) { + callBack?.backupSuccess() + } + + override fun onError(e: Throwable) { + e.printStackTrace() + callBack?.backupError(e.localizedMessage ?: "ERROR") + } + }) + } + + @Throws(Exception::class) + private fun copyBackup(context: Context, uri: Uri, isAuto: Boolean) { + synchronized(this) { + DocumentFile.fromTreeUri(context, uri)?.let { treeDoc -> + for (fileName in backupFileNames) { + val file = File(backupPath + File.separator + fileName) + if (file.exists()) { + if (isAuto) { + treeDoc.findFile("auto")?.findFile(fileName)?.delete() + var autoDoc = treeDoc.findFile("auto") + if (autoDoc == null) { + autoDoc = treeDoc.createDirectory("auto") + } + autoDoc?.createFile("", fileName)?.let { + DocumentUtil.writeBytes(context, file.readBytes(), it) + } + } else { + treeDoc.findFile(fileName)?.delete() + treeDoc.createFile("", fileName)?.let { + DocumentUtil.writeBytes(context, file.readBytes(), it) + } + } + } + } + } + } + } + + @Throws(java.lang.Exception::class) + private fun copyBackup(path: String, isAuto: Boolean) { + synchronized(this) { + for (fileName in backupFileNames) { + if (isAuto) { + val file = File(backupPath + File.separator + fileName) + if (file.exists()) { + file.copyTo(FileUtils.getFile(path + File.separator + "auto" + File.separator + fileName), true) + } + } else { + val file = File(backupPath + File.separator + fileName) + if (file.exists()) { + file.copyTo(FileUtils.getFile(path + File.separator + fileName), true) + } + } + } + } + } + + interface CallBack { + fun backupSuccess() + fun backupError(msg: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/model/storage/BackupRestoreUi.kt b/app/src/main/java/xyz/fycz/myreader/model/storage/BackupRestoreUi.kt new file mode 100644 index 0000000..f39c9a5 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/model/storage/BackupRestoreUi.kt @@ -0,0 +1,239 @@ +package xyz.fycz.myreader.model.storage + +import android.app.Activity +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.net.Uri +import android.text.TextUtils +import androidx.core.content.ContextCompat +import androidx.documentfile.provider.DocumentFile +import io.reactivex.Single +import io.reactivex.SingleEmitter +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.jetbrains.anko.alert +import org.jetbrains.anko.toast +import xyz.fycz.myreader.base.observer.MySingleObserver +import xyz.fycz.myreader.common.APPCONST +import xyz.fycz.myreader.model.storage.WebDavHelp.getWebDavFileNames +import xyz.fycz.myreader.model.storage.WebDavHelp.showRestoreDialog +import xyz.fycz.myreader.util.SharedPreUtils +import xyz.fycz.myreader.util.ToastUtils +import java.util.* + +object BackupRestoreUi : Backup.CallBack, Restore.CallBack { + + private const val backupSelectRequestCode = 22 + private const val restoreSelectRequestCode = 33 + + private fun getBackupPath(): String? { + return SharedPreUtils.getInstance().getString("backupPath", APPCONST.BACKUP_FILE_DIR) + } + + private fun setBackupPath(path: String?) { + if (path.isNullOrEmpty()) { + SharedPreUtils.getInstance().remove("backupPath") + } else { + SharedPreUtils.getInstance().putString("backupPath", path) + } + } + + override fun backupSuccess() { + ToastUtils.showSuccess("备份成功") + } + + override fun backupError(msg: String) { + ToastUtils.showError(msg) + } + + override fun restoreSuccess() { + ToastUtils.showSuccess("恢复成功") + } + + override fun restoreError(msg: String) { + ToastUtils.showError(msg) + } + + fun backup(activity: Activity) { + val backupPath = getBackupPath() + if (backupPath.isNullOrEmpty()) { +// selectBackupFolder(activity) + ToastUtils.showError("backupPath.isNullOrEmpty") + } else { + if (backupPath.isContentPath()) { + val uri = Uri.parse(backupPath) + val doc = DocumentFile.fromTreeUri(activity, uri) + if (doc?.canWrite() == true) { + Backup.backup(activity, backupPath, this) + } else { +// selectBackupFolder(activity) + ToastUtils.showError("doc?.canWrite() != true") + } + } else { +// backupUsePermission(activity) + ToastUtils.showError("backupPath.isNotContentPath") + } + } + } + + /*private fun backupUsePermission(activity: Activity, path: String = Backup.defaultPath) { + PermissionsCompat.Builder(activity) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.get_storage_per) + .onGranted { + setBackupPath(path) + Backup.backup(activity, path, this) + } + .request() + } + + fun selectBackupFolder(activity: Activity) { + activity.alert { + titleResource = R.string.select_folder + items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index -> + when (index) { + 0 -> { + setBackupPath(Backup.defaultPath) + backupUsePermission(activity) + } + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + activity.startActivityForResult(intent, backupSelectRequestCode) + } catch (e: java.lang.Exception) { + e.printStackTrace() + activity.toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> { + PermissionsCompat.Builder(activity) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.get_storage_per) + .onGranted { + selectBackupFolderApp(activity, false) + } + .request() + } + } + } + }.show() + } + + private fun selectBackupFolderApp(activity: Activity, isRestore: Boolean) { + val picker = FilePicker(activity, FilePicker.DIRECTORY) + picker.setBackgroundColor(ContextCompat.getColor(activity, R.color.background)) + picker.setTopBackgroundColor(ContextCompat.getColor(activity, R.color.background)) + picker.setItemHeight(30) + picker.setOnFilePickListener { currentPath: String -> + setBackupPath(currentPath) + if (isRestore) { + Restore.restore(currentPath, this) + } else { + Backup.backup(activity, currentPath, this) + } + } + picker.show() + }*/ + + fun restore(activity: Activity) { + Single.create { emitter: SingleEmitter?> -> + emitter.onSuccess(getWebDavFileNames()) + }.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : MySingleObserver?>() { + override fun onSuccess(strings: ArrayList) { + if (!showRestoreDialog(activity, strings, this@BackupRestoreUi)) { + val path = getBackupPath() + if (TextUtils.isEmpty(path)) { + //selectRestoreFolder(activity) + ToastUtils.showError("TextUtils.isEmpty(path)") + } else { + if (path.isContentPath()) { + val uri = Uri.parse(path) + val doc = DocumentFile.fromTreeUri(activity, uri) + if (doc?.canWrite() == true) { + Restore.restore(activity, Uri.parse(path), this@BackupRestoreUi) + } else { +// selectRestoreFolder(activity) + ToastUtils.showError("doc?.canWrite() != true") + } + } else { +// restoreUsePermission(activity) + ToastUtils.showError("path.isNotContentPath") + } + } + } + } + }) + } + + /*private fun restoreUsePermission(activity: Activity, path: String = Backup.defaultPath) { + PermissionsCompat.Builder(activity) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.get_storage_per) + .onGranted { + setBackupPath(path) + Restore.restore(path, this) + } + .request() + } + + private fun selectRestoreFolder(activity: Activity) { + activity.alert { + titleResource = R.string.select_folder + items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index -> + when (index) { + 0 -> restoreUsePermission(activity) + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + activity.startActivityForResult(intent, restoreSelectRequestCode) + } catch (e: java.lang.Exception) { + e.printStackTrace() + activity.toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> { + PermissionsCompat.Builder(activity) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.get_storage_per) + .onGranted { + selectBackupFolderApp(activity, true) + } + .request() + } + } + } + }.show() + }*/ + + /*fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + backupSelectRequestCode -> if (resultCode == RESULT_OK) { + data?.data?.let { uri -> + MApplication.getInstance().contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + setBackupPath(uri.toString()) + Backup.backup(MApplication.getInstance(), uri.toString(), this) + } + } + restoreSelectRequestCode -> if (resultCode == RESULT_OK) { + data?.data?.let { uri -> + MApplication.getInstance().contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + setBackupPath(uri.toString()) + Restore.restore(MApplication.getInstance(), uri, this) + } + } + } + }*/ + +} + +fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/model/storage/Preferences.kt b/app/src/main/java/xyz/fycz/myreader/model/storage/Preferences.kt new file mode 100644 index 0000000..9cd354c --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/model/storage/Preferences.kt @@ -0,0 +1,48 @@ +package xyz.fycz.myreader.model.storage + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.SharedPreferences +import java.io.File + +object Preferences { + + /** + * 用反射生成 SharedPreferences + * @param context + * @param dir + * @param fileName 文件名,不需要 '.xml' 后缀 + * @return + */ + fun getSharedPreferences( + context: Context, + dir: String, + fileName: String + ): SharedPreferences? { + try { + // 获取 ContextWrapper对象中的mBase变量。该变量保存了 ContextImpl 对象 + val fieldMBase = ContextWrapper::class.java.getDeclaredField("mBase") + fieldMBase.isAccessible = true + // 获取 mBase变量 + val objMBase = fieldMBase.get(context) + // 获取 ContextImpl.mPreferencesDir变量,该变量保存了数据文件的保存路径 + val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField("mPreferencesDir") + fieldMPreferencesDir.isAccessible = true + // 创建自定义路径 + // String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+"/Android"; + val file = File(dir) + // 修改mPreferencesDir变量的值 + fieldMPreferencesDir.set(objMBase, file) + // 返回修改路径以后的 SharedPreferences :%FILE_PATH%/%fileName%.xml + return context.getSharedPreferences(fileName, Activity.MODE_PRIVATE) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/model/storage/Restore.kt b/app/src/main/java/xyz/fycz/myreader/model/storage/Restore.kt new file mode 100644 index 0000000..5215a73 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/model/storage/Restore.kt @@ -0,0 +1,137 @@ +package xyz.fycz.myreader.model.storage + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import io.reactivex.Single +import io.reactivex.SingleOnSubscribe +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import xyz.fycz.myreader.application.MyApplication +import xyz.fycz.myreader.application.SysManager +import xyz.fycz.myreader.base.observer.MySingleObserver +import xyz.fycz.myreader.entity.Setting +import xyz.fycz.myreader.greendao.GreenDaoManager +import xyz.fycz.myreader.greendao.entity.Book +import xyz.fycz.myreader.greendao.entity.BookGroup +import xyz.fycz.myreader.greendao.entity.BookMark +import xyz.fycz.myreader.greendao.entity.SearchHistory +import xyz.fycz.myreader.util.SharedPreUtils +import xyz.fycz.myreader.util.utils.* +import java.io.File + +object Restore { + + fun restore(context: Context, uri: Uri, callBack: CallBack?) { + Single.create(SingleOnSubscribe { e -> + DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc -> + for (fileName in Backup.backupFileNames) { + if (doc.name == fileName) { + DocumentUtil.readBytes(context, doc.uri)?.let { + FileUtils.getFile(Backup.backupPath + File.separator + fileName) + .writeBytes(it) + } + } + } + } + e.onSuccess(true) + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : MySingleObserver() { + override fun onSuccess(t: Boolean) { + restore(Backup.backupPath, callBack) + } + + override fun onError(e: Throwable) { + e.printStackTrace() + callBack?.restoreError(e.localizedMessage ?: "ERROR") + } + }) + } + + fun restore(path: String, callBack: CallBack?) { + Single.create(SingleOnSubscribe { e -> + try { + val file = FileUtils.getFile(path + File.separator + "myBooks.json") + val json = file.readText() + GSON.fromJsonArray(json)?.forEach { bookshelf -> + /*if (bookshelf.noteUrl != null) { + DbHelper.getDaoSession().bookShelfBeanDao.insertOrReplace(bookshelf) + } + if (bookshelf.bookInfoBean.noteUrl != null) { + DbHelper.getDaoSession().bookInfoBeanDao.insertOrReplace(bookshelf.bookInfoBean) + }*/ + GreenDaoManager.getInstance().session.bookDao.insertOrReplace(bookshelf) + } + } catch (e: Exception) { + e.printStackTrace() + } + try { + val file = FileUtils.getFile(path + File.separator + "mySearchHistory.json") + val json = file.readText() + GSON.fromJsonArray(json)?.let { + GreenDaoManager.getInstance().session.searchHistoryDao.insertOrReplaceInTx(it) + } + } catch (e: Exception) { + e.printStackTrace() + } + try { + val file = FileUtils.getFile(path + File.separator + "myBookMark.json") + val json = file.readText() + GSON.fromJsonArray(json)?.let { + GreenDaoManager.getInstance().session.bookMarkDao.insertOrReplaceInTx(it) + } + } catch (e: Exception) { + e.printStackTrace() + } + try { + val file = FileUtils.getFile(path + File.separator + "myBookGroup.json") + val json = file.readText() + GSON.fromJsonArray(json)?.let { + GreenDaoManager.getInstance().session.bookGroupDao.insertOrReplaceInTx(it) + } + } catch (e: Exception) { + e.printStackTrace() + } + try { + val file = FileUtils.getFile(path + File.separator + "setting.json") + val json = file.readText() + SysManager.saveSetting(GSON.fromJsonObject(json)) + } catch (e: Exception) { + e.printStackTrace() + } + Preferences.getSharedPreferences(MyApplication.getmContext(), path, "config")?.all?.map { + val edit = SharedPreUtils.getInstance() + when (val value = it.value) { + is Int -> edit.putInt(it.key, value) + is Boolean -> edit.putBoolean(it.key, value) + is Long -> edit.putLong(it.key, value) + is Float -> edit.putFloat(it.key, value) + is String -> edit.putString(it.key, value) + else -> Unit + } + edit.putInt("versionCode", MyApplication.getVersionCode()) + } + e.onSuccess(true) + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : MySingleObserver() { + override fun onSuccess(t: Boolean) { + MyApplication.getApplication().initNightTheme() + callBack?.restoreSuccess() + } + + override fun onError(e: Throwable) { + e.printStackTrace() + callBack?.restoreError(e.localizedMessage ?: "ERROR") + } + }) + } + + + interface CallBack { + fun restoreSuccess() + fun restoreError(msg: String) + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/model/storage/WebDavHelp.kt b/app/src/main/java/xyz/fycz/myreader/model/storage/WebDavHelp.kt new file mode 100644 index 0000000..ba2e8d0 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/model/storage/WebDavHelp.kt @@ -0,0 +1,121 @@ +package xyz.fycz.myreader.model.storage + +import android.content.Context +import android.os.Handler +import android.os.Looper +import io.reactivex.Single +import io.reactivex.SingleOnSubscribe +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.jetbrains.anko.selector +import xyz.fycz.myreader.base.observer.MySingleObserver +import xyz.fycz.myreader.common.APPCONST +import xyz.fycz.myreader.util.SharedPreUtils +import xyz.fycz.myreader.util.ToastUtils +import xyz.fycz.myreader.util.ZipUtils +import xyz.fycz.myreader.util.utils.FileUtils +import xyz.fycz.myreader.util.webdav.WebDav +import xyz.fycz.myreader.util.webdav.http.HttpAuth +import java.io.File +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.min + +object WebDavHelp { + private val zipFilePath = FileUtils.getCachePath() + "/backup" + ".zip" + private val unzipFilesPath by lazy { + FileUtils.getCachePath() + } + + private fun getWebDavUrl(): String { + var url = SharedPreUtils.getInstance().getString("webdavUrl", APPCONST.DEFAULT_WEB_DAV_URL) + if (url.isNullOrEmpty()) { + url = APPCONST.DEFAULT_WEB_DAV_URL + } + if (!url.endsWith("/")) url += "/" + return url + } + + private fun initWebDav(): Boolean { + val account = SharedPreUtils.getInstance().getString("webdavAccount", "") + val password = SharedPreUtils.getInstance().getString("webdavPassword", "") + if (!account.isNullOrBlank() && !password.isNullOrBlank()) { + HttpAuth.auth = HttpAuth.Auth(account, password) + return true + } + return false + } + + fun getWebDavFileNames(): ArrayList { + val url = getWebDavUrl() + val names = arrayListOf() + try { + if (initWebDav()) { + var files = WebDav(url + "FYReader/").listFiles() + files = files.reversed() + for (index: Int in 0 until min(10, files.size)) { + files[index].displayName?.let { + names.add(it) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return names + } + + fun showRestoreDialog(context: Context, names: ArrayList, callBack: Restore.CallBack?): Boolean { + return if (names.isNotEmpty()) { + context.selector(title = "选择恢复文件", items = names) { _, index -> + if (index in 0 until names.size) { + restoreWebDav(names[index], callBack) + } + } + true + } else { + false + } + } + + private fun restoreWebDav(name: String, callBack: Restore.CallBack?) { + Single.create(SingleOnSubscribe { e -> + getWebDavUrl().let { + val file = WebDav(it + "FYReader/" + name) + file.downloadTo(zipFilePath, true) + @Suppress("BlockingMethodInNonBlockingContext") + ZipUtils.unzipFile(zipFilePath, unzipFilesPath) + } + e.onSuccess(true) + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : MySingleObserver() { + override fun onSuccess(t: Boolean) { + Restore.restore(unzipFilesPath, callBack) + } + }) + } + + fun backUpWebDav(path: String) { + try { + if (initWebDav()) { + val paths = arrayListOf(*Backup.backupFileNames) + for (i in 0 until paths.size) { + paths[i] = path + File.separator + paths[i] + } + FileUtils.deleteFile(zipFilePath) + if (ZipUtils.zipFiles(paths, zipFilePath)) { + WebDav(getWebDavUrl() + "FYReader").makeAsDir() + val putUrl = getWebDavUrl() + "FYReader/backup" + + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + .format(Date(System.currentTimeMillis())) + ".zip" + WebDav(putUrl).upload(zipFilePath) + } + } + } catch (e: Exception) { + Handler(Looper.getMainLooper()).post { + ToastUtils.showError("WebDav\n${e.localizedMessage}") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/AboutActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/AboutActivity.java index 68c4ea4..b1fdec1 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/AboutActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/AboutActivity.java @@ -12,6 +12,7 @@ import butterknife.BindView; import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.base.BaseActivity2; +import xyz.fycz.myreader.common.URLCONST; import xyz.fycz.myreader.ui.dialog.DialogCreator; import xyz.fycz.myreader.util.ShareUtils; import xyz.fycz.myreader.util.SharedPreUtils; @@ -71,7 +72,7 @@ public class AboutActivity extends BaseActivity2 { ToastUtils.showSuccess("邮箱复制成功!"); }); vmShare.setOnClickListener(v -> ShareUtils.share(this, getString(R.string.share_text) + - SharedPreUtils.getInstance().getString("downloadLink"))); + SharedPreUtils.getInstance().getString(getString(R.string.downloadLink, URLCONST.LAN_ZOUS_URL)))); vmUpdate.setOnClickListener(v -> MyApplication.checkVersionByServer(this, true, null)); vmUpdateLog.setOnClickListener(v -> DialogCreator.createAssetTipDialog(this, "更新日志", "updatelog.fy")); vmQQ.setOnClickListener(v -> { diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java index 33f4eb8..326baa1 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java @@ -42,6 +42,7 @@ import xyz.fycz.myreader.ui.adapter.DetailCatalogAdapter; import xyz.fycz.myreader.util.StringHelper; import xyz.fycz.myreader.util.utils.NetworkUtils; import xyz.fycz.myreader.webapi.CommonApi; +import xyz.fycz.myreader.widget.CoverImageView; import java.util.ArrayList; @@ -51,7 +52,7 @@ import java.util.ArrayList; */ public class BookDetailedActivity extends BaseActivity2 { @BindView(R.id.book_detail_iv_cover) - ImageView mIvCover; + CoverImageView mIvCover; /* @BindView(R.id.book_detail_iv_blur_cover) ImageView mIvBlurCover;*/ @BindView(R.id.book_detail_tv_author) @@ -323,13 +324,7 @@ public class BookDetailedActivity extends BaseActivity2 { mTvDesc.setText("\t\t\t\t" + mBook.getDesc()); mTvType.setText(mBook.getType()); if (!MyApplication.isDestroy(this)) { - Glide.with(this) - .load(mBook.getImgUrl()) - .error(R.mipmap.no_image) - .placeholder(R.mipmap.no_image) - //设置圆角 - .apply(RequestOptions.bitmapTransform(new RoundedCorners(8))) - .into(mIvCover); + mIvCover.load(mBook.getImgUrl(), mBook.getName(), mBook.getAuthor()); /*Glide.with(this) .load(mBook.getImgUrl()) .transition(DrawableTransitionOptions.withCrossFade(1500)) diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/BookstoreActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/BookstoreActivity.java index 092dc91..4ebbc5e 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/BookstoreActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/BookstoreActivity.java @@ -204,11 +204,11 @@ public class BookstoreActivity extends BaseActivity2 { getData(); if (findCrawler.hasImg()) { SharedPreUtils spu = SharedPreUtils.getInstance(); - boolean isReadTopTip = spu.getBoolean("isReadTopTip", false); + boolean isReadTopTip = spu.getBoolean(getString(R.string.isReadTopTip), false); if (!isReadTopTip) { DialogCreator.createCommonDialog(this, "提示", getResources().getString(R.string.top_sort_tip, title), true, "知道了", "不再提示", null, - (dialog, which) -> spu.putBoolean("isReadTopTip", true)); + (dialog, which) -> spu.putBoolean(getString(R.string.isReadTopTip), true)); } } } @@ -219,11 +219,11 @@ public class BookstoreActivity extends BaseActivity2 { private void getData() { if (findCrawler instanceof QiDianMobileRank) { SharedPreUtils spu = SharedPreUtils.getInstance(); - if (spu.getString("qdCookie", "").equals("")) { + if (spu.getString(getString(R.string.qdCookie), "").equals("")) { ((QiDianMobileRank) findCrawler).initCookie(this, new ResultCallback() { @Override public void onFinish(Object o, int code) { - spu.putString("qdCookie", (String) o); + spu.putString(getString(R.string.qdCookie), (String) o); mBookTypes = ((QiDianMobileRank) findCrawler).getRankTypes(); curType = mBookTypes.get(0); mHandler.sendMessage(mHandler.obtainMessage(1)); diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/MainActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/MainActivity.java index c879720..436afb7 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/MainActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/MainActivity.java @@ -76,7 +76,7 @@ public class MainActivity extends BaseActivity2 { @Override protected void initData(Bundle savedInstanceState) { super.initData(savedInstanceState); - groupName = SharedPreUtils.getInstance().getString("curBookGroupName", ""); + groupName = SharedPreUtils.getInstance().getString(getString(R.string.curBookGroupName), ""); titles = new String[]{"书架", "发现", "我的"}; mBookcaseFragment = new BookcaseFragment(); mFindFragment = new FindFragment(); @@ -235,7 +235,7 @@ public class MainActivity extends BaseActivity2 { case R.id.action_change_group: case R.id.action_group_man: if (!mBookcaseFragment.getmBookcasePresenter().hasOnGroupChangeListener()) { mBookcaseFragment.getmBookcasePresenter().addOnGroupChangeListener(() -> { - groupName = SharedPreUtils.getInstance().getString("curBookGroupName", "所有书籍"); + groupName = SharedPreUtils.getInstance().getString(getString(R.string.curBookGroupName), "所有书籍"); getSupportActionBar().setSubtitle(groupName); }); } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java index 3e30e14..319e56b 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java @@ -3,6 +3,8 @@ package xyz.fycz.myreader.ui.activity; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.text.InputType; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.*; @@ -11,11 +13,19 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SwitchCompat; import androidx.appcompat.widget.Toolbar; import butterknife.BindView; +import com.google.android.material.textfield.TextInputLayout; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.application.SysManager; import xyz.fycz.myreader.base.BaseActivity2; +import xyz.fycz.myreader.base.observer.MySingleObserver; import xyz.fycz.myreader.common.APPCONST; +import xyz.fycz.myreader.model.storage.BackupRestoreUi; +import xyz.fycz.myreader.model.storage.WebDavHelp; import xyz.fycz.myreader.ui.dialog.DialogCreator; import xyz.fycz.myreader.ui.dialog.MultiChoiceDialog; import xyz.fycz.myreader.ui.dialog.MyAlertDialog; @@ -23,6 +33,7 @@ import xyz.fycz.myreader.entity.Setting; import xyz.fycz.myreader.enums.BookSource; import xyz.fycz.myreader.greendao.entity.Book; import xyz.fycz.myreader.greendao.service.BookService; +import xyz.fycz.myreader.util.StringHelper; import xyz.fycz.myreader.util.utils.FileUtils; import xyz.fycz.myreader.util.SharedPreUtils; import xyz.fycz.myreader.util.ToastUtils; @@ -41,6 +52,8 @@ import static xyz.fycz.myreader.common.APPCONST.BOOK_CACHE_PATH; */ public class MoreSettingActivity extends BaseActivity2 { + @BindView(R.id.more_setting_ll_webdav) + LinearLayout mLlWebdav; @BindView(R.id.more_setting_rl_volume) RelativeLayout mRlVolume; @BindView(R.id.more_setting_sc_volume) @@ -83,6 +96,8 @@ public class MoreSettingActivity extends BaseActivity2 { RelativeLayout mRlBookstore; @BindView(R.id.more_setting_sc_bookstore) SwitchCompat mScBookstore;*/ + + private Setting mSetting; private boolean isVolumeTurnPage; private int resetScreenTime; @@ -125,7 +140,7 @@ public class MoreSettingActivity extends BaseActivity2 { catheCap = mSetting.getCatheGap(); autoRefresh = mSetting.isRefreshWhenStart(); openBookStore = mSetting.isOpenBookStore(); - threadNum = SharedPreUtils.getInstance().getInt("threadNum", 8); + threadNum = SharedPreUtils.getInstance().getInt(getString(R.string.threadNum), 8); } @Override @@ -147,6 +162,7 @@ public class MoreSettingActivity extends BaseActivity2 { mTvThreadNum.setText(getString(R.string.cur_thread_num, threadNum)); } + private void initSwitchStatus() { mScVolume.setChecked(isVolumeTurnPage); mScMatchChapter.setChecked(isMatchChapter); @@ -157,6 +173,8 @@ public class MoreSettingActivity extends BaseActivity2 { @Override protected void initClick() { super.initClick(); + mLlWebdav.setOnClickListener(v -> startActivity(WebDavSettingActivity.class)); + mRlVolume.setOnClickListener( (v) -> { if (isVolumeTurnPage) { @@ -257,7 +275,7 @@ public class MoreSettingActivity extends BaseActivity2 { } } if (sb.lastIndexOf(",") >= 0) sb.deleteCharAt(sb.lastIndexOf(",")); - spu.putString("searchSource", sb.toString()); + spu.putString(getString(R.string.searchSource), sb.toString()); }, null, new DialogCreator.OnMultiDialogListener() { @Override public void onItemClick(DialogInterface dialog, int which, boolean isChecked) { @@ -287,7 +305,7 @@ public class MoreSettingActivity extends BaseActivity2 { .setView(view) .setPositiveButton("确定", (dialog, which) -> { threadNum = threadPick.getValue(); - SharedPreUtils.getInstance().putInt("threadNum", threadNum); + SharedPreUtils.getInstance().putInt(getString(R.string.threadNum), threadNum); mTvThreadNum.setText(getString(R.string.cur_thread_num, threadNum)); }).setNegativeButton("取消", null) .show(); @@ -371,7 +389,7 @@ public class MoreSettingActivity extends BaseActivity2 { String eCatheFileSize; if (eCatheFile.exists() && eCatheFile.isDirectory()) { eCatheFileSize = FileUtils.getFileSize(FileUtils.getDirSize(eCatheFile)); - }else { + } else { eCatheFileSize = "0"; } CharSequence[] cathes = {"章节缓存:" + eCatheFileSize, "图片缓存:" + catheFileSize}; @@ -397,6 +415,7 @@ public class MoreSettingActivity extends BaseActivity2 { } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -533,4 +552,6 @@ public class MoreSettingActivity extends BaseActivity2 { mBooksName[i] = book.getName(); } } + + } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java index c2c42dc..efb5cbb 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java @@ -20,6 +20,7 @@ import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.SysManager; import xyz.fycz.myreader.base.BaseActivity; import xyz.fycz.myreader.common.APPCONST; +import xyz.fycz.myreader.model.storage.Backup; import xyz.fycz.myreader.ui.dialog.DialogCreator; import xyz.fycz.myreader.ui.presenter.ReadPresenter; import xyz.fycz.myreader.widget.page.PageView; @@ -121,6 +122,7 @@ public class ReadActivity extends BaseActivity { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); } + Backup.INSTANCE.autoBack(); super.onBackPressed(); } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java index 3580cf0..f0de8e5 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java @@ -119,9 +119,9 @@ public class SearchBookActivity extends BaseActivity2 { if (srlSearchBookList != null) { srlSearchBookList.finishRefresh(); } - if (curThreadCount == 0 && !isStopSearch) { + /*if (curThreadCount == 0 && !isStopSearch) { rpb.setIsAutoLoading(false); - } + }*/ break; case 3: fabSearchStop.setVisibility(View.GONE); @@ -158,7 +158,7 @@ public class SearchBookActivity extends BaseActivity2 { if (rpb != null) { rpb.setIsAutoLoading(false); } - mHandler.sendEmptyMessage(3); + fabSearchStop.setVisibility(View.GONE); } @Override @@ -357,7 +357,7 @@ public class SearchBookActivity extends BaseActivity2 { } else { isStopSearch = false; - mSearchBookAdapter = new SearchBookAdapter(mBooks); + mSearchBookAdapter = new SearchBookAdapter(mBooks, searchEngine); rvSearchBooksList.setAdapter(mSearchBookAdapter); //进入书籍详情页 diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/WebDavSettingActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/WebDavSettingActivity.java new file mode 100644 index 0000000..fa4ebb8 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/WebDavSettingActivity.java @@ -0,0 +1,131 @@ +package xyz.fycz.myreader.ui.activity; + +import android.os.Bundle; +import android.text.InputType; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import butterknife.BindView; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import xyz.fycz.myreader.R; +import xyz.fycz.myreader.base.BaseActivity2; +import xyz.fycz.myreader.base.observer.MySingleObserver; +import xyz.fycz.myreader.common.APPCONST; +import xyz.fycz.myreader.model.storage.BackupRestoreUi; +import xyz.fycz.myreader.model.storage.WebDavHelp; +import xyz.fycz.myreader.ui.dialog.MyAlertDialog; +import xyz.fycz.myreader.util.SharedPreUtils; +import xyz.fycz.myreader.util.StringHelper; +import xyz.fycz.myreader.util.ToastUtils; + +import java.util.ArrayList; + +/** + * @author fengyue + * @date 2020/10/4 20:44 + */ +public class WebDavSettingActivity extends BaseActivity2 { + @BindView(R.id.webdav_setting_webdav_url) + LinearLayout llWebdavUrl; + @BindView(R.id.tv_webdav_url) + TextView tvWebdavUrl; + @BindView(R.id.webdav_setting_webdav_account) + LinearLayout llWebdavAccount; + @BindView(R.id.tv_webdav_account) + TextView tvWebdavAccount; + @BindView(R.id.webdav_setting_webdav_password) + LinearLayout llWebdavPassword; + @BindView(R.id.tv_webdav_password) + TextView tvWebdavPassword; + @BindView(R.id.webdav_setting_webdav_restore) + LinearLayout llWebdavRestore; + + private String webdavUrl; + private String webdavAccount; + private String webdavPassword; + @Override + protected int getContentId() { + return R.layout.activity_webdav_setting; + } + + @Override + protected void setUpToolbar(Toolbar toolbar) { + super.setUpToolbar(toolbar); + setStatusBarColor(R.color.colorPrimary, true); + getSupportActionBar().setTitle(getString(R.string.webdav_setting)); + } + + @Override + protected void initData(Bundle savedInstanceState) { + super.initData(savedInstanceState); + webdavUrl = SharedPreUtils.getInstance().getString("webdavUrl", APPCONST.DEFAULT_WEB_DAV_URL); + webdavAccount = SharedPreUtils.getInstance().getString("webdavAccount", ""); + webdavPassword = SharedPreUtils.getInstance().getString("webdavPassword", ""); + } + + @Override + protected void initWidget() { + super.initWidget(); + tvWebdavUrl.setText(webdavUrl); + tvWebdavAccount.setText(StringHelper.isEmpty(webdavAccount) ? "请输入WebDav账号" : webdavAccount); + tvWebdavPassword.setText(StringHelper.isEmpty(webdavPassword) ? "请输入WebDav授权密码" : "************"); + } + + @Override + protected void initClick() { + super.initClick(); + final String[] webdavTexts = new String[3]; + llWebdavUrl.setOnClickListener(v -> { + MyAlertDialog.createInputDia(this, getString(R.string.webdav_url), + "", webdavUrl.equals(APPCONST.DEFAULT_WEB_DAV_URL) ? + "" : webdavUrl, true, 100, + text -> webdavTexts[0] = text, + (dialog, which) -> { + webdavUrl = webdavTexts[0]; + tvWebdavUrl.setText(webdavUrl); + SharedPreUtils.getInstance().putString("webdavUrl", webdavUrl); + dialog.dismiss(); + }); + }); + llWebdavAccount.setOnClickListener(v -> { + MyAlertDialog.createInputDia(this, getString(R.string.webdav_account), + "", webdavAccount, true, 100, + text -> webdavTexts[1] = text, + (dialog, which) -> { + webdavAccount = webdavTexts[1]; + tvWebdavAccount.setText(webdavAccount); + SharedPreUtils.getInstance().putString("webdavAccount", webdavAccount); + dialog.dismiss(); + }); + }); + llWebdavPassword.setOnClickListener(v -> { + MyAlertDialog.createInputDia(this, getString(R.string.webdav_password), + "", webdavPassword, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD, + true, 100, + text -> webdavTexts[2] = text, + (dialog, which) -> { + webdavPassword = webdavTexts[2]; + tvWebdavPassword.setText("************"); + SharedPreUtils.getInstance().putString("webdavPassword", webdavPassword); + dialog.dismiss(); + }); + }); + llWebdavRestore.setOnClickListener(v -> { + Single.create((SingleOnSubscribe>) emitter -> { + emitter.onSuccess(WebDavHelp.INSTANCE.getWebDavFileNames()); + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new MySingleObserver>() { + @Override + public void onSuccess(ArrayList strings) { + if (!WebDavHelp.INSTANCE.showRestoreDialog(WebDavSettingActivity.this, strings, BackupRestoreUi.INSTANCE)) { + ToastUtils.showWarring("没有备份"); + } + } + }); + }); + } +} diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseAdapter.java index 0e1c52b..f31ab34 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseAdapter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseAdapter.java @@ -17,6 +17,7 @@ import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.ui.dialog.DialogCreator; import xyz.fycz.myreader.ui.dialog.MyAlertDialog; +import xyz.fycz.myreader.widget.CoverImageView; import xyz.fycz.myreader.widget.custom.DragAdapter; import xyz.fycz.myreader.greendao.entity.Book; import xyz.fycz.myreader.greendao.entity.Chapter; @@ -311,7 +312,7 @@ public abstract class BookcaseAdapter extends DragAdapter { static class ViewHolder { CheckBox cbBookChecked; - ImageView ivBookImg; + CoverImageView ivBookImg; TextView tvBookName; BadgeView tvNoReadNum; ProgressBar pbLoading; diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDetailedAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDetailedAdapter.java index 64d1df5..eaeece1 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDetailedAdapter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDetailedAdapter.java @@ -70,13 +70,7 @@ public class BookcaseDetailedAdapter extends BookcaseAdapter { if (StringHelper.isEmpty(book.getImgUrl())) { book.setImgUrl(""); } - Glide.with(mContext) - .load(book.getImgUrl()) - .error(R.mipmap.no_image) - .placeholder(R.mipmap.no_image) - //设置圆角 - .apply(RequestOptions.bitmapTransform(new RoundedCorners(8))) - .into(viewHolder.ivBookImg); + viewHolder.ivBookImg.load(book.getImgUrl(), book.getName(),book.getAuthor()); viewHolder.tvBookName.setText(book.getName()); diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDragAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDragAdapter.java index 017ece3..85051d9 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDragAdapter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDragAdapter.java @@ -67,13 +67,7 @@ public class BookcaseDragAdapter extends BookcaseAdapter { book.setImgUrl(""); } - Glide.with(mContext) - .load(book.getImgUrl()) - .error(R.mipmap.no_image) - .placeholder(R.mipmap.no_image) - //设置圆角 - .apply(RequestOptions.bitmapTransform(new RoundedCorners(8))) - .into(viewHolder.ivBookImg); + viewHolder.ivBookImg.load(book.getImgUrl(), book.getName(),book.getAuthor()); viewHolder.tvBookName.setText(book.getName()); diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchBookAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchBookAdapter.java index b70c0a1..f31dce3 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchBookAdapter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchBookAdapter.java @@ -7,6 +7,7 @@ import xyz.fycz.myreader.base.adapter.BaseListAdapter; import xyz.fycz.myreader.base.adapter.IViewHolder; import xyz.fycz.myreader.entity.SearchBookBean; import xyz.fycz.myreader.greendao.entity.Book; +import xyz.fycz.myreader.model.SearchEngine; import xyz.fycz.myreader.model.mulvalmap.ConcurrentMultiValueMap; import xyz.fycz.myreader.ui.adapter.holder.SearchBookHolder; @@ -19,6 +20,12 @@ import java.util.List; */ public class SearchBookAdapter extends BaseListAdapter { private ConcurrentMultiValueMap mBooks; + private SearchEngine searchEngine; + + public SearchBookAdapter(ConcurrentMultiValueMap mBooks, SearchEngine searchEngine) { + this.mBooks = mBooks; + this.searchEngine = searchEngine; + } public SearchBookAdapter(ConcurrentMultiValueMap mBooks) { this.mBooks = mBooks; @@ -26,7 +33,7 @@ public class SearchBookAdapter extends BaseListAdapter { @Override protected IViewHolder createViewHolder(int viewType) { - return new SearchBookHolder(mBooks); + return new SearchBookHolder(mBooks, searchEngine); } public synchronized void addAll(List newDataS, String keyWord) { diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/BookStoreBookHolder.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/BookStoreBookHolder.java index 7a6a9e4..7fed604 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/BookStoreBookHolder.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/BookStoreBookHolder.java @@ -16,6 +16,7 @@ import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.enums.BookSource; import xyz.fycz.myreader.greendao.entity.Book; import xyz.fycz.myreader.ui.activity.BookDetailedActivity; +import xyz.fycz.myreader.widget.CoverImageView; /** * @author fengyue @@ -23,7 +24,7 @@ import xyz.fycz.myreader.ui.activity.BookDetailedActivity; */ public class BookStoreBookHolder extends ViewHolderImpl { - private ImageView tvBookImg; + private CoverImageView tvBookImg; private TextView tvBookName; private TextView tvBookAuthor; private TextView tvBookTime; @@ -63,13 +64,7 @@ public class BookStoreBookHolder extends ViewHolderImpl { if (hasImg){ tvBookImg.setVisibility(View.VISIBLE); if (!MyApplication.isDestroy(mActivity)) { - Glide.with(getContext()) - .load(data.getImgUrl()) - .error(R.mipmap.no_image) - .placeholder(R.mipmap.no_image) - //设置圆角 - .apply(RequestOptions.bitmapTransform(new RoundedCorners(8))) - .into(tvBookImg); + tvBookImg.load(data.getImgUrl(), data.getName(), data.getAuthor()); } } if (data.getSource() != null) { diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/SearchBookHolder.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/SearchBookHolder.java index 6718720..81c3052 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/SearchBookHolder.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/SearchBookHolder.java @@ -1,5 +1,6 @@ package xyz.fycz.myreader.ui.adapter.holder; +import android.annotation.SuppressLint; import android.app.Activity; import android.os.Handler; import android.util.Log; @@ -11,6 +12,7 @@ import com.bumptech.glide.request.RequestOptions; import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.base.adapter.ViewHolderImpl; +import xyz.fycz.myreader.model.SearchEngine; import xyz.fycz.myreader.webapi.callback.ResultCallback; import xyz.fycz.myreader.entity.SearchBookBean; import xyz.fycz.myreader.enums.BookSource; @@ -21,6 +23,7 @@ import xyz.fycz.myreader.webapi.CommonApi; import xyz.fycz.myreader.webapi.crawler.ReadCrawlerUtil; import xyz.fycz.myreader.webapi.crawler.base.BookInfoCrawler; import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler; +import xyz.fycz.myreader.widget.CoverImageView; import java.util.List; @@ -30,22 +33,25 @@ import java.util.List; */ public class SearchBookHolder extends ViewHolderImpl { private ConcurrentMultiValueMap mBooks; + private SearchEngine searchEngine; private Handler mHandle = new Handler(message -> { switch (message.what) { case 1: Book book = (Book) message.obj; - initOtherInfo(book); + int pos = message.arg1; + initOtherInfo(book, pos); break; } return false; }); - public SearchBookHolder(ConcurrentMultiValueMap mBooks) { + public SearchBookHolder(ConcurrentMultiValueMap mBooks, SearchEngine searchEngine) { this.mBooks = mBooks; + this.searchEngine = searchEngine; } - ImageView ivBookImg; + CoverImageView ivBookImg; TextView tvBookName; TextView tvDesc; TextView tvAuthor; @@ -69,6 +75,7 @@ public class SearchBookHolder extends ViewHolderImpl { } + @SuppressLint("SetTextI18n") @Override public void onBind(SearchBookBean data, int pos) { List aBooks = mBooks.getValues(data); @@ -77,56 +84,61 @@ public class SearchBookHolder extends ViewHolderImpl { if (StringHelper.isEmpty(book.getImgUrl())){ book.setImgUrl(""); } + + if (!StringHelper.isEmpty(book.getDesc())) { + tvDesc.setText("简介:" + book.getDesc()); + } + if (!StringHelper.isEmpty(book.getType())) { + tvType.setText(book.getType()); + } + if (!StringHelper.isEmpty(book.getNewestChapterTitle())) { + tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, book.getNewestChapterTitle())); + } + if (!StringHelper.isEmpty(book.getAuthor())) { + tvAuthor.setText(book.getAuthor()); + } tvBookName.setText(book.getName()); - tvAuthor.setText(book.getAuthor()); tvSource.setText(getContext().getString(R.string.source_title_num, BookSource.fromString(book.getSource()).text, bookCount)); ReadCrawler rc = ReadCrawlerUtil.getReadCrawler(book.getSource()); if (rc instanceof BookInfoCrawler){ if (tvBookName.getTag() == null || !(Boolean) tvBookName.getTag()) { tvBookName.setTag(true); } else { - initOtherInfo(book); + initOtherInfo(book, pos); return; } Log.i(book.getName(), "initOtherInfo"); BookInfoCrawler bic = (BookInfoCrawler) rc; - CommonApi.getBookInfo(book, bic, new ResultCallback() { - @Override - public void onFinish(Object o, int code) { + searchEngine.getBookInfo(book, bic, isSuccess -> { + if (isSuccess) mHandle.sendMessage(mHandle.obtainMessage(1, pos, 0, book)); - } - - @Override - public void onError(Exception e) { + else tvBookName.setTag(false); - } }); }else { - initOtherInfo(book); + initOtherInfo(book, pos); } } - private void initOtherInfo(Book book){ + private void initOtherInfo(Book book, int pos){ //简介 - if (book.getDesc() == null) { - tvDesc.setText(""); - }else { + if (StringHelper.isEmpty(tvDesc.getText().toString())) { tvDesc.setText("简介:" + book.getDesc()); } - tvType.setText(book.getType()); - tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, book.getNewestChapterTitle())); - tvAuthor.setText(book.getAuthor()); + if (StringHelper.isEmpty(tvType.getText().toString())) { + tvType.setText(book.getType()); + } + if (StringHelper.isEmpty(tvNewestChapter.getText().toString())) { + tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, book.getNewestChapterTitle())); + } + if (StringHelper.isEmpty(tvAuthor.getText().toString())) { + tvAuthor.setText(book.getAuthor()); + } //图片 if (!MyApplication.isDestroy((Activity) getContext())) { - Glide.with(getContext()) - .load(book.getImgUrl()) -// .override(DipPxUtil.dip2px(getContext(), 80), DipPxUtil.dip2px(getContext(), 150)) - .error(R.mipmap.no_image) - .placeholder(R.mipmap.no_image) - //设置圆角 - .apply(RequestOptions.bitmapTransform(new RoundedCorners(8))) - .into(ivBookImg); + ivBookImg.load(book.getImgUrl(), book.getName(), book.getAuthor()); } } + } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/dialog/MyAlertDialog.java b/app/src/main/java/xyz/fycz/myreader/ui/dialog/MyAlertDialog.java index ad9e444..8562a5a 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/dialog/MyAlertDialog.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/dialog/MyAlertDialog.java @@ -1,15 +1,89 @@ package xyz.fycz.myreader.ui.dialog; import android.content.Context; +import android.content.DialogInterface; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; import androidx.appcompat.app.AlertDialog; +import com.google.android.material.textfield.TextInputLayout; import xyz.fycz.myreader.R; +import xyz.fycz.myreader.application.MyApplication; +import xyz.fycz.myreader.util.StringHelper; /** * @author fengyue * @date 2020/9/20 9:48 */ public class MyAlertDialog { - public static AlertDialog.Builder build(Context context){ + public static AlertDialog.Builder build(Context context) { return new AlertDialog.Builder(context, R.style.alertDialogTheme); } + + public static AlertDialog createInputDia(Context context, String title, String hint, String initText, + Integer inputType, boolean cancelable, int maxLen, onInputChangeListener oic, + DialogInterface.OnClickListener posListener) { + View view = LayoutInflater.from(context).inflate(R.layout.edit_dialog, null); + TextInputLayout textInputLayout = view.findViewById(R.id.text_input_lay); + + textInputLayout.setCounterMaxLength(maxLen); + EditText editText = textInputLayout.getEditText(); + editText.setHint(hint); + if (inputType != null) editText.setInputType(inputType); + if (!StringHelper.isEmpty(initText)) editText.setText(initText); + editText.requestFocus(); + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + MyApplication.getHandler().postDelayed(() -> imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED), 220); + AlertDialog inputDia = build(context) + .setTitle(title) + .setView(view) + .setCancelable(cancelable) + .setPositiveButton("确认", (dialog, which) -> { + posListener.onClick(dialog, which); + imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED); + }) + .setNegativeButton("取消", (dialog, which) -> { + imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED); + }) + .show(); + Button posBtn = inputDia.getButton(AlertDialog.BUTTON_POSITIVE); + posBtn.setEnabled(false); + editText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + String text = editText.getText().toString(); + if (editText.getText().length() > 0 && editText.getText().length() <= maxLen && !text.equals(initText)) { + posBtn.setEnabled(true); + } else { + posBtn.setEnabled(false); + } + oic.onChange(text); + } + }); + return inputDia; + } + public static AlertDialog createInputDia(Context context, String title, String hint, String initText, + boolean cancelable, int maxLen, onInputChangeListener oic, + DialogInterface.OnClickListener posListener) { + return createInputDia(context, title, hint, initText, InputType.TYPE_CLASS_TEXT, cancelable, maxLen, oic, posListener); + } + + public interface onInputChangeListener{ + void onChange(String text); + } } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java b/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java index 364afe9..4404df8 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java @@ -18,6 +18,9 @@ import xyz.fycz.myreader.model.backup.BackupAndRestore; import xyz.fycz.myreader.model.backup.UserService; import xyz.fycz.myreader.base.BaseFragment; import xyz.fycz.myreader.common.APPCONST; +import xyz.fycz.myreader.model.storage.Backup; +import xyz.fycz.myreader.model.storage.Restore; +import xyz.fycz.myreader.ui.activity.MainActivity; import xyz.fycz.myreader.ui.dialog.DialogCreator; import xyz.fycz.myreader.ui.dialog.MyAlertDialog; import xyz.fycz.myreader.entity.Setting; @@ -29,6 +32,7 @@ import xyz.fycz.myreader.ui.activity.MoreSettingActivity; import xyz.fycz.myreader.util.SharedPreUtils; import xyz.fycz.myreader.util.ToastUtils; import xyz.fycz.myreader.util.utils.NetworkUtils; +import xyz.fycz.myreader.webapi.callback.ResultCallback; import java.io.File; import java.text.SimpleDateFormat; @@ -150,15 +154,15 @@ public class MineFragment extends BaseFragment { AlertDialog bookDialog = MyAlertDialog.build(getContext()) .setTitle(getContext().getResources().getString(R.string.menu_bookcase_backup)) .setItems(backupMenu, (dialog, which) -> { - switch (which) { - case 0: - mHandler.sendMessage(mHandler.obtainMessage(2)); - break; - case 1: - mHandler.sendMessage(mHandler.obtainMessage(3)); - break; - } - }) + switch (which) { + case 0: + mHandler.sendMessage(mHandler.obtainMessage(2)); + break; + case 1: + mHandler.sendMessage(mHandler.obtainMessage(3)); + break; + } + }) .setNegativeButton(null, null) .setPositiveButton(null, null) .create(); @@ -224,15 +228,15 @@ public class MineFragment extends BaseFragment { themeMode = which; switch (which) { case 0: - SharedPreUtils.getInstance().putBoolean("isNightFS", true); + SharedPreUtils.getInstance().putBoolean(getString(R.string.isNightFS), true); break; case 1: - SharedPreUtils.getInstance().putBoolean("isNightFS", false); + SharedPreUtils.getInstance().putBoolean(getString(R.string.isNightFS), false); mSetting.setDayStyle(true); SysManager.saveSetting(mSetting); break; case 2: - SharedPreUtils.getInstance().putBoolean("isNightFS", false); + SharedPreUtils.getInstance().putBoolean(getString(R.string.isNightFS), false); mSetting.setDayStyle(false); SysManager.saveSetting(mSetting); break; @@ -255,7 +259,7 @@ public class MineFragment extends BaseFragment { mRlFeedback.setOnClickListener(v -> { DialogCreator.createCommonDialog(getContext(), "问题反馈", "请加入QQ群(1085028304)反馈问题!", true, "加入QQ群", "取消", (dialog, which) -> { - if (!MyApplication.joinQQGroup(getContext(),"8PIOnHFuH6A38hgxvD_Rp2Bu-Ke1ToBn")){ + if (!MyApplication.joinQQGroup(getContext(), "8PIOnHFuH6A38hgxvD_Rp2Bu-Ke1ToBn")) { ClipboardManager mClipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); //数据 ClipData mClipData = ClipData.newPlainText("Label", "1085028304"); @@ -285,11 +289,22 @@ public class MineFragment extends BaseFragment { DialogCreator.createCommonDialog(getContext(), "确认备份吗?", "新备份会替换原有备份!", true, (dialogInterface, i) -> { dialogInterface.dismiss(); - if (mBackupAndRestore.backup("localBackup")) { + /*if (mBackupAndRestore.backup("localBackup")) { DialogCreator.createTipDialog(getContext(), "备份成功,备份文件路径:" + APPCONST.BACKUP_FILE_DIR); } else { DialogCreator.createTipDialog(getContext(), "未给予储存权限,备份失败!"); - } + }*/ + Backup.INSTANCE.backup(MyApplication.getmContext(), APPCONST.BACKUP_FILE_DIR, new Backup.CallBack() { + @Override + public void backupSuccess() { + DialogCreator.createTipDialog(getContext(), "备份成功,备份文件路径:" + APPCONST.BACKUP_FILE_DIR); + } + + @Override + public void backupError(@io.reactivex.annotations.NonNull String msg) { + DialogCreator.createTipDialog(getContext(), "未给予储存权限,备份失败!"); + } + }, false); }, (dialogInterface, i) -> dialogInterface.dismiss()); } @@ -300,7 +315,7 @@ public class MineFragment extends BaseFragment { DialogCreator.createCommonDialog(getContext(), "确认恢复吗?", "恢复书架会覆盖原有书架!", true, (dialogInterface, i) -> { dialogInterface.dismiss(); - if (mBackupAndRestore.restore("localBackup")) { + /*if (mBackupAndRestore.restore("localBackup")) { mHandler.sendMessage(mHandler.obtainMessage(7)); // DialogCreator.createTipDialog(mMainActivity, // "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!"); @@ -308,7 +323,22 @@ public class MineFragment extends BaseFragment { ToastUtils.showSuccess("书架恢复成功!"); } else { DialogCreator.createTipDialog(getContext(), "未找到备份文件或未给予储存权限,恢复失败!"); - } + }*/ + Restore.INSTANCE.restore(APPCONST.BACKUP_FILE_DIR, new Restore.CallBack() { + @Override + public void restoreSuccess() { + mHandler.sendMessage(mHandler.obtainMessage(7)); +// DialogCreator.createTipDialog(mMainActivity, +// "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!"); + mSetting = SysManager.getSetting(); + ToastUtils.showSuccess("书架恢复成功!"); + } + + @Override + public void restoreError(@io.reactivex.annotations.NonNull String msg) { + DialogCreator.createTipDialog(getContext(), "未找到备份文件或未给予储存权限,恢复失败!"); + } + }); }, (dialogInterface, i) -> dialogInterface.dismiss()); } @@ -333,17 +363,27 @@ public class MineFragment extends BaseFragment { SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd"); String nowTimeStr = sdf.format(nowTime); SharedPreUtils spb = SharedPreUtils.getInstance(); - String synTime = spb.getString("synTime"); + String synTime = spb.getString(getString(R.string.synTime)); if (!nowTimeStr.equals(synTime) || !isAutoSyn) { - MyApplication.getApplication().newThread(() -> { - if (UserService.webBackup()) { - spb.putString("synTime", nowTimeStr); - if (!isAutoSyn) { - DialogCreator.createTipDialog(getContext(), "成功将书架同步至网络!"); + UserService.webBackup(new ResultCallback() { + @Override + public void onFinish(Object o, int code) { + if ((boolean) o) { + spb.putString(getString(R.string.synTime), nowTimeStr); + if (!isAutoSyn) { + DialogCreator.createTipDialog(getContext(), "成功将书架同步至网络!"); + } + } else { + if (!isAutoSyn) { + DialogCreator.createTipDialog(getContext(), "同步失败,请重试!"); + } } - } else { + } + + @Override + public void onError(Exception e) { if (!isAutoSyn) { - DialogCreator.createTipDialog(getContext(), "同步失败,请重试!"); + DialogCreator.createTipDialog(getContext(), "同步失败,请重试!\n" + e.getLocalizedMessage()); } } }); @@ -362,7 +402,7 @@ public class MineFragment extends BaseFragment { (dialogInterface, i) -> { dialogInterface.dismiss(); MyApplication.getApplication().newThread(() -> { - if (UserService.webRestore()) { + /*if (UserService.webRestore()) { mHandler.sendMessage(mHandler.obtainMessage(7)); // DialogCreator.createTipDialog(mMainActivity, // "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!");、 @@ -370,7 +410,26 @@ public class MineFragment extends BaseFragment { ToastUtils.showSuccess("成功将书架从网络同步至本地!"); } else { DialogCreator.createTipDialog(getContext(), "未找到同步文件,同步失败!"); - } + }*/ + UserService.webRestore(new ResultCallback() { + @Override + public void onFinish(Object o, int code) { + if ((boolean) o) { + mHandler.sendMessage(mHandler.obtainMessage(7)); +// DialogCreator.createTipDialog(mMainActivity, +// "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!");、 + mSetting = SysManager.getSetting(); + ToastUtils.showSuccess("成功将书架从网络同步至本地!"); + } else { + DialogCreator.createTipDialog(getContext(), "未找到同步文件,同步失败!"); + } + } + + @Override + public void onError(Exception e) { + DialogCreator.createTipDialog(getContext(), "未找到同步文件,同步失败!\n" + e.getLocalizedMessage()); + } + }); }); }, (dialogInterface, i) -> dialogInterface.dismiss()); } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookcasePresenter.java b/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookcasePresenter.java index 687daac..fb3ae2e 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookcasePresenter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookcasePresenter.java @@ -336,7 +336,7 @@ public class BookcasePresenter implements BasePresenter { //初始化书籍 private void initBook() { mBooks.clear(); - String curBookGroupId = SharedPreUtils.getInstance().getString("curBookGroupId", ""); + String curBookGroupId = SharedPreUtils.getInstance().getString(mMainActivity.getString(R.string.curBookGroupId), ""); isGroup = !"".equals(curBookGroupId); if (mBookcaseAdapter != null) { mBookcaseAdapter.setGroup(isGroup); @@ -481,12 +481,12 @@ public class BookcasePresenter implements BasePresenter { mMainActivity.startActivity(fileSystemIntent); break; case R.id.action_download_all: - if (!SharedPreUtils.getInstance().getBoolean("isReadDownloadAllTip")) { + if (!SharedPreUtils.getInstance().getBoolean(mMainActivity.getString(R.string.isReadDownloadAllTip), false)) { DialogCreator.createCommonDialog(mMainActivity, "一键缓存", mMainActivity.getString(R.string.all_cathe_tip), true, (dialog, which) -> { downloadAll(true); - SharedPreUtils.getInstance().putBoolean("isReadDownloadAllTip", true); + SharedPreUtils.getInstance().putBoolean(mMainActivity.getString(R.string.isReadDownloadAllTip), true); }, null); } else { downloadAll(true); @@ -515,8 +515,8 @@ public class BookcasePresenter implements BasePresenter { curBookGroupId = mBookGroups.get(menuItem.getOrder() - 1).getId(); curBookGroupName = mBookGroups.get(menuItem.getOrder() - 1).getName(); } - SharedPreUtils.getInstance().putString("curBookGroupId", curBookGroupId); - SharedPreUtils.getInstance().putString("curBookGroupName", curBookGroupName); + SharedPreUtils.getInstance().putString(mMainActivity.getString(R.string.curBookGroupId), curBookGroupId); + SharedPreUtils.getInstance().putString(mMainActivity.getString(R.string.curBookGroupName), curBookGroupName); ogcl.onChange(); init(); return true; @@ -587,6 +587,7 @@ public class BookcasePresenter implements BasePresenter { private void showAddOrRenameGroupDia(boolean isRename, boolean isAddGroup, int groupNum){ View view = LayoutInflater.from(mMainActivity).inflate(R.layout.edit_dialog, null); TextInputLayout textInputLayout = view.findViewById(R.id.text_input_lay); + textInputLayout.setCounterMaxLength(10); EditText editText = textInputLayout.getEditText(); editText.setHint("请输入分组名"); BookGroup bookGroup = !isRename ? new BookGroup() : mBookGroups.get(groupNum); @@ -622,8 +623,8 @@ public class BookcasePresenter implements BasePresenter { }else { mBookGroupService.updateEntity(bookGroup); SharedPreUtils spu = SharedPreUtils.getInstance(); - if (spu.getString("curBookGroupName", "").equals(oldName)){ - spu.putString("curBookGroupName", newGroupName.toString()); + if (spu.getString(mMainActivity.getString(R.string.curBookGroupName), "").equals(oldName)){ + spu.putString(mMainActivity.getString(R.string.curBookGroupName), newGroupName.toString()); ogcl.onChange(); } } @@ -677,9 +678,9 @@ public class BookcasePresenter implements BasePresenter { sb.deleteCharAt(sb.lastIndexOf("、")); } SharedPreUtils spu = SharedPreUtils.getInstance(); - if (mBookGroupService.getGroupById(spu.getString("curBookGroupId", "")) == null){ - spu.putString("curBookGroupId", ""); - spu.putString("curBookGroupName", ""); + if (mBookGroupService.getGroupById(spu.getString(mMainActivity.getString(R.string.curBookGroupId), "")) == null){ + spu.putString(mMainActivity.getString(R.string.curBookGroupId), ""); + spu.putString(mMainActivity.getString(R.string.curBookGroupName), ""); ogcl.onChange(); init(); } @@ -959,20 +960,30 @@ public class BookcasePresenter implements BasePresenter { SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd"); String nowTimeStr = sdf.format(nowTime); SharedPreUtils spb = SharedPreUtils.getInstance(); - String synTime = spb.getString("synTime"); + String synTime = spb.getString(mMainActivity.getString(R.string.synTime)); if (!nowTimeStr.equals(synTime) || !isAutoSyn) { - MyApplication.getApplication().newThread(() -> { - if (UserService.webBackup()) { - spb.putString("synTime", nowTimeStr); - if (!isAutoSyn) { - DialogCreator.createTipDialog(mMainActivity, "成功将书架同步至网络!"); + UserService.webBackup(new ResultCallback() { + @Override + public void onFinish(Object o, int code) { + if ((boolean) o){ + spb.putString(mMainActivity.getString(R.string.synTime), nowTimeStr); + if (!isAutoSyn) { + DialogCreator.createTipDialog(mMainActivity, "成功将书架同步至网络!"); + } + }else { + if (!isAutoSyn) { + DialogCreator.createTipDialog(mMainActivity, "同步失败,请重试!"); + } + } } - } else { - if (!isAutoSyn) { - DialogCreator.createTipDialog(mMainActivity, "同步失败,请重试!"); + + @Override + public void onError(Exception e) { + if (!isAutoSyn) { + DialogCreator.createTipDialog(mMainActivity, "同步失败,请重试!\n" + e.getLocalizedMessage()); + } } - } - }); + }); } } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/presenter/ReadPresenter.java b/app/src/main/java/xyz/fycz/myreader/ui/presenter/ReadPresenter.java index 42ef7d5..10873dc 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/presenter/ReadPresenter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/presenter/ReadPresenter.java @@ -216,7 +216,7 @@ public class ReadPresenter implements BasePresenter { @Override public void start() { - if (SharedPreUtils.getInstance().getBoolean("isNightFS", false)) { + if (SharedPreUtils.getInstance().getBoolean(mReadActivity.getString(R.string.isNightFS), false)) { mSetting.setDayStyle(!ColorUtil.isColorLight(mReadActivity.getColor(R.color.textPrimary))); } @@ -256,7 +256,7 @@ public class ReadPresenter implements BasePresenter { //当书籍Collected且书籍id不为空的时候保存上次阅读信息 if (isCollected && !StringHelper.isEmpty(mBook.getId())) { //保存上次阅读信息 - SharedPreUtils.getInstance().putString("lastRead", mBook.getId()); + SharedPreUtils.getInstance().putString(mReadActivity.getString(R.string.lastRead), mBook.getId()); } @@ -325,7 +325,7 @@ public class ReadPresenter implements BasePresenter { mBook = (Book) mReadActivity.getIntent().getSerializableExtra(APPCONST.BOOK); //mBook为空,说明是从快捷方式启动 if (mBook == null) { - String bookId = SharedPreUtils.getInstance().getString("lastRead", ""); + String bookId = SharedPreUtils.getInstance().getString(mReadActivity.getString(R.string.lastRead), ""); if ("".equals(bookId)) {//没有上次阅读信息 ToastUtils.showWarring("当前没有阅读任何书籍,无法加载上次阅读书籍!"); mReadActivity.finish(); diff --git a/app/src/main/java/xyz/fycz/myreader/util/HttpUtil.java b/app/src/main/java/xyz/fycz/myreader/util/HttpUtil.java index fdb169f..57c9f98 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/HttpUtil.java +++ b/app/src/main/java/xyz/fycz/myreader/util/HttpUtil.java @@ -1,11 +1,13 @@ package xyz.fycz.myreader.util; +import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.util.Base64; import android.util.Log; import com.google.gson.Gson; import okhttp3.*; +import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.application.TrustAllCerts; import xyz.fycz.myreader.webapi.callback.HttpCallback; @@ -53,14 +55,51 @@ public class HttpUtil { return ssfFactory; } + public static X509TrustManager createTrustAllManager() { + X509TrustManager tm = null; + try { + tm = new X509TrustManager() { + @SuppressLint("TrustAllX509TrustManager") + public void checkClientTrusted(X509Certificate[] chain, String authType) { + //do nothing,接受任意客户端证书 + } + + @SuppressLint("TrustAllX509TrustManager") + public void checkServerTrusted(X509Certificate[] chain, String authType) { + //do nothing,接受任意服务端证书 + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + } catch (Exception ignored) { + } + return tm; + } + + private static Interceptor getHeaderInterceptor() { + return chain -> { + Request request = chain.request() + .newBuilder() + .addHeader("Keep-Alive", "300") + .addHeader("Connection", "Keep-Alive") + .addHeader("Cache-Control", "no-cache") + .build(); + return chain.proceed(request); + }; + } + public static synchronized OkHttpClient getOkHttpClient() { if (mClient == null) { OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.connectTimeout(5, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS); - builder.sslSocketFactory(createSSLSocketFactory()); - builder.hostnameVerifier((hostname, session) -> true); + .writeTimeout(15, TimeUnit.SECONDS) + .sslSocketFactory(createSSLSocketFactory(), createTrustAllManager()) + .hostnameVerifier((hostname, session) -> true) + .protocols(Collections.singletonList(Protocol.HTTP_1_1)) + .addInterceptor(getHeaderInterceptor()); mClient = builder .build(); } @@ -271,10 +310,10 @@ public class HttpUtil { .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4168.3 Safari/537.36"); if (address.contains("qidian.com")) { SharedPreUtils spu = SharedPreUtils.getInstance(); - String cookie = spu.getString("qdCookie", ""); + String cookie = spu.getString(MyApplication.getmContext().getString(R.string.qdCookie), ""); if (cookie.equals("")) { requestBuilder.addHeader("cookie", "_csrfToken=eXRDlZxmRDLvFAmdgzqvwWAASrxxp2WkVlH4ZM7e; newstatisticUUID=1595991935_2026387981"); - }else { + } else { requestBuilder.addHeader("cookie", cookie); } } diff --git a/app/src/main/java/xyz/fycz/myreader/util/SharedPreUtils.java b/app/src/main/java/xyz/fycz/myreader/util/SharedPreUtils.java index b10afb8..7774671 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/SharedPreUtils.java +++ b/app/src/main/java/xyz/fycz/myreader/util/SharedPreUtils.java @@ -5,6 +5,8 @@ import android.content.SharedPreferences; import xyz.fycz.myreader.application.MyApplication; +import java.util.Map; + /** * SharedPreferences工具类 */ @@ -38,17 +40,27 @@ public class SharedPreUtils { public void putString(String key,String value){ sharedWritable.putString(key,value); - sharedWritable.commit(); + sharedWritable.apply(); } public void putInt(String key,int value){ sharedWritable.putInt(key, value); - sharedWritable.commit(); + sharedWritable.apply(); } public void putBoolean(String key,boolean value){ sharedWritable.putBoolean(key, value); - sharedWritable.commit(); + sharedWritable.apply(); + } + + public void putFloat(String key,float value){ + sharedWritable.putFloat(key, value); + sharedWritable.apply(); + } + + public void putLong(String key, long value){ + sharedWritable.putLong(key, value); + sharedWritable.apply(); } public String getString(String key){ @@ -74,4 +86,26 @@ public class SharedPreUtils { public boolean getBoolean(String key,boolean def){ return sharedReadable.getBoolean(key, false); } + + public float getFloat(String key){ + return getFloat(key, 0); + } + public float getFloat(String key, float def){ + return sharedReadable.getFloat(key, def); + } + public long getLong(String key){ + return getLong(key, 0); + } + public long getLong(String key, long def){ + return sharedReadable.getLong(key, def); + } + + public void remove(String key){ + sharedWritable.remove(key).apply(); + } + + public Map getAll(){ + return sharedReadable.getAll(); + } + } diff --git a/app/src/main/java/xyz/fycz/myreader/util/StringHelper.java b/app/src/main/java/xyz/fycz/myreader/util/StringHelper.java index ca2da97..f1bc149 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/StringHelper.java +++ b/app/src/main/java/xyz/fycz/myreader/util/StringHelper.java @@ -1,5 +1,7 @@ package xyz.fycz.myreader.util; +import xyz.fycz.myreader.util.utils.StringUtils; + import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Random; @@ -118,9 +120,9 @@ public class StringHelper { public static boolean isEmpty(String str){ if (str != null){ - str = str.replace(" ",""); + str = StringUtils.deleteWhitespace(str); } - return str == null || str.equals(""); + return str == null || str.equals("") || str.equals("null"); } /** diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/DocumentUtil.java b/app/src/main/java/xyz/fycz/myreader/util/utils/DocumentUtil.java new file mode 100644 index 0000000..a9e8ab7 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/DocumentUtil.java @@ -0,0 +1,387 @@ +package xyz.fycz.myreader.util.utils; + +import android.content.Context; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.documentfile.provider.DocumentFile; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.regex.Pattern; + +/** + * Created by PureDark on 2016/9/24. + */ + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class DocumentUtil { + + private static Pattern FilePattern = Pattern.compile("[\\\\/:*?\"<>|]"); + + public static boolean isFileExist(Context context, String fileName, String rootPath, String... subDirs) { + Uri rootUri; + if (rootPath.startsWith("content")) + rootUri = Uri.parse(rootPath); + else + rootUri = Uri.parse(Uri.decode(rootPath)); + return isFileExist(context, fileName, rootUri, subDirs); + } + + public static boolean isFileExist(Context context, String fileName, Uri rootUri, String... subDirs) { + DocumentFile root; + if ("content".equals(rootUri.getScheme())) + root = DocumentFile.fromTreeUri(context, rootUri); + else + root = DocumentFile.fromFile(new File(rootUri.getPath())); + return isFileExist(fileName, root, subDirs); + } + + public static boolean isFileExist(String fileName, DocumentFile root, String... subDirs) { + DocumentFile parent = getDirDocument(root, subDirs); + if (parent == null) + return false; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + if (file != null && file.exists()) + return true; + return false; + } + + public static DocumentFile createDirIfNotExist(Context context, String rootPath, String... subDirs) { + Uri rootUri; + if (rootPath.startsWith("content")) + rootUri = Uri.parse(rootPath); + else + rootUri = Uri.parse(Uri.decode(rootPath)); + return createDirIfNotExist(context, rootUri, subDirs); + } + + public static DocumentFile createDirIfNotExist(Context context, Uri rootUri, String... subDirs) { + DocumentFile root; + if ("content".equals(rootUri.getScheme())) + root = DocumentFile.fromTreeUri(context, rootUri); + else + root = DocumentFile.fromFile(new File(rootUri.getPath())); + return createDirIfNotExist(root, subDirs); + } + + public static DocumentFile createDirIfNotExist(@NonNull DocumentFile root, String... subDirs) { + DocumentFile parent = root; + try { + for (String subDir1 : subDirs) { + String subDirName = filenameFilter(Uri.decode(subDir1)); + DocumentFile subDir = parent.findFile(subDirName); + if (subDir == null) { + subDir = parent.createDirectory(subDirName); + } + parent = subDir; + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return parent; + } + + public static DocumentFile createFileIfNotExist(Context context, String fileName, String rootPath, String... subDirs) { + Uri rootUri; + if (rootPath.startsWith("content")) + rootUri = Uri.parse(rootPath); + else + rootUri = Uri.parse(Uri.decode(rootPath)); + return createFileIfNotExist(context, "", fileName, rootUri, subDirs); + } + + public static DocumentFile createFileIfNotExist(Context context, String fileName, Uri rootUri, String... subDirs) { + return createFileIfNotExist(context, "", fileName, rootUri, subDirs); + } + + public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, String rootPath, String... subDirs) { + Uri rootUri; + if (rootPath.startsWith("content")) + rootUri = Uri.parse(rootPath); + else + rootUri = Uri.parse(Uri.decode(rootPath)); + return createFileIfNotExist(context, mimeType, fileName, rootUri, subDirs); + } + + public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, Uri rootUri, String... subDirs) { + DocumentFile parent = createDirIfNotExist(context, rootUri, subDirs); + if (parent == null) + return null; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + if (file == null) { + file = parent.createFile(mimeType, fileName); + } + return file; + } + + public static boolean deleteFile(Context context, String fileName, String rootPath, String... subDirs) { + Uri rootUri; + if (rootPath.startsWith("content")) + rootUri = Uri.parse(rootPath); + else + rootUri = Uri.parse(Uri.decode(rootPath)); + return deleteFile(context, fileName, rootUri, subDirs); + } + + public static boolean deleteFile(Context context, String fileName, Uri rootUri, String... subDirs) { + DocumentFile root; + if ("content".equals(rootUri.getScheme())) + root = DocumentFile.fromTreeUri(context, rootUri); + else + root = DocumentFile.fromFile(new File(rootUri.getPath())); + return deleteFile(fileName, root, subDirs); + } + + public static boolean deleteFile(String fileName, DocumentFile root, String... subDirs) { + DocumentFile parent = getDirDocument(root, subDirs); + if (parent == null) + return false; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + return file != null && file.exists() && file.delete(); + } + + public static boolean writeBytes(Context context, byte[] data, String fileName, String rootPath, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootPath, subDirs); + if (parent == null) + return false; + DocumentFile file = parent.findFile(fileName); + return writeBytes(context, data, file.getUri()); + } + + public static boolean writeBytes(Context context, byte[] data, String fileName, Uri rootUri, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootUri, subDirs); + if (parent == null) + return false; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + return writeBytes(context, data, file.getUri()); + } + + public static boolean writeBytes(Context context, byte[] data, String fileName, DocumentFile root, String... subDirs) { + DocumentFile parent = getDirDocument(root, subDirs); + if (parent == null) + return false; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + return writeBytes(context, data, file.getUri()); + } + + public static boolean writeBytes(Context context, byte[] data, DocumentFile file) { + return writeBytes(context, data, file.getUri()); + } + + public static boolean writeBytes(Context context, byte[] data, Uri fileUri) { + try { + OutputStream out = context.getContentResolver().openOutputStream(fileUri, "wt"); //Write file need open with truncate mode, the mode truncate file upon opening (to zero bytes) + out.write(data); + out.close(); + return true; + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + public static boolean writeFromInputStream(Context context, InputStream inStream, DocumentFile file) { + return writeFromInputStream(context, inStream, file.getUri()); + } + + public static boolean writeFromInputStream(Context context, InputStream inStream, Uri fileUri) { + try { + OutputStream out = context.getContentResolver().openOutputStream(fileUri); + int byteread; + byte[] buffer = new byte[1024]; + while ((byteread = inStream.read(buffer)) > 0) { + out.write(buffer, 0, byteread); + } + inStream.close(); + out.close(); + return true; + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + public static byte[] readBytes(Context context, String fileName, String rootPath, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootPath, subDirs); + if (parent == null) + return null; + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return readBytes(context, file.getUri()); + } + + public static byte[] readBytes(Context context, String fileName, Uri rootUri, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootUri, subDirs); + if (parent == null) + return null; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return readBytes(context, file.getUri()); + } + + public static byte[] readBytes(Context context, String fileName, DocumentFile root, String... subDirs) { + DocumentFile parent = getDirDocument(root, subDirs); + if (parent == null) + return null; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return readBytes(context, file.getUri()); + } + + public static byte[] readBytes(Context context, DocumentFile file) { + if (file == null) + return null; + return readBytes(context, file.getUri()); + } + + public static byte[] readBytes(Context context, Uri fileUri) { + try { + InputStream fis = context.getContentResolver().openInputStream(fileUri); + int len = fis.available(); + byte[] buffer = new byte[len]; + fis.read(buffer); + fis.close(); + return buffer; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static DocumentFile getDirDocument(Context context, String rootPath, String... subDirs) { + Uri rootUri; + if (rootPath.startsWith("content")) + rootUri = Uri.parse(rootPath); + else + rootUri = Uri.parse(Uri.decode(rootPath)); + return getDirDocument(context, rootUri, subDirs); + } + + public static DocumentFile getDirDocument(Context context, Uri rootUri, String... subDirs) { + DocumentFile root; + if ("content".equals(rootUri.getScheme())) + root = DocumentFile.fromTreeUri(context, rootUri); + else + root = DocumentFile.fromFile(new File(rootUri.getPath())); + return getDirDocument(root, subDirs); + } + + public static DocumentFile getDirDocument(DocumentFile root, String... subDirs) { + DocumentFile parent = root; + for (int i = 0; i < subDirs.length; i++) { + String subDirName = Uri.decode(subDirs[i]); + DocumentFile subDir = parent.findFile(subDirName); + if (subDir != null) + parent = subDir; + else + return null; + } + return parent; + } + + public static OutputStream getFileOutputSteam(Context context, String fileName, String rootPath, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootPath, subDirs); + if (parent == null) + return null; + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return getFileOutputSteam(context, file.getUri()); + } + + public static OutputStream getFileOutputSteam(Context context, String fileName, Uri rootUri, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootUri, subDirs); + if (parent == null) + return null; + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return getFileOutputSteam(context, file.getUri()); + } + + public static OutputStream getFileOutputSteam(Context context, String fileName, DocumentFile root, String... subDirs) { + DocumentFile parent = getDirDocument(root, subDirs); + if (parent == null) + return null; + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return getFileOutputSteam(context, file.getUri()); + } + + public static OutputStream getFileOutputSteam(Context context, DocumentFile file) { + return getFileOutputSteam(context, file.getUri()); + } + + public static OutputStream getFileOutputSteam(Context context, Uri fileUri) { + try { + OutputStream out = context.getContentResolver().openOutputStream(fileUri); + return out; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static InputStream getFileInputSteam(Context context, String fileName, String rootPath, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootPath, subDirs); + if (parent == null) + return null; + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return getFileInputSteam(context, file.getUri()); + } + + public static InputStream getFileInputSteam(Context context, String fileName, Uri rootUri, String... subDirs) { + DocumentFile parent = getDirDocument(context, rootUri, subDirs); + if (parent == null) + return null; + fileName = filenameFilter(Uri.decode(fileName)); + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return getFileInputSteam(context, file.getUri()); + } + + public static InputStream getFileInputSteam(Context context, String fileName, DocumentFile root, String... subDirs) { + DocumentFile parent = getDirDocument(root, subDirs); + if (parent == null) + return null; + DocumentFile file = parent.findFile(fileName); + if (file == null) + return null; + return getFileInputSteam(context, file.getUri()); + } + + public static InputStream getFileInputSteam(Context context, DocumentFile file) { + return getFileInputSteam(context, file.getUri()); + } + + public static InputStream getFileInputSteam(Context context, Uri fileUri) { + try { + InputStream in = context.getContentResolver().openInputStream(fileUri); + return in; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String filenameFilter(String str) { + return str == null ? null : FilePattern.matcher(str).replaceAll("_"); + } + +} 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 5f299fd..6f7653d 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 @@ -1,21 +1,28 @@ package xyz.fycz.myreader.util.utils; import android.app.Application; +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.os.StatFs; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.util.Log; import info.monitorenter.cpdetector.io.*; import io.reactivex.Single; import io.reactivex.SingleEmitter; import io.reactivex.SingleOnSubscribe; import xyz.fycz.myreader.application.MyApplication; +import xyz.fycz.myreader.util.StringHelper; import java.io.*; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; -/** - * Created by newbiechen on 17-5-11. - */ public class FileUtils { //采用自己的格式去设置文件,防止文件被系统文件查询到 @@ -234,4 +241,592 @@ public class FileUtils { return charsetName; } + + + /** + * 写文本文件 在Android系统中,文件保存在 /data/data/PACKAGE_NAME/files/ 目录下 + * + * @param context + */ + public static void write(Context context, String fileName, String content) { + if (content == null) + content = ""; + + try { + FileOutputStream fos = context.openFileOutput(fileName, + Context.MODE_PRIVATE); + fos.write(content.getBytes()); + + fos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * 读取文本文件 + * + * @param context + * @param fileName + * @return + */ + public static String read(Context context, String fileName) { + try { + FileInputStream in = context.openFileInput(fileName); + return readInStream(in); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + private static String readInStream(FileInputStream inStream) { + try { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[512]; + int length = -1; + while ((length = inStream.read(buffer)) != -1) { + outStream.write(buffer, 0, length); + } + + outStream.close(); + inStream.close(); + return outStream.toString(); + } catch (IOException e) { + Log.i("FileTest", e.getMessage()); + } + return null; + } + + + /** + * 向手机写图片 + * + * @param buffer + * @param folder + * @param fileName + * @return + */ + public static boolean writeFile(byte[] buffer, String folder, + String fileName) { + boolean writeSucc = false; + + File fileDir = new File(folder); + if (!fileDir.exists()) { + fileDir.mkdirs(); + } + + File file = new File(folder,fileName); + FileOutputStream out = null; + try { + out = new FileOutputStream(file); + out.write(buffer); + writeSucc = true; + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return writeSucc; + } + + public static boolean writeFile(byte[] buffer, File file) { + boolean writeSucc = false; + FileOutputStream out = null; + try { + out = new FileOutputStream(file); + out.write(buffer); + writeSucc = true; + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return writeSucc; + } + + /** + * 根据文件绝对路径获取文件名 + * + * @param filePath + * @return + */ + public static String getFileName(String filePath) { + if (StringHelper.isEmpty(filePath)) + return ""; + //return filePath.substring(filePath.lastIndexOf("?") + 1); + return filePath.substring(filePath.lastIndexOf("/") + 1); + } + + /** + * 根据文件的绝对路径获取文件名但不包含扩展名 + * + * @param filePath + * @return + */ + public static String getFileNameNoFormat(String filePath) { + if (StringHelper.isEmpty(filePath)) { + return ""; + } + int point = filePath.lastIndexOf('.'); + return filePath.substring(filePath.lastIndexOf(File.separator) + 1, + point); + } + + /** + * 获取文件扩展名 + * + * @param fileName + * @return + */ + public static String getFileFormat(String fileName) { + if (StringHelper.isEmpty(fileName)) + return ""; + + int point = fileName.lastIndexOf('.'); + return fileName.substring(point + 1); + } + + + /** + * 获取目录文件个数 + * + * @return + */ + public long getFileList(File dir) { + long count = 0; + File[] files = dir.listFiles(); + count = files.length; + for (File file : files) { + if (file.isDirectory()) { + count = count + getFileList(file);// 递归 + count--; + } + } + return count; + } + + public static byte[] toBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int ch; + while ((ch = in.read()) != -1) { + out.write(ch); + } + byte buffer[] = out.toByteArray(); + out.close(); + return buffer; + } + + /** + * 检查文件是否存在 + * + * @param name + * @return + */ + public static boolean checkFileExists(String name) { + boolean status; + if (!name.equals("")) { + File path = Environment.getExternalStorageDirectory(); + File newPath = new File(path.toString() + name); + status = newPath.exists(); + } else { + status = false; + } + return status; + } + /** + * 返回项目的files目录 + * @return + */ + public static String getPath(){ + File path = Environment.getExternalStorageDirectory(); + return path.toString(); + } + + /** + * 检查路径是否存在 + * + * @param path + * @return + */ + public static boolean checkFilePathExists(String path) { + return new File(path).exists(); + } + + /** + * 计算SD卡的剩余空间 + * + * @return 返回-1,说明没有安装sd卡 + */ + public static long getFreeDiskSpace() { + String status = Environment.getExternalStorageState(); + long freeSpace = 0; + if (status.equals(Environment.MEDIA_MOUNTED)) { + try { + File path = Environment.getExternalStorageDirectory(); + StatFs stat = new StatFs(path.getPath()); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + freeSpace = availableBlocks * blockSize / 1024; + } catch (Exception e) { + e.printStackTrace(); + } + } else { + return -1; + } + return (freeSpace); + } + + /** + * 新建目录 + * + * @param directoryName + * @return + */ + public static boolean createDirectory(String directoryName) { + boolean status; + if (!directoryName.equals("")) { + File path = Environment.getExternalStorageDirectory(); + File newPath = new File(path.toString() + directoryName); + status = newPath.mkdir(); + status = true; + } else + status = false; + return status; + } + + /** + * 检查是否安装SD卡 + * + * @return + */ + public static boolean checkSaveLocationExists() { + String sDCardStatus = Environment.getExternalStorageState(); + boolean status; + status = sDCardStatus.equals(Environment.MEDIA_MOUNTED); + return status; + } + + /** + * 删除目录(包括:目录里的所有文件) + * + * @param fileName + * @return + */ + public static boolean deleteDirectory(String fileName) { + boolean status; + SecurityManager checker = new SecurityManager(); + + if (!fileName.equals("")) { + + File path = Environment.getExternalStorageDirectory(); + File newPath = new File(path.toString() + fileName); + checker.checkDelete(newPath.toString()); + if (newPath.isDirectory()) { + String[] listfile = newPath.list(); + // delete all files within the specified directory and then + // delete the directory + try { + for (int i = 0; i < listfile.length; i++) { + File deletedFile = new File(newPath.toString() + "/" + + listfile[i].toString()); + deletedFile.delete(); + } + newPath.delete(); + Log.i("DirManager delDirectory", fileName); + status = true; + } catch (Exception e) { + e.printStackTrace(); + status = false; + } + + } else + status = false; + } else + status = false; + return status; + } + + + + /** + * 删除空目录 + * + * 返回 0代表成功 ,1 代表没有删除权限, 2代表不是空目录,3 代表未知错误 + * + * @return + */ + public static int deleteBlankPath(String path) { + File f = new File(path); + if (!f.canWrite()) { + return 1; + } + if (f.list() != null && f.list().length > 0) { + return 2; + } + if (f.delete()) { + return 0; + } + return 3; + } + + /** + * 重命名 + * + * @param oldName + * @param newName + * @return + */ + public static boolean reNamePath(String oldName, String newName) { + File f = new File(oldName); + return f.renameTo(new File(newName)); + } + + /** + * 删除文件 + * + * @param filePath + */ + public static boolean deleteFileWithPath(String filePath) { + SecurityManager checker = new SecurityManager(); + File f = new File(filePath); + checker.checkDelete(filePath); + if (f.isFile()) { + Log.i("DirManager delFile", filePath); + f.delete(); + return true; + } + return false; + } + + /** + * 获取SD卡的根目录,末尾带\ + * + * @return + */ + public static String getSDRoot() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + + File.separator; + } + + /** + * 列出root目录下所有子目录 + * + + * @return 绝对路径 + */ + public static List listPath(String root) { + List allDir = new ArrayList(); + SecurityManager checker = new SecurityManager(); + File path = new File(root); + checker.checkRead(root); + if (path.isDirectory()) { + for (File f : path.listFiles()) { + if (f.isDirectory()) { + allDir.add(f.getAbsolutePath()); + } + } + } + return allDir; + } + + public enum PathStatus { + SUCCESS, EXITS, ERROR + } + + /** + * 创建目录 + * + */ + public static xyz.fycz.myreader.util.FileUtils.PathStatus createPath(String newPath) { + File path = new File(newPath); + if (path.exists()) { + return xyz.fycz.myreader.util.FileUtils.PathStatus.EXITS; + } + if (path.mkdir()) { + return xyz.fycz.myreader.util.FileUtils.PathStatus.SUCCESS; + } else { + return xyz.fycz.myreader.util.FileUtils.PathStatus.ERROR; + } + } + + /** + * 截取路径名 + * + * @return + */ + public static String getPathName(String absolutePath) { + int start = absolutePath.lastIndexOf(File.separator) + 1; + int end = absolutePath.length(); + return absolutePath.substring(start, end); + } + + + public static String getFilePathFromContentUri(Context context, Uri uri) { + String photoPath = ""; + if(context == null || uri == null) { + return photoPath; + } + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) { + String docId = DocumentsContract.getDocumentId(uri); + if(isExternalStorageDocument(uri)) { + String[] split = docId.split(":"); + if(split.length >= 2) { + String type = split[0]; + if("primary".equalsIgnoreCase(type)) { + photoPath = Environment.getExternalStorageDirectory() + "/" + split[1]; + } + } + } + else if(isDownloadsDocument(uri)) { + Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId)); + photoPath = getDataColumn(context, contentUri, null, null); + } + else if(isMediaDocument(uri)) { + String[] split = docId.split(":"); + if(split.length >= 2) { + String type = split[0]; + Uri contentUris = null; + if("image".equals(type)) { + contentUris = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } + else if("video".equals(type)) { + contentUris = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } + else if("audio".equals(type)) { + contentUris = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + String selection = MediaStore.Images.Media._ID + "=?"; + String[] selectionArgs = new String[] { split[1] }; + photoPath = getDataColumn(context, contentUris, selection, selectionArgs); + } + } + } + else if("file".equalsIgnoreCase(uri.getScheme())) { + photoPath = uri.getPath(); + } + else { + photoPath = getDataColumn(context, uri, null, null); + } + + return photoPath; + } + + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + String column = MediaStore.Images.Media.DATA; + String[] projection = { column }; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + int index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(index); + } + } finally { + if (cursor != null && !cursor.isClosed()) + cursor.close(); + } + return null; + } + + public static byte[] File2byte(String filePath) + { + byte[] buffer = null; + try + { + File file = new File(filePath); + FileInputStream fis = new FileInputStream(file); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] b = new byte[1024]; + int n; + while ((n = fis.read(b)) != -1) + { + bos.write(b, 0, n); + } + fis.close(); + bos.close(); + buffer = bos.toByteArray(); + } + catch (FileNotFoundException e) + { + e.printStackTrace(); + } + catch (IOException e) + { + e.printStackTrace(); + } + return buffer; + } + + public static void byte2File(byte[] buf, String filePath, String fileName) + { + BufferedOutputStream bos = null; + FileOutputStream fos = null; + File file = null; + try + { + File dir = new File(filePath); + if (!dir.exists() && dir.isDirectory()) + { + dir.mkdirs(); + } + file = new File(filePath + File.separator + fileName); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos); + bos.write(buf); + } + catch (Exception e) + { + e.printStackTrace(); + } + finally + { + if (bos != null) + { + try + { + bos.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + if (fos != null) + { + try + { + fos.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + } + } } diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/GsonExtensions.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/GsonExtensions.kt new file mode 100644 index 0000000..784586b --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/GsonExtensions.kt @@ -0,0 +1,43 @@ +package xyz.fycz.myreader.util.utils + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken +import org.jetbrains.anko.attempt +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +val GSON: Gson by lazy { + GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .create() +} + +inline fun genericType() = object : TypeToken() {}.type + + +@Throws(JsonSyntaxException::class) +inline fun Gson.fromJsonObject(json: String?): T? {//可转成任意类型 + return attempt { + val result: T? = fromJson(json, genericType()) + result + }.value +} + +@Throws(JsonSyntaxException::class) +inline fun Gson.fromJsonArray(json: String?): List? { + return attempt { + val result: List? = fromJson(json, ParameterizedTypeImpl(T::class.java)) + result + }.value +} + +class ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType { + override fun getRawType(): Type = List::class.java + + override fun getOwnerType(): Type? = null + + override fun getActualTypeArguments(): Array = arrayOf(clazz) +} diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/ImageLoader.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/ImageLoader.kt new file mode 100644 index 0000000..caf0bbb --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/ImageLoader.kt @@ -0,0 +1,50 @@ +package xyz.fycz.myreader.util.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.annotation.DrawableRes +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import java.io.File + +object ImageLoader { + + fun load(context: Context, path: String?): RequestBuilder { + return when { + path.isNullOrEmpty() -> Glide.with(context).load(path) + path.startsWith("http", true) -> Glide.with(context).load(path) + else -> try { + Glide.with(context).load(File(path)) + } catch (e: Exception) { + Glide.with(context).load(path) + } + } + } + + fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder { + return Glide.with(context).load(resId) + } + + fun load(context: Context, file: File?): RequestBuilder { + return Glide.with(context).load(file) + } + + fun load(context: Context, uri: Uri?): RequestBuilder { + return Glide.with(context).load(uri) + } + + fun load(context: Context, drawable: Drawable?): RequestBuilder { + return Glide.with(context).load(drawable) + } + + fun load(context: Context, bitmap: Bitmap?): RequestBuilder { + return Glide.with(context).load(bitmap) + } + + fun load(context: Context, bytes: ByteArray?): RequestBuilder { + return Glide.with(context).load(bytes) + } + +} diff --git a/app/src/main/java/xyz/fycz/myreader/util/webdav/README.md b/app/src/main/java/xyz/fycz/myreader/util/webdav/README.md new file mode 100644 index 0000000..f6ac57a --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/webdav/README.md @@ -0,0 +1 @@ +## 用于网络备份的WebDav \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/util/webdav/WebDav.kt b/app/src/main/java/xyz/fycz/myreader/util/webdav/WebDav.kt new file mode 100644 index 0000000..d6fbee4 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/webdav/WebDav.kt @@ -0,0 +1,250 @@ +package xyz.fycz.myreader.util.webdav + +import xyz.fycz.myreader.util.webdav.http.Handler +import xyz.fycz.myreader.util.webdav.http.HttpAuth +import okhttp3.* +import org.jsoup.Jsoup +import xyz.fycz.myreader.util.HttpUtil +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.UnsupportedEncodingException +import java.net.MalformedURLException +import java.net.URL +import java.net.URLEncoder +import java.util.* + +class WebDav @Throws(MalformedURLException::class) +constructor(urlStr: String) { + companion object { + // 指定返回哪些属性 + private const val DIR = + """ + + + + + + + + %s + + """ + } + + private val url: URL = URL(null, urlStr, Handler) + private val httpUrl: String? by lazy { + val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://") + try { + return@lazy URLEncoder.encode(raw, "UTF-8") + .replace("\\+".toRegex(), "%20") + .replace("%3A".toRegex(), ":") + .replace("%2F".toRegex(), "/") + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + return@lazy null + } + } + + var displayName: String? = null + var size: Long = 0 + var exists = false + var parent = "" + var urlName = "" + get() { + if (field.isEmpty()) { + this.urlName = ( + if (parent.isEmpty()) url.file + else url.toString().replace(parent, "") + ).replace("/", "") + } + return field + } + + fun getPath() = url.toString() + + fun getHost() = url.host + + /** + * 填充文件信息。实例化WebDAVFile对象时,并没有将远程文件的信息填充到实例中。需要手动填充! + * + * @return 远程文件是否存在 + */ + @Throws(IOException::class) + fun indexFileInfo(): Boolean { + propFindResponse(ArrayList())?.let { response -> + if (!response.isSuccessful) { + this.exists = false + return false + } + response.body()?.let { + if (it.string().isNotEmpty()) { + return true + } + } + } + return false + } + + /** + * 列出当前路径下的文件 + * + * @param propsList 指定列出文件的哪些属性 + * @return 文件列表 + */ + @Throws(IOException::class) + @JvmOverloads + fun listFiles(propsList: ArrayList = ArrayList()): List { + propFindResponse(propsList)?.let { response -> + if (response.isSuccessful) { + response.body()?.let { body -> + return parseDir(body.string()) + } + } + } + return ArrayList() + } + + @Throws(IOException::class) + private fun propFindResponse(propsList: ArrayList, depth: Int = 1): Response? { + val requestProps = StringBuilder() + for (p in propsList) { + requestProps.append("\n") + } + val requestPropsStr: String + requestPropsStr = if (requestProps.toString().isEmpty()) { + DIR.replace("%s", "") + } else { + String.format(DIR, requestProps.toString() + "\n") + } + httpUrl?.let { url -> + val request = Request.Builder() + .url(url) + // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 + // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 + .method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)) + + HttpAuth.auth?.let { + request.header( + "Authorization", + Credentials.basic(it.user, it.pass) + ) + } + request.header("Depth", if (depth < 0) "infinity" else depth.toString()) + return HttpUtil.getOkHttpClient().newCall(request.build()).execute() + } + return null + } + + private fun parseDir(s: String): List { + val list = ArrayList() + val document = Jsoup.parse(s) + val elements = document.getElementsByTag("d:response") + httpUrl?.let { url -> + val baseUrl = if (url.endsWith("/")) url else "$url/" + for (element in elements) { + val href = element.getElementsByTag("d:href")[0].text() + if (!href.endsWith("/")) { + val fileName = href.substring(href.lastIndexOf("/") + 1) + val webDavFile: WebDav + try { + webDavFile = WebDav(baseUrl + fileName) + webDavFile.displayName = fileName + webDavFile.urlName = href + list.add(webDavFile) + } catch (e: MalformedURLException) { + e.printStackTrace() + } + } + } + } + return list + } + + /** + * 根据自己的URL,在远程处创建对应的文件夹 + * + * @return 是否创建成功 + */ + @Throws(IOException::class) + fun makeAsDir(): Boolean { + httpUrl?.let { url -> + val request = Request.Builder() + .url(url) + .method("MKCOL", null) + return execRequest(request) + } + return false + } + + /** + * 下载到本地 + * + * @param savedPath 本地的完整路径,包括最后的文件名 + * @param replaceExisting 是否替换本地的同名文件 + * @return 下载是否成功 + */ + fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { + if (File(savedPath).exists()) { + if (!replaceExisting) return false + } + val inputS = getInputStream() ?: return false + File(savedPath).writeBytes(inputS.readBytes()) + return true + } + + /** + * 上传文件 + */ + @Throws(IOException::class) + @JvmOverloads + fun upload(localPath: String, contentType: String? = null): Boolean { + val file = File(localPath) + if (!file.exists()) return false + val mediaType = if (contentType == null) null else MediaType.parse(contentType) + // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 + val fileBody = RequestBody.create(mediaType, file) + httpUrl?.let { + val request = Request.Builder() + .url(it) + .put(fileBody) + return execRequest(request) + } + return false + } + + /** + * 执行请求,获取响应结果 + * @param requestBuilder 因为还需要追加验证信息,所以此处传递Request.Builder的对象,而不是Request的对象 + * @return 请求执行的结果 + */ + @Throws(IOException::class) + private fun execRequest(requestBuilder: Request.Builder): Boolean { + HttpAuth.auth?.let { + requestBuilder.header( + "Authorization", + Credentials.basic(it.user, it.pass) + ) + } + val response = HttpUtil.getOkHttpClient().newCall(requestBuilder.build()).execute() + return response.isSuccessful + } + + private fun getInputStream(): InputStream? { + httpUrl?.let { url -> + val request = Request.Builder().url(url) + HttpAuth.auth?.let { + request.header("Authorization", Credentials.basic(it.user, it.pass)) + } + try { + return HttpUtil.getOkHttpClient().newCall(request.build()).execute().body()?.byteStream() + } catch (e: IOException) { + e.printStackTrace() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/util/webdav/http/Handler.kt b/app/src/main/java/xyz/fycz/myreader/util/webdav/http/Handler.kt new file mode 100644 index 0000000..fed42e9 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/webdav/http/Handler.kt @@ -0,0 +1,16 @@ +package xyz.fycz.myreader.util.webdav.http + +import java.net.URL +import java.net.URLConnection +import java.net.URLStreamHandler + +object Handler : URLStreamHandler() { + + override fun getDefaultPort(): Int { + return 80 + } + + public override fun openConnection(u: URL): URLConnection? { + return null + } +} diff --git a/app/src/main/java/xyz/fycz/myreader/util/webdav/http/HttpAuth.kt b/app/src/main/java/xyz/fycz/myreader/util/webdav/http/HttpAuth.kt new file mode 100644 index 0000000..b40118d --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/util/webdav/http/HttpAuth.kt @@ -0,0 +1,9 @@ +package xyz.fycz.myreader.util.webdav.http + +object HttpAuth { + + var auth: Auth? = null + + class Auth internal constructor(val user: String, val pass: String) + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/CommonApi.java b/app/src/main/java/xyz/fycz/myreader/webapi/CommonApi.java index 2804f90..b48db7f 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/CommonApi.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/CommonApi.java @@ -1,10 +1,14 @@ package xyz.fycz.myreader.webapi; import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.annotations.NonNull; import okhttp3.MediaType; import okhttp3.RequestBody; import xyz.fycz.myreader.common.URLCONST; import xyz.fycz.myreader.entity.SearchBookBean; +import xyz.fycz.myreader.greendao.entity.Chapter; import xyz.fycz.myreader.model.mulvalmap.ConcurrentMultiValueMap; import xyz.fycz.myreader.util.StringHelper; import xyz.fycz.myreader.util.utils.OkHttpUtils; @@ -16,6 +20,7 @@ import xyz.fycz.myreader.webapi.crawler.read.FYReadCrawler; import xyz.fycz.myreader.webapi.crawler.read.TianLaiReadCrawler; import java.io.IOException; +import java.util.List; public class CommonApi extends BaseApi { @@ -41,6 +46,27 @@ public class CommonApi extends BaseApi { }); } + + + /** + * 获取章节列表 + * + * @param url + */ + public static Observable> getBookChapters(String url, final ReadCrawler rc) { + String charset = rc.getCharset(); + + return Observable.create(emitter -> { + try { + emitter.onNext(rc.getChaptersFromHtml(OkHttpUtils.getHtml(url, charset))); + } catch (Exception e) { + e.printStackTrace(); + emitter.onError(e); + } + emitter.onComplete(); + }); + } + /** * 获取章节正文 * @@ -75,6 +101,24 @@ public class CommonApi extends BaseApi { }); } + /** + * 获取章节正文 + * + * @param url + */ + + public static Observable getChapterContent(String url, final ReadCrawler rc) { + String charset = rc.getCharset(); + return Observable.create(emitter -> { + try { + emitter.onNext(rc.getContentFormHtml(OkHttpUtils.getHtml(url, charset))); + } catch (Exception e) { + e.printStackTrace(); + emitter.onError(e); + } + emitter.onComplete(); + }); + } /** * 搜索小说 @@ -130,11 +174,11 @@ public class CommonApi extends BaseApi { }else { emitter.onNext(rc.getBooksFromSearchHtml(OkHttpUtils.getHtml(makeSearchUrl(rc.getSearchLink(), key), finalCharset))); } - emitter.onComplete(); - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); emitter.onError(e); } + emitter.onComplete(); }); } @@ -155,7 +199,12 @@ public class CommonApi extends BaseApi { url = book.getInfoUrl(); } return Observable.create(emitter -> { - emitter.onNext(bic.getBookInfo(OkHttpUtils.getHtml(url, bic.getCharset()), book)); + try { + emitter.onNext(bic.getBookInfo(OkHttpUtils.getHtml(url, bic.getCharset()), book)); + } catch (Exception e) { + e.printStackTrace(); + emitter.onError(e); + } emitter.onComplete(); }); } @@ -180,7 +229,6 @@ public class CommonApi extends BaseApi { @Override public void onError(Exception e) { callback.onError(e); - } }); } diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/LanZousApi.java b/app/src/main/java/xyz/fycz/myreader/webapi/LanZousApi.java index 90063fa..edccd43 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/LanZousApi.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/LanZousApi.java @@ -2,6 +2,7 @@ package xyz.fycz.myreader.webapi; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; +import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.webapi.callback.ResultCallback; import xyz.fycz.myreader.common.URLCONST; @@ -93,7 +94,7 @@ public class LanZousApi { SharedPreUtils spu = SharedPreUtils.getInstance(); String lanzousKeyStart = "var pposturl = '"; try { - lanzousKeyStart = spu.getString("lanzousKeyStart"); + lanzousKeyStart = spu.getString(MyApplication.getmContext().getString(R.string.lanzousKeyStart)); }catch (Exception e){ e.printStackTrace(); } diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java index c1a97e6..1716f60 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java @@ -1,6 +1,8 @@ package xyz.fycz.myreader.webapi.crawler; +import xyz.fycz.myreader.R; +import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.enums.BookSource; import xyz.fycz.myreader.util.SharedPreUtils; import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler; @@ -20,7 +22,7 @@ public class ReadCrawlerUtil { public static ArrayList getReadCrawlers() { SharedPreUtils spu = SharedPreUtils.getInstance(); - String searchSource = spu.getString("searchSource", null); + String searchSource = spu.getString(MyApplication.getmContext().getString(R.string.searchSource), null); ArrayList readCrawlers = new ArrayList<>(); if (searchSource == null) { StringBuilder sb = new StringBuilder(); @@ -33,7 +35,7 @@ public class ReadCrawlerUtil { } sb.deleteCharAt(sb.lastIndexOf(",")); searchSource = sb.toString(); - spu.putString("searchSource", searchSource); + spu.putString(MyApplication.getmContext().getString(R.string.searchSource), searchSource); } else if (!"".equals(searchSource)){ String[] sources = searchSource.split(","); for (String source : sources) { @@ -45,7 +47,7 @@ public class ReadCrawlerUtil { public static HashMap getDisableSources() { SharedPreUtils spu = SharedPreUtils.getInstance(); - String searchSource = spu.getString("searchSource", null); + String searchSource = spu.getString(MyApplication.getmContext().getString(R.string.searchSource), null); HashMap mSources = new HashMap<>(); if (searchSource == null) { for (BookSource bookSource : BookSource.values()) { @@ -79,7 +81,7 @@ public class ReadCrawlerUtil { sb.append(","); } sb.deleteCharAt(sb.lastIndexOf(",")); - SharedPreUtils.getInstance().putString("searchSource", sb.toString()); + SharedPreUtils.getInstance().putString(MyApplication.getmContext().getString(R.string.searchSource), sb.toString()); } public static ReadCrawler getReadCrawler(String bookSource) { diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/find/QiDianMobileRank.java b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/find/QiDianMobileRank.java index 1be184a..53263cd 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/find/QiDianMobileRank.java +++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/find/QiDianMobileRank.java @@ -4,6 +4,7 @@ import android.content.Context; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.webapi.callback.ResultCallback; import xyz.fycz.myreader.entity.bookstore.BookType; @@ -136,7 +137,7 @@ public class QiDianMobileRank extends FindCrawler { } url = url.replace("{sex}", !isFemale ? sex[0] : sex[1]); SharedPreUtils spu = SharedPreUtils.getInstance(); - String cookie = spu.getString("qdCookie", ""); + String cookie = spu.getString(MyApplication.getmContext().getString(R.string.qdCookie), ""); if (!cookie.equals("")) { url = url.replace("{cookie}", StringHelper.getSubString(cookie, "_csrfToken=", ";")); } else { diff --git a/app/src/main/java/xyz/fycz/myreader/widget/CoverImageView.kt b/app/src/main/java/xyz/fycz/myreader/widget/CoverImageView.kt new file mode 100644 index 0000000..702e37e --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/widget/CoverImageView.kt @@ -0,0 +1,158 @@ +package xyz.fycz.myreader.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import android.text.TextPaint +import android.util.AttributeSet +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import xyz.fycz.myreader.R +import xyz.fycz.myreader.util.utils.ImageLoader + + +class CoverImageView : androidx.appcompat.widget.AppCompatImageView { + internal var width: Float = 0.toFloat() + internal var height: Float = 0.toFloat() + private var nameHeight = 0f + private var authorHeight = 0f + private val namePaint = TextPaint() + private val authorPaint = TextPaint() + private var name: String? = null + private var author: String? = null + private var loadFailed = false + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + init { + namePaint.typeface = Typeface.DEFAULT_BOLD + namePaint.isAntiAlias = true + namePaint.textAlign = Paint.Align.CENTER + namePaint.textSkewX = -0.2f + authorPaint.typeface = Typeface.DEFAULT + authorPaint.isAntiAlias = true + authorPaint.textAlign = Paint.Align.CENTER + authorPaint.textSkewX = -0.1f + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) + val measuredHeight = measuredWidth * 7 / 5 + super.onMeasure( + widthMeasureSpec, + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + ) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + width = getWidth().toFloat() + height = getHeight().toFloat() + namePaint.textSize = width / 6 + namePaint.strokeWidth = namePaint.textSize / 10 + authorPaint.textSize = width / 9 + authorPaint.strokeWidth = authorPaint.textSize / 10 + nameHeight = height / 2 + authorHeight = nameHeight + authorPaint.fontSpacing + } + + override fun onDraw(canvas: Canvas) { + if (width >= 10 && height > 10) { + @SuppressLint("DrawAllocation") + val path = Path() + //四个圆角 + path.moveTo(10f, 0f) + path.lineTo(width - 10, 0f) + path.quadTo(width, 0f, width, 10f) + path.lineTo(width, height - 10) + path.quadTo(width, height, width - 10, height) + path.lineTo(10f, height) + path.quadTo(0f, height, 0f, height - 10) + path.lineTo(0f, 10f) + path.quadTo(0f, 0f, 10f, 0f) + + canvas.clipPath(path) + } + super.onDraw(canvas) + if (!loadFailed) return + name?.let { + namePaint.color = Color.WHITE + namePaint.style = Paint.Style.STROKE + canvas.drawText(it, width / 2, nameHeight, namePaint) + namePaint.color = Color.RED + namePaint.style = Paint.Style.FILL + canvas.drawText(it, width / 2, nameHeight, namePaint) + } + author?.let { + authorPaint.color = Color.WHITE + authorPaint.style = Paint.Style.STROKE + canvas.drawText(it, width / 2, authorHeight, authorPaint) + authorPaint.color = Color.RED + authorPaint.style = Paint.Style.FILL + canvas.drawText(it, width / 2, authorHeight, authorPaint) + } + } + + fun setHeight(height: Int) { + val width = height * 5 / 7 + minimumWidth = width + } + + private fun setText(name: String?, author: String?) { + this.name = + when { + name == null -> null + name.length > 5 -> name.substring(0, 4) + "…" + else -> name + } + this.author = + when { + author == null -> null + author.length > 8 -> author.substring(0, 7) + "…" + else -> author + } + } + + fun load(path: String?, name: String?, author: String?) { + setText(name, author) + ImageLoader.load(context, path)//Glide自动识别http://和file:// + .placeholder(R.mipmap.default_cover) + .error(R.mipmap.default_cover) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + loadFailed = true + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + loadFailed = false + return false + } + + }) + .centerCrop() + .into(this) + } +} diff --git a/app/src/main/java/xyz/fycz/myreader/widget/page/LocalPageLoader.java b/app/src/main/java/xyz/fycz/myreader/widget/page/LocalPageLoader.java index ebbb13e..c48cb16 100644 --- a/app/src/main/java/xyz/fycz/myreader/widget/page/LocalPageLoader.java +++ b/app/src/main/java/xyz/fycz/myreader/widget/page/LocalPageLoader.java @@ -2,7 +2,10 @@ package xyz.fycz.myreader.widget.page; import android.util.Log; import io.reactivex.*; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; import xyz.fycz.myreader.application.MyApplication; import xyz.fycz.myreader.webapi.callback.ResultCallback; import xyz.fycz.myreader.common.APPCONST; @@ -82,178 +85,197 @@ public class LocalPageLoader extends PageLoader { * 1. 序章的添加 * 2. 章节存在的书本的虚拟分章效果 */ - public void loadChapters(final ResultCallback resultCallback) { - MyApplication.getApplication().newThread(() -> { - mBookFile = new File(mCollBook.getChapterUrl()); - //获取文件编码 - mCharset = FileUtils.getFileEncode(mBookFile.getAbsolutePath()); - List chapters = new ArrayList<>(); - RandomAccessFile bookStream = null; - boolean hasChapter = false; - try { - //获取文件流 - bookStream = new RandomAccessFile(mBookFile, "r"); - //寻找匹配文章标题的正则表达式,判断是否存在章节名 - hasChapter = checkChapterType(bookStream); - //加载章节 - byte[] buffer = new byte[BUFFER_SIZE]; - //获取到的块起始点,在文件中的位置 - long curOffset = 0; - //block的个数 - int blockPos = 0; - //读取的长度 - int length; - - //获取文件中的数据到buffer,直到没有数据为止 - while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) { - ++blockPos; - //如果存在Chapter - if (hasChapter) { - //将数据转换成String - String blockContent = new String(buffer, 0, length, mCharset); - //当前Block下使过的String的指针 - int seekPos = 0; - //进行正则匹配 - Matcher matcher = mChapterPattern.matcher(blockContent); - //如果存在相应章节 - while (matcher.find()) { - //获取匹配到的字符在字符串中的起始位置 - int chapterStart = matcher.start(); - - //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容 - //第一种情况一定是序章 第二种情况可能是上一个章节的内容 - if (seekPos == 0 && chapterStart != 0) { - //获取当前章节的内容 - String chapterContent = blockContent.substring(seekPos, chapterStart); - //设置指针偏移 - seekPos += chapterContent.length(); - - //如果当前对整个文件的偏移位置为0的话,那么就是序章 - if (curOffset == 0) { - //创建序章 - TxtChapter preChapter = new TxtChapter(); - preChapter.title = "序章"; - preChapter.start = 0; - preChapter.end = chapterContent.getBytes(mCharset).length; //获取String的byte值,作为最终值 - - //如果序章大小大于30才添加进去 - if (preChapter.end - preChapter.start > 30) { - chapters.add(preChapter); - } - - //创建当前章节 - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = preChapter.end; - chapters.add(curChapter); - } - //否则就block分割之后,上一个章节的剩余内容 - else { - //获取上一章节 - TxtChapter lastChapter = chapters.get(chapters.size() - 1); - //将当前段落添加上一章去 - lastChapter.end += chapterContent.getBytes(mCharset).length; - - //如果章节内容太小,则移除 - if (lastChapter.end - lastChapter.start < 30) { - chapters.remove(lastChapter); - } - - //创建当前章节 - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = lastChapter.end; - chapters.add(curChapter); - } - } else { - //是否存在章节 - if (chapters.size() != 0) { - //获取章节内容 - String chapterContent = blockContent.substring(seekPos, matcher.start()); - seekPos += chapterContent.length(); - - //获取上一章节 - TxtChapter lastChapter = chapters.get(chapters.size() - 1); - lastChapter.end = lastChapter.start + chapterContent.getBytes(mCharset).length; - - //如果章节内容太小,则移除 - if (lastChapter.end - lastChapter.start < 30) { - chapters.remove(lastChapter); - } - - //创建当前章节 - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = lastChapter.end; - chapters.add(curChapter); - } - //如果章节不存在则创建章节 - else { - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = 0; - chapters.add(curChapter); - } + public List loadChapters() throws IOException { + mBookFile = new File(mCollBook.getChapterUrl()); + //获取文件编码 + mCharset = FileUtils.getFileEncode(mBookFile.getAbsolutePath()); + List chapters = new ArrayList<>(); + RandomAccessFile bookStream = null; + boolean hasChapter = false; + //获取文件流 + bookStream = new RandomAccessFile(mBookFile, "r"); + //寻找匹配文章标题的正则表达式,判断是否存在章节名 + hasChapter = checkChapterType(bookStream); + //加载章节 + byte[] buffer = new byte[BUFFER_SIZE]; + //获取到的块起始点,在文件中的位置 + long curOffset = 0; + //block的个数 + int blockPos = 0; + //读取的长度 + int length; + + //获取文件中的数据到buffer,直到没有数据为止 + while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) { + ++blockPos; + //如果存在Chapter + if (hasChapter) { + //将数据转换成String + String blockContent = new String(buffer, 0, length, mCharset); + //当前Block下使过的String的指针 + int seekPos = 0; + //进行正则匹配 + Matcher matcher = mChapterPattern.matcher(blockContent); + //如果存在相应章节 + while (matcher.find()) { + //获取匹配到的字符在字符串中的起始位置 + int chapterStart = matcher.start(); + + //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容 + //第一种情况一定是序章 第二种情况可能是上一个章节的内容 + if (seekPos == 0 && chapterStart != 0) { + //获取当前章节的内容 + String chapterContent = blockContent.substring(seekPos, chapterStart); + //设置指针偏移 + seekPos += chapterContent.length(); + + //如果当前对整个文件的偏移位置为0的话,那么就是序章 + if (curOffset == 0) { + //创建序章 + TxtChapter preChapter = new TxtChapter(); + preChapter.title = "序章"; + preChapter.start = 0; + preChapter.end = chapterContent.getBytes(mCharset).length; //获取String的byte值,作为最终值 + + //如果序章大小大于30才添加进去 + if (preChapter.end - preChapter.start > 30) { + chapters.add(preChapter); + } + + //创建当前章节 + TxtChapter curChapter = new TxtChapter(); + curChapter.title = matcher.group(); + curChapter.start = preChapter.end; + chapters.add(curChapter); + } + //否则就block分割之后,上一个章节的剩余内容 + else { + //获取上一章节 + TxtChapter lastChapter = chapters.get(chapters.size() - 1); + //将当前段落添加上一章去 + lastChapter.end += chapterContent.getBytes(mCharset).length; + + //如果章节内容太小,则移除 + if (lastChapter.end - lastChapter.start < 30) { + chapters.remove(lastChapter); } + + //创建当前章节 + TxtChapter curChapter = new TxtChapter(); + curChapter.title = matcher.group(); + curChapter.start = lastChapter.end; + chapters.add(curChapter); + } + } else { + //是否存在章节 + if (chapters.size() != 0) { + //获取章节内容 + String chapterContent = blockContent.substring(seekPos, matcher.start()); + seekPos += chapterContent.length(); + + //获取上一章节 + TxtChapter lastChapter = chapters.get(chapters.size() - 1); + lastChapter.end = lastChapter.start + chapterContent.getBytes(mCharset).length; + + //如果章节内容太小,则移除 + if (lastChapter.end - lastChapter.start < 30) { + chapters.remove(lastChapter); + } + + //创建当前章节 + TxtChapter curChapter = new TxtChapter(); + curChapter.title = matcher.group(); + curChapter.start = lastChapter.end; + chapters.add(curChapter); + } + //如果章节不存在则创建章节 + else { + TxtChapter curChapter = new TxtChapter(); + curChapter.title = matcher.group(); + curChapter.start = 0; + chapters.add(curChapter); } } - //进行本地虚拟分章 - else { - //章节在buffer的偏移量 - int chapterOffset = 0; - //当前剩余可分配的长度 - int strLength = length; - //分章的位置 - int chapterPos = 0; - - while (strLength > 0) { - ++chapterPos; - //是否长度超过一章 - if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) { - //在buffer中一章的终止点 - int end = length; - //寻找换行符作为终止点 - for (int i = chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER; i < length; ++i) { - if (buffer[i] == Charset.BLANK) { - end = i; - break; - } - } - TxtChapter chapter = new TxtChapter(); - chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")"; - chapter.start = curOffset + chapterOffset + 1; - chapter.end = curOffset + end; - chapters.add(chapter); - //减去已经被分配的长度 - strLength = strLength - (end - chapterOffset); - //设置偏移的位置 - chapterOffset = end; - } else { - TxtChapter chapter = new TxtChapter(); - chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")"; - chapter.start = curOffset + chapterOffset + 1; - chapter.end = curOffset + length; - chapters.add(chapter); - strLength = 0; + } + } + //进行本地虚拟分章 + else { + //章节在buffer的偏移量 + int chapterOffset = 0; + //当前剩余可分配的长度 + int strLength = length; + //分章的位置 + int chapterPos = 0; + + while (strLength > 0) { + ++chapterPos; + //是否长度超过一章 + if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) { + //在buffer中一章的终止点 + int end = length; + //寻找换行符作为终止点 + for (int i = chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER; i < length; ++i) { + if (buffer[i] == Charset.BLANK) { + end = i; + break; } } + TxtChapter chapter = new TxtChapter(); + chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")"; + chapter.start = curOffset + chapterOffset + 1; + chapter.end = curOffset + end; + chapters.add(chapter); + //减去已经被分配的长度 + strLength = strLength - (end - chapterOffset); + //设置偏移的位置 + chapterOffset = end; + } else { + TxtChapter chapter = new TxtChapter(); + chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")"; + chapter.start = curOffset + chapterOffset + 1; + chapter.end = curOffset + length; + chapters.add(chapter); + strLength = 0; } + } + } - //block的偏移点 - curOffset += length; + //block的偏移点 + curOffset += length; - if (hasChapter) { - //设置上一章的结尾 - TxtChapter lastChapter = chapters.get(chapters.size() - 1); - lastChapter.end = curOffset; - } + if (hasChapter) { + //设置上一章的结尾 + TxtChapter lastChapter = chapters.get(chapters.size() - 1); + lastChapter.end = curOffset; + } - //当添加的block太多的时候,执行GC - if (blockPos % 15 == 0) { - System.gc(); - System.runFinalization(); - } - } + //当添加的block太多的时候,执行GC + if (blockPos % 15 == 0) { + System.gc(); + System.runFinalization(); + } + } + + IOUtils.close(bookStream); + System.gc(); + System.runFinalization(); + return chapters; + } + + public void loadChapters(final ResultCallback resultCallback) { + // 通过RxJava异步处理分章事件 + Single.create((SingleOnSubscribe>) e -> { + e.onSuccess(loadChapters()); + }).compose(RxUtils::toSimpleSingle).subscribe(new SingleObserver>() { + @Override + public void onSubscribe(Disposable d) { + mChapterDisp = d; + } + + @Override + public void onSuccess(List chapters) { + mChapterDisp = null; + isChapterListPrepare = true; List mChapters = new ArrayList<>(); int i = 0; for (TxtChapter txtChapter : chapters) { @@ -272,17 +294,14 @@ public class LocalPageLoader extends PageLoader { if (resultCallback != null) { resultCallback.onFinish(mChapters, 1); } - } catch (IOException e) { - e.printStackTrace(); - if (resultCallback != null) { - resultCallback.onError(e); - } - } finally { - IOUtils.close(bookStream); - System.gc(); - System.runFinalization(); } + @Override + public void onError(Throwable e) { + resultCallback.onError((Exception) e); + chapterError(); + Log.d(TAG, "file load error:" + e.toString()); + } }); } @@ -369,59 +388,6 @@ public class LocalPageLoader extends PageLoader { return; } - - // 通过RxJava异步处理分章事件 - Single.create((SingleOnSubscribe) e -> { - loadChapters(null); - e.onSuccess(new Void()); - }).compose(RxUtils::toSimpleSingle).subscribe(new SingleObserver() { - @Override - public void onSubscribe(Disposable d) { - mChapterDisp = d; - } - - @Override - public void onSuccess(Void value) { - mChapterDisp = null; - isChapterListPrepare = true; - - // 提示目录加载完成 - if (mPageChangeListener != null) { - mPageChangeListener.onCategoryFinish(mChapterList); - } - - mCollBook.setChapterTotalNum(mChapterList.size()); - - // 加载并显示当前章节 - openChapter(); - } - - @Override - public void onError(Throwable e) { - chapterError(); - Log.d(TAG, "file load error:" + e.toString()); - } - }); - /*loadChapters(new ResultCallback() { - @Override - public void onFinish(Object o, int code) { - isChapterListPrepare = true; - - // 提示目录加载完成 - if (mPageChangeListener != null) { - mPageChangeListener.onCategoryFinish(mChapterList); - } - - // 加载并显示当前章节 - openChapter(); - } - - @Override - public void onError(Exception e) { - chapterError(); - Log.d(TAG, "file load error:" + e.toString()); - } - });*/ } @Override diff --git a/app/src/main/res/layout/activity_more_setting.xml b/app/src/main/res/layout/activity_more_setting.xml index 72562e8..961a2bc 100644 --- a/app/src/main/res/layout/activity_more_setting.xml +++ b/app/src/main/res/layout/activity_more_setting.xml @@ -16,6 +16,28 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + + android:layout_height="match_parent" + android:descendantFocusability="blocksDescendants"> diff --git a/app/src/main/res/layout/activity_webdav_setting.xml b/app/src/main/res/layout/activity_webdav_setting.xml new file mode 100644 index 0000000..455b969 --- /dev/null +++ b/app/src/main/res/layout/activity_webdav_setting.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/edit_dialog.xml b/app/src/main/res/layout/edit_dialog.xml index 2d819ca..a06af11 100644 --- a/app/src/main/res/layout/edit_dialog.xml +++ b/app/src/main/res/layout/edit_dialog.xml @@ -6,8 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="5dp" - app:counterEnabled="true" - app:counterMaxLength="10"> + app:counterEnabled="true"> - + app:srcCompat="@mipmap/default_cover" /> - + - + + diff --git a/app/src/main/res/layout/listview_book_store_book_item.xml b/app/src/main/res/layout/listview_book_store_book_item.xml index 61653a3..e64c3c3 100644 --- a/app/src/main/res/layout/listview_book_store_book_item.xml +++ b/app/src/main/res/layout/listview_book_store_book_item.xml @@ -2,17 +2,17 @@ - + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools" + android:orientation="horizontal" + android:background="@color/colorBackground" + android:padding="5dp"> - + app:srcCompat="@mipmap/default_cover" /> @@ -45,7 +45,7 @@ android:layout_height="wrap_content" android:paddingStart="5dp" android:layout_marginTop="6dp" - android:text="author" + tools:text="author" android:maxLines="1" android:textSize="13sp" android:textColor="@color/textSecondary"/> @@ -56,7 +56,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="1dp" - android:text="newest_chapter" + tools:text="newest_chapter" android:maxLines="1" android:textSize="13sp" android:textColor="@color/textSecondary"/> @@ -65,7 +65,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="2dp" - android:text="book desc" + tools:text="book desc" android:ellipsize="end" android:maxLines="1" android:textSize="13sp" @@ -77,7 +77,7 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentStart="true" - android:text="type" + tools:text="type" android:maxLines="1" android:textSize="13sp" android:textColor="@color/textSecondary"/> @@ -88,7 +88,7 @@ android:paddingTop="3dp" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" - android:text="book source" + tools:text="book source" android:maxLines="1" android:textSize="13sp" android:textColor="@color/textSecondary"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 78cbd9f..7ff3058 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -156,6 +156,26 @@ 最新章节:%1$s 书源:%1$s 共%2$d个源 + + lanzousKeyStart + downloadLink + qdCookie + searchSource + synTime + curBookGroupId + curBookGroupName + lastRead + threadNum + isNightFS + isReadTopTip + isReadDownloadAllTip + WebDav 服务器地址 + WebDav 账号 + WebDav 密码 + 从WebDav恢复 + WebDav设置 + WebDav备份与恢复 + 常亮 diff --git a/app/version_code.properties b/app/version_code.properties index 94c95aa..09cb3df 100644 --- a/app/version_code.properties +++ b/app/version_code.properties @@ -1,2 +1,2 @@ -#Fri Oct 02 22:23:04 CST 2020 -VERSION_CODE=150 +#Sat Oct 03 16:52:44 CST 2020 +VERSION_CODE=151 diff --git a/build.gradle b/build.gradle index 9e4877d..4f0e703 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,21 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. - +ext { + support_library_version = '28.0.0' +} buildscript { + ext.kotlin_version = '1.3.72' repositories { jcenter() mavenCentral() google() + maven { url 'http://s3.amazonaws.com/fabric-artifacts/public' } + maven { url 'https://plugins.gradle.org/m2/' } } dependencies { classpath 'com.android.tools.build:gradle:3.6.2' classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2' classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }