Repository: gedoor/MyBookshelf Branch: master Commit: bb5a99058f38 Files: 732 Total size: 3.2 MB Directory structure: gitextract_l4utlapa/ ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── ReadMe.md │ ├── build.gradle │ ├── gradle.properties │ ├── key.properties.jks │ ├── proguard-rules.pro │ └── src/ │ ├── debug/ │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ ├── 18PlusList.txt │ │ │ ├── data/ │ │ │ │ └── BookSourceXxl.json │ │ │ ├── disclaimer.md │ │ │ ├── txtChapterRule.json │ │ │ ├── updateLog.md │ │ │ └── web/ │ │ │ ├── bookshelf.css │ │ │ ├── bookshelf.html │ │ │ ├── bookshelf.js │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── java/ │ │ │ └── com/ │ │ │ └── kunfei/ │ │ │ └── bookshelf/ │ │ │ ├── DbHelper.java │ │ │ ├── MApplication.java │ │ │ ├── base/ │ │ │ │ ├── BaseDialogFragment.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ ├── BaseModelImpl.java │ │ │ │ ├── BaseService.kt │ │ │ │ ├── BaseTabActivity.java │ │ │ │ ├── BaseViewModel.kt │ │ │ │ ├── MBaseActivity.java │ │ │ │ ├── MBaseFragment.java │ │ │ │ ├── README.md │ │ │ │ ├── VMBaseFragment.kt │ │ │ │ ├── adapter/ │ │ │ │ │ ├── DiffRecyclerAdapter.kt │ │ │ │ │ ├── ItemAnimation.kt │ │ │ │ │ ├── ItemViewHolder.kt │ │ │ │ │ ├── RecyclerAdapter.kt │ │ │ │ │ └── animations/ │ │ │ │ │ ├── AlphaInAnimation.kt │ │ │ │ │ ├── BaseAnimation.kt │ │ │ │ │ ├── ScaleInAnimation.kt │ │ │ │ │ ├── SlideInBottomAnimation.kt │ │ │ │ │ ├── SlideInLeftAnimation.kt │ │ │ │ │ └── SlideInRightAnimation.kt │ │ │ │ └── observer/ │ │ │ │ ├── MyObserver.java │ │ │ │ └── MySingleObserver.java │ │ │ ├── bean/ │ │ │ │ ├── BaseBookBean.java │ │ │ │ ├── BaseChapterBean.java │ │ │ │ ├── BookChapterBean.java │ │ │ │ ├── BookContentBean.java │ │ │ │ ├── BookInfoBean.java │ │ │ │ ├── BookKindBean.java │ │ │ │ ├── BookShelfBean.java │ │ │ │ ├── BookSource3Bean.java │ │ │ │ ├── BookSourceBean.java │ │ │ │ ├── BookmarkBean.java │ │ │ │ ├── CookieBean.java │ │ │ │ ├── DownloadBookBean.java │ │ │ │ ├── DownloadChapterBean.java │ │ │ │ ├── FindKindBean.java │ │ │ │ ├── FindKindGroupBean.java │ │ │ │ ├── LocBookShelfBean.java │ │ │ │ ├── OpenChapterBean.java │ │ │ │ ├── ReplaceRuleBean.java │ │ │ │ ├── SearchBookBean.java │ │ │ │ ├── SearchHistoryBean.java │ │ │ │ ├── TwoDataBean.java │ │ │ │ ├── TxtChapterRuleBean.java │ │ │ │ ├── UpdateInfoBean.java │ │ │ │ └── WebChapterBean.java │ │ │ ├── constant/ │ │ │ │ ├── AppConst.kt │ │ │ │ ├── AppConstant.java │ │ │ │ ├── BookType.java │ │ │ │ ├── RxBusTag.java │ │ │ │ └── TimeConstants.java │ │ │ ├── help/ │ │ │ │ ├── AppFrontBackHelper.java │ │ │ │ ├── BlurTransformation.java │ │ │ │ ├── BookshelfHelp.java │ │ │ │ ├── ChangeSourceHelp.java │ │ │ │ ├── ChapterContentHelp.java │ │ │ │ ├── CrashHandler.java │ │ │ │ ├── DefaultValueHelper.kt │ │ │ │ ├── DocumentHelper.java │ │ │ │ ├── Donate.java │ │ │ │ ├── EncodeConverter.java │ │ │ │ ├── ExoPlayerHelper.kt │ │ │ │ ├── FileHelp.java │ │ │ │ ├── IntentData.kt │ │ │ │ ├── ItemTouchCallback.java │ │ │ │ ├── JsExtensions.java │ │ │ │ ├── LauncherIcon.java │ │ │ │ ├── MediaManager.java │ │ │ │ ├── ProcessTextHelp.java │ │ │ │ ├── ReadBookControl.java │ │ │ │ ├── SSLSocketClient.java │ │ │ │ ├── SourceHelp.kt │ │ │ │ ├── UTF8BOMFighter.java │ │ │ │ ├── UpdateManager.java │ │ │ │ ├── coroutine/ │ │ │ │ │ ├── CompositeCoroutine.kt │ │ │ │ │ ├── Coroutine.kt │ │ │ │ │ └── CoroutineContainer.kt │ │ │ │ ├── glide/ │ │ │ │ │ ├── ImageLoader.kt │ │ │ │ │ ├── OkHttpGlideModule.kt │ │ │ │ │ ├── OkHttpModeLoaderFactory.kt │ │ │ │ │ ├── OkHttpModelLoader.kt │ │ │ │ │ └── OkHttpStreamFetcher.kt │ │ │ │ ├── media/ │ │ │ │ │ ├── LoaderCreator.java │ │ │ │ │ ├── LocalFileLoader.java │ │ │ │ │ └── MediaStoreHelper.java │ │ │ │ ├── permission/ │ │ │ │ │ ├── ActivitySource.kt │ │ │ │ │ ├── FragmentSource.kt │ │ │ │ │ ├── OnPermissionsDeniedCallback.kt │ │ │ │ │ ├── OnPermissionsGrantedCallback.kt │ │ │ │ │ ├── OnPermissionsResultCallback.kt │ │ │ │ │ ├── OnRequestPermissionsResultCallback.kt │ │ │ │ │ ├── PermissionActivity.kt │ │ │ │ │ ├── Permissions.kt │ │ │ │ │ ├── PermissionsCompat.kt │ │ │ │ │ ├── Request.kt │ │ │ │ │ ├── RequestManager.kt │ │ │ │ │ ├── RequestPlugins.kt │ │ │ │ │ └── RequestSource.kt │ │ │ │ └── storage/ │ │ │ │ ├── Backup.kt │ │ │ │ ├── BackupRestoreUi.kt │ │ │ │ ├── Preferences.kt │ │ │ │ ├── Restore.kt │ │ │ │ └── WebDavHelp.kt │ │ │ ├── model/ │ │ │ │ ├── BookSourceManager.java │ │ │ │ ├── Exceptions.kt │ │ │ │ ├── ImportBookModel.java │ │ │ │ ├── ReplaceRuleManager.java │ │ │ │ ├── SavedSource.java │ │ │ │ ├── SearchBookModel.java │ │ │ │ ├── TxtChapterRuleManager.java │ │ │ │ ├── UpLastChapterModel.java │ │ │ │ ├── WebBookModel.java │ │ │ │ ├── analyzeRule/ │ │ │ │ │ ├── AnalyzeByJSonPath.java │ │ │ │ │ ├── AnalyzeByJSoup.java │ │ │ │ │ ├── AnalyzeByRegex.java │ │ │ │ │ ├── AnalyzeByXPath.java │ │ │ │ │ ├── AnalyzeHeaders.java │ │ │ │ │ ├── AnalyzeRule.java │ │ │ │ │ └── AnalyzeUrl.java │ │ │ │ ├── content/ │ │ │ │ │ ├── BookChapterList.java │ │ │ │ │ ├── BookContent.java │ │ │ │ │ ├── BookInfo.java │ │ │ │ │ ├── BookList.java │ │ │ │ │ ├── Debug.java │ │ │ │ │ ├── VipThrowable.java │ │ │ │ │ └── WebBook.java │ │ │ │ ├── impl/ │ │ │ │ │ ├── IDownloadTask.java │ │ │ │ │ ├── IHttpGetApi.java │ │ │ │ │ └── IHttpPostApi.java │ │ │ │ └── task/ │ │ │ │ ├── AnalyzeNextUrlTask.java │ │ │ │ ├── CheckSourceTask.java │ │ │ │ └── DownloadTaskImpl.java │ │ │ ├── presenter/ │ │ │ │ ├── BookDetailPresenter.java │ │ │ │ ├── BookListPresenter.java │ │ │ │ ├── BookSourcePresenter.java │ │ │ │ ├── ChoiceBookPresenter.java │ │ │ │ ├── FindBookPresenter.java │ │ │ │ ├── ImportBookPresenter.java │ │ │ │ ├── MainPresenter.java │ │ │ │ ├── ReadBookPresenter.java │ │ │ │ ├── ReplaceRulePresenter.java │ │ │ │ ├── SearchBookPresenter.java │ │ │ │ ├── SourceEditPresenter.java │ │ │ │ ├── TxtChapterRulePresenter.java │ │ │ │ └── contract/ │ │ │ │ ├── BookDetailContract.java │ │ │ │ ├── BookListContract.java │ │ │ │ ├── BookSourceContract.java │ │ │ │ ├── ChoiceBookContract.java │ │ │ │ ├── FindBookContract.java │ │ │ │ ├── ImportBookContract.java │ │ │ │ ├── MainContract.java │ │ │ │ ├── ReadBookContract.java │ │ │ │ ├── ReplaceRuleContract.java │ │ │ │ ├── SearchBookContract.java │ │ │ │ ├── SourceEditContract.java │ │ │ │ └── TxtChapterRuleContract.java │ │ │ ├── service/ │ │ │ │ ├── CheckSourceService.java │ │ │ │ ├── DownloadService.java │ │ │ │ ├── MediaButtonIntentReceiver.java │ │ │ │ ├── ReadAloudService.java │ │ │ │ ├── ShareService.java │ │ │ │ └── WebService.java │ │ │ ├── utils/ │ │ │ │ ├── ACache.java │ │ │ │ ├── ActivityExtensions.kt │ │ │ │ ├── BatteryUtil.java │ │ │ │ ├── BitmapUtil.java │ │ │ │ ├── ColorUtils.kt │ │ │ │ ├── ContextExtensions.kt │ │ │ │ ├── ConvertUtils.kt │ │ │ │ ├── DensityUtil.java │ │ │ │ ├── DialogExtensions.kt │ │ │ │ ├── DocumentExtensions.kt │ │ │ │ ├── DocumentUtil.java │ │ │ │ ├── DrawableUtil.kt │ │ │ │ ├── EncoderUtils.kt │ │ │ │ ├── EncodingDetect.java │ │ │ │ ├── FastXmlSerializer.java │ │ │ │ ├── FileStack.java │ │ │ │ ├── FileUtils.kt │ │ │ │ ├── FloatExtensions.kt │ │ │ │ ├── GsonExtensions.kt │ │ │ │ ├── GsonUtils.java │ │ │ │ ├── HandlerUtils.kt │ │ │ │ ├── IOUtils.java │ │ │ │ ├── IntentExtensions.kt │ │ │ │ ├── ListUtil.java │ │ │ │ ├── MD5Utils.java │ │ │ │ ├── MarkdownUtils.java │ │ │ │ ├── MeUtils.java │ │ │ │ ├── NetworkUtils.java │ │ │ │ ├── ReadAssets.java │ │ │ │ ├── RealPathUtil.kt │ │ │ │ ├── RxUtils.java │ │ │ │ ├── ScreenUtils.java │ │ │ │ ├── Selector.java │ │ │ │ ├── SoftInputUtil.java │ │ │ │ ├── StringExtensions.kt │ │ │ │ ├── StringJoiner.java │ │ │ │ ├── StringUtils.java │ │ │ │ ├── SystemUtil.java │ │ │ │ ├── TimeUtils.java │ │ │ │ ├── Toasts.kt │ │ │ │ ├── UriExtensions.kt │ │ │ │ ├── UrlEncoderUtils.java │ │ │ │ ├── XmlUtils.java │ │ │ │ ├── ZipUtils.java │ │ │ │ ├── dialogs/ │ │ │ │ │ ├── AlertBuilder.kt │ │ │ │ │ ├── AndroidAlertBuilder.kt │ │ │ │ │ ├── AndroidDialogs.kt │ │ │ │ │ ├── AndroidSelectors.kt │ │ │ │ │ └── SelectItem.kt │ │ │ │ ├── download/ │ │ │ │ │ ├── DownloadUtils.java │ │ │ │ │ ├── JsDownloadInterceptor.java │ │ │ │ │ ├── JsDownloadListener.java │ │ │ │ │ ├── JsResponseBody.java │ │ │ │ │ └── Service.java │ │ │ │ ├── theme/ │ │ │ │ │ ├── ATH.java │ │ │ │ │ ├── ATHUtil.java │ │ │ │ │ ├── MaterialValueHelper.java │ │ │ │ │ ├── MaterialValueHelper.kt │ │ │ │ │ ├── NavigationViewUtil.java │ │ │ │ │ ├── ThemeStore.java │ │ │ │ │ ├── ThemeStoreInterface.java │ │ │ │ │ ├── ThemeStorePrefKeys.java │ │ │ │ │ ├── TintHelper.java │ │ │ │ │ └── ViewUtil.java │ │ │ │ ├── viewbindingdelegate/ │ │ │ │ │ ├── ActivityViewBindings.kt │ │ │ │ │ ├── FragmentViewBindings.kt │ │ │ │ │ └── ViewBindingProperty.kt │ │ │ │ └── webdav/ │ │ │ │ ├── README.md │ │ │ │ ├── WebDav.kt │ │ │ │ └── http/ │ │ │ │ ├── Handler.kt │ │ │ │ └── HttpAuth.kt │ │ │ ├── view/ │ │ │ │ ├── activity/ │ │ │ │ │ ├── AboutActivity.java │ │ │ │ │ ├── BookCoverEditActivity.java │ │ │ │ │ ├── BookDetailActivity.java │ │ │ │ │ ├── BookInfoEditActivity.java │ │ │ │ │ ├── BookSourceActivity.java │ │ │ │ │ ├── ChapterListActivity.java │ │ │ │ │ ├── ChoiceBookActivity.java │ │ │ │ │ ├── DonateActivity.java │ │ │ │ │ ├── DownloadActivity.java │ │ │ │ │ ├── ImportBookActivity.java │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ ├── QRCodeScanActivity.java │ │ │ │ │ ├── ReadBookActivity.java │ │ │ │ │ ├── ReadStyleActivity.java │ │ │ │ │ ├── ReceivingSharedActivity.java │ │ │ │ │ ├── ReplaceRuleActivity.java │ │ │ │ │ ├── SearchBookActivity.java │ │ │ │ │ ├── SettingActivity.java │ │ │ │ │ ├── SourceDebugActivity.java │ │ │ │ │ ├── SourceEditActivity.java │ │ │ │ │ ├── SourceLoginActivity.java │ │ │ │ │ ├── ThemeSettingActivity.java │ │ │ │ │ ├── TxtChapterRuleActivity.java │ │ │ │ │ ├── WebViewActivity.kt │ │ │ │ │ ├── WelcomeActivity.java │ │ │ │ │ └── WelcomeBookActivity.java │ │ │ │ ├── adapter/ │ │ │ │ │ ├── BookShelfAdapter.java │ │ │ │ │ ├── BookShelfGridAdapter.java │ │ │ │ │ ├── BookShelfListAdapter.java │ │ │ │ │ ├── BookSourceAdapter.java │ │ │ │ │ ├── BookmarkAdapter.java │ │ │ │ │ ├── ChangeSourceAdapter.java │ │ │ │ │ ├── ChapterListAdapter.java │ │ │ │ │ ├── ChoiceBookAdapter.java │ │ │ │ │ ├── DownloadAdapter.java │ │ │ │ │ ├── FileSystemAdapter.java │ │ │ │ │ ├── FindKindAdapter.java │ │ │ │ │ ├── FindLeftAdapter.java │ │ │ │ │ ├── FindRightAdapter.java │ │ │ │ │ ├── ReplaceRuleAdapter.java │ │ │ │ │ ├── SearchBookAdapter.java │ │ │ │ │ ├── SearchBookshelfAdapter.java │ │ │ │ │ ├── SourceDebugAdapter.java │ │ │ │ │ ├── SourceEditAdapter.java │ │ │ │ │ ├── TxtChapterRuleAdapter.java │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── BaseListAdapter.java │ │ │ │ │ │ ├── BaseViewHolder.java │ │ │ │ │ │ ├── IViewHolder.java │ │ │ │ │ │ ├── OnItemClickListenerTwo.java │ │ │ │ │ │ └── ViewHolderImpl.java │ │ │ │ │ └── view/ │ │ │ │ │ └── FileHolder.java │ │ │ │ ├── dialog/ │ │ │ │ │ └── SourceLoginDialog.kt │ │ │ │ ├── fragment/ │ │ │ │ │ ├── BaseFileFragment.java │ │ │ │ │ ├── BookListFragment.java │ │ │ │ │ ├── BookmarkFragment.java │ │ │ │ │ ├── ChapterListFragment.java │ │ │ │ │ ├── FileCategoryFragment.java │ │ │ │ │ ├── FindBookFragment.java │ │ │ │ │ ├── LocalBookFragment.java │ │ │ │ │ ├── SettingsFragment.kt │ │ │ │ │ ├── ThemeSettingsFragment.java │ │ │ │ │ └── WebDavSettingsFragment.java │ │ │ │ └── popupwindow/ │ │ │ │ ├── CheckAddShelfPop.java │ │ │ │ ├── KeyboardToolPop.kt │ │ │ │ ├── MediaPlayerPop.java │ │ │ │ ├── MoreSettingPop.kt │ │ │ │ ├── ReadAdjustMarginPop.kt │ │ │ │ ├── ReadAdjustPop.java │ │ │ │ ├── ReadBottomMenu.java │ │ │ │ ├── ReadInterfacePop.kt │ │ │ │ └── ReadLongPressPop.java │ │ │ ├── web/ │ │ │ │ ├── HttpServer.java │ │ │ │ ├── ShareServer.java │ │ │ │ ├── WebSocketServer.java │ │ │ │ ├── controller/ │ │ │ │ │ ├── BookshelfController.java │ │ │ │ │ ├── SourceController.java │ │ │ │ │ └── SourceDebugWebSocket.java │ │ │ │ └── utils/ │ │ │ │ ├── AssetsWeb.java │ │ │ │ └── ReturnData.java │ │ │ └── widget/ │ │ │ ├── BadgeView.java │ │ │ ├── HorizontalListView.java │ │ │ ├── RotateLoading.java │ │ │ ├── ScrollTextView.java │ │ │ ├── check_box/ │ │ │ │ └── SmoothCheckBox.java │ │ │ ├── explosion_field/ │ │ │ │ ├── ExplosionAnimator.java │ │ │ │ ├── ExplosionField.java │ │ │ │ ├── OnAnimatorListener.java │ │ │ │ └── Utils.java │ │ │ ├── filepicker/ │ │ │ │ ├── adapter/ │ │ │ │ │ ├── FileAdapter.java │ │ │ │ │ └── PathAdapter.java │ │ │ │ ├── drawable/ │ │ │ │ │ ├── StateBaseDrawable.java │ │ │ │ │ └── StateColorDrawable.java │ │ │ │ ├── entity/ │ │ │ │ │ ├── FileItem.java │ │ │ │ │ └── JavaBean.java │ │ │ │ ├── icons/ │ │ │ │ │ └── FilePickerIcon.java │ │ │ │ ├── picker/ │ │ │ │ │ └── FilePicker.java │ │ │ │ ├── popup/ │ │ │ │ │ ├── BasicPopup.java │ │ │ │ │ └── ConfirmPopup.java │ │ │ │ └── util/ │ │ │ │ ├── ConvertUtils.java │ │ │ │ ├── DateUtils.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── ScreenUtils.java │ │ │ │ └── StorageUtils.java │ │ │ ├── font/ │ │ │ │ ├── FontAdapter.java │ │ │ │ └── FontSelector.java │ │ │ ├── image/ │ │ │ │ ├── CoverImageView.kt │ │ │ │ └── FilletImageView.java │ │ │ ├── itemdecoration/ │ │ │ │ ├── DividerGridItemDecoration.java │ │ │ │ └── DividerItemDecoration.java │ │ │ ├── modialog/ │ │ │ │ ├── BaseDialog.java │ │ │ │ ├── BookmarkDialog.java │ │ │ │ ├── ChangeSourceDialog.java │ │ │ │ ├── DownLoadDialog.java │ │ │ │ ├── InputDialog.java │ │ │ │ ├── MoDialogHUD.java │ │ │ │ ├── MoDialogView.java │ │ │ │ ├── PageKeyDialog.kt │ │ │ │ ├── ReplaceRuleDialog.java │ │ │ │ └── TxtChapterRuleDialog.java │ │ │ ├── number/ │ │ │ │ ├── NumberButton.java │ │ │ │ ├── NumberPickerDialog.java │ │ │ │ └── NumberPickerPreference.java │ │ │ ├── page/ │ │ │ │ ├── ChapterProvider.java │ │ │ │ ├── PageLoader.java │ │ │ │ ├── PageLoaderEpub.java │ │ │ │ ├── PageLoaderNet.java │ │ │ │ ├── PageLoaderText.java │ │ │ │ ├── PageView.java │ │ │ │ ├── TxtChapter.kt │ │ │ │ ├── TxtChar.kt │ │ │ │ ├── TxtLine.kt │ │ │ │ ├── TxtPage.kt │ │ │ │ └── animation/ │ │ │ │ ├── CoverPageAnim.java │ │ │ │ ├── HorizonPageAnim.java │ │ │ │ ├── NonePageAnim.java │ │ │ │ ├── PageAnimation.java │ │ │ │ ├── ScrollPageAnim.java │ │ │ │ ├── SimulationPageAnim.java │ │ │ │ └── SlidePageAnim.java │ │ │ ├── prefs/ │ │ │ │ ├── ATEPreferenceCategory.java │ │ │ │ ├── ATESwitchPreference.java │ │ │ │ └── IconListPreference.java │ │ │ ├── recycler/ │ │ │ │ ├── expandable/ │ │ │ │ │ ├── BaseExpandAbleViewHolder.java │ │ │ │ │ ├── BaseExpandableRecyclerAdapter.java │ │ │ │ │ ├── OnRecyclerViewListener.java │ │ │ │ │ └── bean/ │ │ │ │ │ ├── BaseItem.java │ │ │ │ │ ├── GroupItem.java │ │ │ │ │ └── RecyclerViewData.java │ │ │ │ ├── refresh/ │ │ │ │ │ ├── BaseRefreshListener.java │ │ │ │ │ ├── OnLoadMoreListener.java │ │ │ │ │ ├── OnRefreshWithProgressListener.java │ │ │ │ │ ├── RefreshLayout.java │ │ │ │ │ ├── RefreshProgressBar.java │ │ │ │ │ ├── RefreshRecyclerView.java │ │ │ │ │ ├── RefreshRecyclerViewAdapter.java │ │ │ │ │ └── RefreshScrollView.java │ │ │ │ ├── scroller/ │ │ │ │ │ ├── FastScrollRecyclerView.java │ │ │ │ │ ├── FastScrollStateChangeListener.java │ │ │ │ │ └── FastScroller.java │ │ │ │ └── sectioned/ │ │ │ │ ├── GridSpacingItemDecoration.java │ │ │ │ ├── SectionedRecyclerViewAdapter.java │ │ │ │ └── SectionedSpanSizeLookup.java │ │ │ ├── seekbar/ │ │ │ │ ├── VerticalSeekBar.kt │ │ │ │ └── VerticalSeekBarWrapper.kt │ │ │ └── views/ │ │ │ ├── ATEAccentBgTextView.java │ │ │ ├── ATEAccentStrokeTextView.java │ │ │ ├── ATEAutoCompleteTextView.java │ │ │ ├── ATECheckBox.java │ │ │ ├── ATEEditText.java │ │ │ ├── ATEPrimaryTextView.java │ │ │ ├── ATEProgressBar.java │ │ │ ├── ATERadioButton.java │ │ │ ├── ATERadioNoButton.java │ │ │ ├── ATESecondaryTextView.java │ │ │ ├── ATESeekBar.java │ │ │ ├── ATEStockSwitch.java │ │ │ ├── ATEStrokeTextView.java │ │ │ ├── ATESwitch.java │ │ │ └── ATETextInputLayout.java │ │ └── res/ │ │ ├── anim/ │ │ │ ├── anim_none.xml │ │ │ ├── anim_readbook_bottom_in.xml │ │ │ ├── anim_readbook_bottom_out.xml │ │ │ ├── anim_readbook_top_in.xml │ │ │ ├── anim_readbook_top_out.xml │ │ │ ├── moprogress_bottom_in.xml │ │ │ ├── moprogress_bottom_out.xml │ │ │ ├── moprogress_in.xml │ │ │ ├── moprogress_in_bottom_right.xml │ │ │ ├── moprogress_in_top_right.xml │ │ │ ├── moprogress_out.xml │ │ │ ├── moprogress_out_bottom_right.xml │ │ │ └── moprogress_out_top_right.xml │ │ ├── color/ │ │ │ └── selector_menu_text.xml │ │ ├── drawable/ │ │ │ ├── bg_chapter_item_divider.xml │ │ │ ├── bg_edit.xml │ │ │ ├── bg_ib_pre.xml │ │ │ ├── bg_ib_pre_round.xml │ │ │ ├── bg_textfield_search.xml │ │ │ ├── fastscroll_bubble.xml │ │ │ ├── fastscroll_handle.xml │ │ │ ├── fastscroll_track.xml │ │ │ ├── ic_about.xml │ │ │ ├── ic_add.xml │ │ │ ├── ic_add_online.xml │ │ │ ├── ic_arrange.xml │ │ │ ├── ic_arrow_back.xml │ │ │ ├── ic_arrow_drop_down.xml │ │ │ ├── ic_arrow_drop_up.xml │ │ │ ├── ic_author.xml │ │ │ ├── ic_auto_page.xml │ │ │ ├── ic_auto_page_stop.xml │ │ │ ├── ic_back_last.xml │ │ │ ├── ic_backup.xml │ │ │ ├── ic_baseline_label.xml │ │ │ ├── ic_book_has.xml │ │ │ ├── ic_book_last.xml │ │ │ ├── ic_book_source_manage.xml │ │ │ ├── ic_bookmark.xml │ │ │ ├── ic_brightness.xml │ │ │ ├── ic_bug_report_black_24dp.xml │ │ │ ├── ic_cancel.xml │ │ │ ├── ic_chapter_list.xml │ │ │ ├── ic_check.xml │ │ │ ├── ic_check_source.xml │ │ │ ├── ic_clear_all.xml │ │ │ ├── ic_copy.xml │ │ │ ├── ic_cursor_left.xml │ │ │ ├── ic_cursor_right.xml │ │ │ ├── ic_daytime.xml │ │ │ ├── ic_disclaimer.xml │ │ │ ├── ic_donate.xml │ │ │ ├── ic_download.xml │ │ │ ├── ic_download_line.xml │ │ │ ├── ic_edit.xml │ │ │ ├── ic_exchange.xml │ │ │ ├── ic_expand_less_24dp.xml │ │ │ ├── ic_expand_more_24dp.xml │ │ │ ├── ic_faq.xml │ │ │ ├── ic_find_replace.xml │ │ │ ├── ic_folder.xml │ │ │ ├── ic_format_line_spacing.xml │ │ │ ├── ic_groups.xml │ │ │ ├── ic_history.xml │ │ │ ├── ic_import.xml │ │ │ ├── ic_interface_setting.xml │ │ │ ├── ic_last_read.xml │ │ │ ├── ic_launch.xml │ │ │ ├── ic_list.xml │ │ │ ├── ic_mail.xml │ │ │ ├── ic_more_vert.xml │ │ │ ├── ic_network_check.xml │ │ │ ├── ic_pause_24dp.xml │ │ │ ├── ic_pause_outline_24dp.xml │ │ │ ├── ic_play_24dp.xml │ │ │ ├── ic_play_outline_24dp.xml │ │ │ ├── ic_qq_group.xml │ │ │ ├── ic_read.xml │ │ │ ├── ic_read_aloud.xml │ │ │ ├── ic_refresh_black_24dp.xml │ │ │ ├── ic_refresh_white_24dp.xml │ │ │ ├── ic_remove.xml │ │ │ ├── ic_restore.xml │ │ │ ├── ic_save.xml │ │ │ ├── ic_scan.xml │ │ │ ├── ic_scoring.xml │ │ │ ├── ic_search.xml │ │ │ ├── ic_select_all.xml │ │ │ ├── ic_settings.xml │ │ │ ├── ic_share.xml │ │ │ ├── ic_skip_next.xml │ │ │ ├── ic_skip_previous.xml │ │ │ ├── ic_stop_black_24dp.xml │ │ │ ├── ic_swap_outline_24dp.xml │ │ │ ├── ic_theme.xml │ │ │ ├── ic_time_add_24dp.xml │ │ │ ├── ic_timer_black_24dp.xml │ │ │ ├── ic_toc.xml │ │ │ ├── ic_top_source.xml │ │ │ ├── ic_translate.xml │ │ │ ├── ic_tune.xml │ │ │ ├── ic_update.xml │ │ │ ├── ic_version.xml │ │ │ ├── ic_view_quilt.xml │ │ │ ├── ic_volume_up.xml │ │ │ ├── ic_web_outline.xml │ │ │ ├── ic_web_service_noti.xml │ │ │ ├── ic_web_service_phone.xml │ │ │ ├── image_welcome.xml │ │ │ ├── searchview_line.xml │ │ │ ├── selector_common_bg.xml │ │ │ ├── selector_fillet_btn_bg.xml │ │ │ ├── selector_tv_black.xml │ │ │ ├── shape_card_view.xml │ │ │ ├── shape_fillet_btn.xml │ │ │ ├── shape_fillet_btn_press.xml │ │ │ ├── shape_pop_checkaddshelf_bg.xml │ │ │ ├── shape_radius_1dp.xml │ │ │ ├── shape_space_divider.xml │ │ │ └── shape_text_cursor.xml │ │ ├── drawable-v21/ │ │ │ ├── bg_ib_pre.xml │ │ │ └── bg_ib_pre_round.xml │ │ ├── layout/ │ │ │ ├── activity_about.xml │ │ │ ├── activity_book_choice.xml │ │ │ ├── activity_book_cover_edit.xml │ │ │ ├── activity_book_detail.xml │ │ │ ├── activity_book_info_edit.xml │ │ │ ├── activity_book_read.xml │ │ │ ├── activity_book_source.xml │ │ │ ├── activity_chapterlist.xml │ │ │ ├── activity_donate.xml │ │ │ ├── activity_import_book.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_qrcode_capture.xml │ │ │ ├── activity_read_style.xml │ │ │ ├── activity_recycler_vew.xml │ │ │ ├── activity_search_book.xml │ │ │ ├── activity_settings.xml │ │ │ ├── activity_source_debug.xml │ │ │ ├── activity_source_edit.xml │ │ │ ├── activity_source_login.xml │ │ │ ├── activity_update.xml │ │ │ ├── activity_web_view.xml │ │ │ ├── activity_welcome.xml │ │ │ ├── content_main.xml │ │ │ ├── dialog_bookmark.xml │ │ │ ├── dialog_change_source.xml │ │ │ ├── dialog_download_choice.xml │ │ │ ├── dialog_file_chooser.xml │ │ │ ├── dialog_input.xml │ │ │ ├── dialog_login.xml │ │ │ ├── dialog_number_picker.xml │ │ │ ├── dialog_page_key.xml │ │ │ ├── dialog_replace_rule.xml │ │ │ ├── dialog_txt_chpater_rule.xml │ │ │ ├── fragment_book_find.xml │ │ │ ├── fragment_book_list.xml │ │ │ ├── fragment_bookmark_list.xml │ │ │ ├── fragment_chapter_list.xml │ │ │ ├── fragment_file_category.xml │ │ │ ├── fragment_local_book.xml │ │ │ ├── item_1line_text_and_del.xml │ │ │ ├── item_book_source.xml │ │ │ ├── item_bookshelf_grid.xml │ │ │ ├── item_bookshelf_list.xml │ │ │ ├── item_change_cover.xml │ │ │ ├── item_change_source.xml │ │ │ ├── item_chapter_list.xml │ │ │ ├── item_download.xml │ │ │ ├── item_file.xml │ │ │ ├── item_file_filepicker.xml │ │ │ ├── item_find1_group.xml │ │ │ ├── item_find1_kind.xml │ │ │ ├── item_find2_childer_view.xml │ │ │ ├── item_find2_header_view.xml │ │ │ ├── item_find_left.xml │ │ │ ├── item_font.xml │ │ │ ├── item_icon_preference.xml │ │ │ ├── item_path_filepicker.xml │ │ │ ├── item_read_bg.xml │ │ │ ├── item_replace_rule.xml │ │ │ ├── item_search_book.xml │ │ │ ├── item_search_history.xml │ │ │ ├── item_source_debug.xml │ │ │ ├── item_source_edit.xml │ │ │ ├── item_text.xml │ │ │ ├── mo_dialog_image_text.xml │ │ │ ├── mo_dialog_infor.xml │ │ │ ├── mo_dialog_loading.xml │ │ │ ├── mo_dialog_markdown.xml │ │ │ ├── mo_dialog_text_large.xml │ │ │ ├── mo_dialog_two.xml │ │ │ ├── navigation_header.xml │ │ │ ├── pop_media_player.xml │ │ │ ├── pop_more_setting.xml │ │ │ ├── pop_read_adjust.xml │ │ │ ├── pop_read_adjust_margin.xml │ │ │ ├── pop_read_interface.xml │ │ │ ├── pop_read_long_press.xml │ │ │ ├── pop_read_menu.xml │ │ │ ├── popup_keyboard_tool.xml │ │ │ ├── tab_view_icon_right.xml │ │ │ ├── view_empty.xml │ │ │ ├── view_fastscroller.xml │ │ │ ├── view_file_picker.xml │ │ │ ├── view_icon.xml │ │ │ ├── view_loading.xml │ │ │ ├── view_net_error.xml │ │ │ ├── view_night_theme.xml │ │ │ ├── view_number_buttom.xml │ │ │ ├── view_recycler_font.xml │ │ │ ├── view_refresh_error.xml │ │ │ ├── view_refresh_load_more.xml │ │ │ ├── view_refresh_no_data.xml │ │ │ └── view_refresh_recycler.xml │ │ ├── menu/ │ │ │ ├── menu_book_download.xml │ │ │ ├── menu_book_info.xml │ │ │ ├── menu_book_read_activity.xml │ │ │ ├── menu_book_search_activity.xml │ │ │ ├── menu_book_source_activity.xml │ │ │ ├── menu_book_source_edit.xml │ │ │ ├── menu_debug_activity.xml │ │ │ ├── menu_main_activity.xml │ │ │ ├── menu_main_drawer.xml │ │ │ ├── menu_qr_code_scan.xml │ │ │ ├── menu_read_style_activity.xml │ │ │ ├── menu_replace_rule_activity.xml │ │ │ ├── menu_search_view.xml │ │ │ ├── menu_source_login.xml │ │ │ ├── menu_txt_chapter_rule_activity.xml │ │ │ └── menu_update_activity.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── book_launcher.xml │ │ │ ├── book_launcher_round.xml │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── arrays.xml │ │ │ ├── attrs.xml │ │ │ ├── book_launcher_background.xml │ │ │ ├── colors.xml │ │ │ ├── colors_material_design.xml │ │ │ ├── dimens.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ids.xml │ │ │ ├── pref_key_value.xml │ │ │ ├── strings.xml │ │ │ ├── strings_me.xml │ │ │ └── styles.xml │ │ ├── values-en/ │ │ │ └── strings.xml │ │ ├── values-night/ │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── values-v27/ │ │ │ └── styles.xml │ │ ├── values-v28/ │ │ │ └── styles.xml │ │ ├── values-zh-rCN/ │ │ │ └── strings.xml │ │ ├── values-zh-rTW/ │ │ │ └── strings.xml │ │ └── xml/ │ │ ├── file_paths.xml │ │ ├── network_security_config.xml │ │ ├── pref_settings.xml │ │ ├── pref_settings_theme.xml │ │ ├── pref_settings_web_dav.xml │ │ └── shortcuts.xml │ └── test/ │ └── java/ │ └── com/ │ └── kunfei/ │ └── bookshelf/ │ └── ExampleUnitTest.java ├── basemvplib/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── kunfei/ │ │ │ └── basemvplib/ │ │ │ ├── AppActivityManager.java │ │ │ ├── BaseActivity.java │ │ │ ├── BaseFragment.java │ │ │ ├── BasePresenterImpl.java │ │ │ ├── BitIntentDataManager.java │ │ │ └── impl/ │ │ │ ├── IPresenter.java │ │ │ └── IView.java │ │ └── res/ │ │ └── values/ │ │ ├── colors.xml │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── kunfei/ │ └── basemvplib/ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── mail ├── settings.gradle └── tool/ └── 书源整理工具/ ├── BookSourceMgr.dpr ├── BookSourceMgr.dproj ├── BookSourceMgr.res ├── ReadMe.txt ├── uBookSourceBean.pas ├── uFrmEditSource.dfm ├── uFrmEditSource.pas ├── uFrmMain.dfm ├── uFrmMain.pas ├── uFrmReplaceGroup.dfm ├── uFrmReplaceGroup.pas ├── uFrmWait.dfm └── uFrmWait.pas ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml /.gradle /.idea .DS_Store /local.properties /build /captures /app/release/ google-services.json src/androidTest/ /back /tool/书源整理工具/bin/*.exe /tool/书源整理工具/*.otares ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # 本项目已转到新地址使用kotlin重新开发,项目地址 https://github.com/gedoor/legado # 本软件为开源软件,不要在任何地方购买! # 关注公众号请搜索:开源阅读,有福利噢 # 开发 - 本项目Fork于 https://github.com/ZhangQinhao/MONKOVEL **代码贡献人员** - 大古队员 https://github.com/DaguDuiyuan - atbest https://github.com/atbest - Antecer https://github.com/Antecer - mabDc https://github.com/mabDc - 繁体-翻译者:Cello琴弦之間 **其它贡献人员** - 图标绘制 群管理员-新奥尔良烤鲟魚堡 # 软件截图 ![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook1.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook2.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook3.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook4.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook5.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook6.jpg) # 免责声明(Disclaimer) 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。 各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。 任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务, 阅读对其合法性概不负责,亦不承担任何法律责任。 第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读, 不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。 您应该对使用搜索引擎的结果自行承担风险。 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求, 不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。 因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读, 阅读不承担任何法律责任。阅读尊重并保护所有使用阅读用户的个人隐私权。 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费, 通过专业搜索展示不同网站中网络文学的最新章节。 阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时, 也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。 阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商, 并建议阅读正版图书。 任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权, 应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。 阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。 ================================================ FILE: app/.gitignore ================================================ /build /src/main/java/com/kunfei/bookshelf/dao/ ================================================ FILE: app/ReadMe.md ================================================ # ע MyBookshelf_Keys ѹļС google-services.json gradle.properties key.properties.jks ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'org.greenrobot.greendao' apply plugin: 'kotlin-parcelize' apply plugin: "de.timfreiheit.resourceplaceholders" apply plugin: 'kotlin-kapt' static def releaseTime() { return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8")) } def name = "YueDu" def version = "2." + releaseTime() def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], project.rootDir).text.trim()) android { compileSdkVersion 31 signingConfigs { myConfig { storeFile file(RELEASE_STORE_FILE) storePassword RELEASE_KEY_PASSWORD keyAlias RELEASE_KEY_ALIAS keyPassword RELEASE_STORE_PASSWORD } } defaultConfig { applicationId "com.gedoor.monkeybook" minSdkVersion 21 targetSdkVersion 31 versionCode 10000 + gitCommits versionName version project.ext.set("archivesBaseName", name + "_" + version) multiDexEnabled true } buildFeatures { viewBinding true } lintOptions { abortOnError false } buildTypes { release { signingConfig signingConfigs.myConfig minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { signingConfig signingConfigs.myConfig applicationIdSuffix '.debug' versionNameSuffix 'debug' minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } android.applicationVariants.all { variant -> variant.outputs.all { outputFileName = "${name}_${defaultConfig.versionName}.apk" } } } kotlinOptions { jvmTarget = "1.8" } buildToolsVersion '30.0.3' compileOptions { // Flag to enable support for the new language APIs coreLibraryDesugaringEnabled true // Sets Java compatibility to Java 8 sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:unchecked" //options.compilerArgs << "-Xlint:deprecation" } } resourcePlaceholders { files = ['xml/shortcuts.xml'] } dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' testImplementation 'junit:junit:4.13.2' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':basemvplib') implementation('androidx.multidex:multidex:2.0.1') implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" //协程 def coroutines_version = '1.6.0' implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_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" //lifecycle def lifecycle_version = '2.4.1' implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version") //androidX implementation('androidx.appcompat:appcompat:1.4.1') implementation('androidx.core:core-ktx:1.7.0') implementation("androidx.activity:activity-ktx:1.4.0") implementation("androidx.fragment:fragment-ktx:1.4.1") implementation('androidx.preference:preference-ktx:1.2.0') implementation('androidx.constraintlayout:constraintlayout:2.1.3') implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0') implementation('androidx.viewpager2:viewpager2:1.0.0') implementation('com.google.android.material:material:1.5.0') implementation('com.google.android.flexbox:flexbox:3.0.0') implementation('com.google.code.gson:gson:2.9.0') implementation('androidx.webkit:webkit:1.4.0') //Splitties def splitties_version = '3.0.0' implementation("com.louiscad.splitties:splitties-appctx:$splitties_version") implementation("com.louiscad.splitties:splitties-systemservices:$splitties_version") implementation("com.louiscad.splitties:splitties-views:$splitties_version") //media implementation("androidx.media:media:1.5.0") def exoplayer_version = '2.17.1' implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" implementation "com.google.android.exoplayer:extension-okhttp:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-hls:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-rtsp:$exoplayer_version" //google implementation 'com.google.android.material:material:1.5.0' implementation 'com.google.code.gson:gson:2.9.0' //RxAndroid implementation 'io.reactivex.rxjava2:rxjava:2.2.19' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' //RxBus implementation 'com.hwangjr.rxbus:rxbus:2.0.1' //Retrofit //noinspection GradleDependency implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' //J_SOUP implementation 'org.jsoup:jsoup:1.14.3' implementation 'cn.wanghaomiao:JsoupXpath:2.5.1' implementation 'com.jayway.jsonpath:json-path:2.7.0' //JS //noinspection GradleDependency implementation 'com.github.gedoor:rhino-android:1.3' //GreenDao implementation 'org.greenrobot:greendao:3.3.0' implementation 'com.github.yuweiguocn:GreenDaoUpgradeHelper:v2.2.1' //Glide implementation 'com.github.bumptech.glide:glide:4.13.1' kapt 'com.github.bumptech.glide:compiler:4.13.1' //CircleImageView implementation 'de.hdodenhof:circleimageview:3.1.0' //webServer implementation 'org.nanohttpd:nanohttpd:2.3.1' implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1' //二维码 implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7' //颜色选择 implementation 'com.jaredrummler:colorpicker:1.1.0' //apache implementation('org.apache.commons:commons-text:1.9') //简繁转换 implementation 'com.luhuiguo:chinese-utils:1.0' //字符串比较 implementation 'net.ricecode:string-similarity:1.0.0' //MarkDown implementation 'ru.noties.markwon:core:3.1.0' //epub implementation('com.positiondev.epublib:epublib-core:3.1') { exclude group: 'org.slf4j' exclude group: 'xmlpull' } } greendao { schemaVersion 68 daoPackage 'com.kunfei.bookshelf.dao' targetGenDir 'src/main/java' } afterEvaluate { // for (Task task : project.tasks.matching { it.name.startsWith('crashlyticsUploadDeobs') }) { // task.enabled = false // } } ================================================ FILE: app/gradle.properties ================================================ # RELEASE_STORE_FILE = "..\\key.properties.jks" RELEASE_STORE_FILE=.\\key.properties.jks RELEASE_KEY_PASSWORD=android RELEASE_KEY_ALIAS=key0 RELEASE_STORE_PASSWORD=android ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in D:\CodeTool\Android\Android_SDK/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # # 对于一些基本指令的添加 # ############################################# # 代码混淆压缩比,在0~7之间,默认为5,一般不做修改 -optimizationpasses 5 # 混合时不使用大小写混合,混合后的类名为小写 -dontusemixedcaseclassnames # 指定不去忽略非公共库的类 -dontskipnonpubliclibraryclasses # 这句话能够使我们的项目混淆后产生映射文件 # 包含有类名->混淆后类名的映射关系 -verbose # 指定不去忽略非公共库的类成员 -dontskipnonpubliclibraryclassmembers # 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。 -dontpreverify # 保留Annotation不混淆 -keepattributes *Annotation*,InnerClasses # 避免混淆泛型 -keepattributes Signature # 抛出异常时保留代码行号 -keepattributes SourceFile,LineNumberTable # 指定混淆是采用的算法,后面的参数是一个过滤器 # 这个过滤器是谷歌推荐的算法,一般不做更改 -optimizations !code/simplification/cast,!field/*,!class/merging/* ############################################# # # Android开发中一些需要保留的公共部分 # ############################################# # 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 # 因为这些子类都有可能被外部调用 -keep public class * extends android.app.Activity -keep public class * extends android.app.Application -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.preference.Preference -keep public class * extends android.view.View -keep public class com.android.vending.licensing.ILicensingService # 保留androidx下的所有类及其内部类 -keep class androidx.** {*;} # 保留继承的 -keep public class * extends androidx.** # 保留R下面的资源 -keep class **.R$* {*;} # 保留本地native方法不被混淆 -keepclasseswithmembernames class * { native ; } # 保留在Activity中的方法参数是view的方法, # 这样以来我们在layout中写的onClick就不会被影响 -keepclassmembers class * extends android.app.Activity{ public void *(android.view.View); } # 保留枚举类不被混淆 -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } # 保留我们自定义控件(继承自View)不被混淆 -keep public class * extends android.view.View{ *** get*(); void set*(***); public (android.content.Context); public (android.content.Context, android.util.AttributeSet); public (android.content.Context, android.util.AttributeSet, int); } # 保留Parcelable序列化类不被混淆 -keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; } # 保留Serializable序列化的类不被混淆 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; !static !transient ; !private ; !private ; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } # 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆 -keepclassmembers class * { void *(**On*Event); void *(**On*Listener); } # webView处理,项目中没有使用到webView忽略即可 -keepclassmembers class fqcn.of.javascript.interface.for.webview { public *; } -keepclassmembers class * extends android.webkit.webViewClient { public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); public boolean *(android.webkit.WebView, java.lang.String); } -keepclassmembers class * extends android.webkit.webViewClient { public void *(android.webkit.webView, jav.lang.String); } # 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用 # 记得proguard-android.txt中一定不要加-dontoptimize才起作用 # 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制 -assumenosideeffects class android.util.Log { public static int v(...); public static int i(...); public static int w(...); public static int d(...); public static int e(...); } # 保持js引擎调用的java类 -keep class **.analyzeRule.**{*;} # 保持web类 -keep class **.web.**{*;} ### greenDAO 3 -keep class org.greenrobot.greendao.**{ *; } -keepclassmembers class * extends org.greenrobot.greendao.AbstractDao { public static java.lang.String TABLENAME; } -keep class **$Properties -dontwarn org.greenrobot.greendao.database.** -dontwarn rx.** -dontwarn okio.** -dontwarn retrofit2.** -dontwarn javax.annotation.** -dontwarn org.apache.log4j.lf5.viewer.** -dontnote org.apache.log4j.lf5.viewer.** -dontwarn freemarker.** -dontnote org.python.core.** -dontwarn com.hwangjr.rxbus.** -dontwarn okhttp3.** -keep class retrofit2.**{*;} -keep class okhttp3.**{*;} -keep class okio.**{*;} -keep class com.hwangjr.rxbus.**{*;} -keep class org.conscrypt.**{*;} -keep class com.kunfei.bookshelf.widget.**{*;} -keep class com.kunfei.bookshelf.bean.**{*;} -keep class android.support.**{*;} -keep class me.grantland.widget.**{*;} -keep class de.hdodenhof.circleimageview.**{*;} -keep class tyrant.explosionfield.**{*;} -keep class tyrantgit.explosionfield.**{*;} -keep class freemarker.**{*;} -keep class com.gyf.barlibrary.* {*;} ##JSOUP -keep class org.jsoup.**{*;} -keep class com.monke.mprogressbar.**{ *;} -keep class org.slf4j.**{*;} -dontwarn org.slf4j.** -keep class org.codehaus.**{*;} -dontwarn org.codehaus.** -keep class com.jayway.**{*;} -dontwarn com.jayway.** -keep class com.fasterxml.**{*;} -keep class javax.swing..**{*;} -dontwarn javax.swing.** -keep class java.awt.**{*;} -dontwarn java.awt.** -keep class sun.misc.**{*;} -dontwarn sun.misc.** -keep class sun.reflect.**{*;} -dontwarn sun.reflect.** ## Rhino -keep class javax.script.** { *; } -keep class com.sun.script.javascript.** { *; } -keep class org.mozilla.javascript.** { *; } -dontwarn org.mozilla.javascript.** -dontwarn sun.** ###EPUB -dontwarn nl.siegmann.epublib.** -dontwarn org.xmlpull.** -keep class nl.siegmann.epublib.**{*;} -keep class javax.xml.**{*;} -keep class org.xmlpull.**{*;} -keep class org.simpleframework.xml.**{*;} -dontwarn org.simpleframework.xml.** -keepclassmembers class * { public (org.json.JSONObject); } -keep public class com.kunfei.bookshelf.R$*{ public static final int *; } -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } ================================================ FILE: app/src/debug/res/values/strings.xml ================================================ 阅读.debug 阅读.debug·搜索 ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/assets/18PlusList.txt ================================================ OGN5dS5jb20= c2cwMC54eXo= aXRyYWZmaWNuZXQuY29t eGlhb3FpYW5nNTIw MTIzeGlhb3FpYW5n eGlhb3FpYW5neHM= eGlhb3FpYW5nNTIw MzM1eHM= eGN4czk= eGN4czUyMA== c2h1YmFvYW4= c2h1YmFvd2FuZzEyMw== c2h1YmFvYW4= aGFpdGFuZzEyMw== eXV6aGFpd3VsYQ== cG8xOA== Ymwtbm92ZWw= NXRucw== c2hhb3NodWdl amluamlzaHV3dQ== NDJ3Zw== eWlxdXNodQ== c2h1YmFvd2FuZzEyMw== M2hlYmFv MzNoZWJhbw== bHVvcWl1enc= bXlzaHVnZQ== c3NzeHN3 eWl0ZQ== Y3Vpd2VpanV1 Y3Vpd2VpanV4cw== Y3Vpd2VpanV4 eGlhb3FpYW5nd3g= YXN6dw== YXN6dzY= c2FuaGFveHM= ODdzaHV3dQ== NDh3eA== bG9uZ3Rlbmcy NnF3eA== bG9uZ3Rlbmd4cw== aGF4ZHU= M3R3eA== aGF4d3g1 NjZsZXdlbg== eGJhbnpodQ== aGR5cA== ZHliejk= ZGl5aWJhbnpodTk= ZGl5aWJhbnpodQ== ZGl5aWJhbnpodTc= YnoyMjI= d29kZWFwaTAwMQ== dGFuZ3poZWthbg== YmF4aWFueHM= eGlhb3NodW9zaGVuemhhbg== ZGFtb2tl emh3ZW5wZw== eXV6aGFpZ2U= d21wOA== OXhpYW53ZW4= bmFucmVudmlw cmV5b28= eWZ4aWFvc2h1b2U= c2Fuaml1enc= N3Fpbmc3 cWR4aWFvc2h1bw== Y2hpbmVzZXpq MzlzaHViYW8= a3l4czU= NTZtcw== bml1c2hh bWt4czY= MjIyMjJ4cw== OTVkdXNodQ== YmFuemh1MjI= d3JsdHh0 dHVkb3V0eHQ= cm5neHM= OTl3ZW5rdQ== bGFvc2lqaXhz ZnVzaHV6aGFpMQ== ================================================ FILE: app/src/main/assets/data/BookSourceXxl.json ================================================ { "bookSourceUrl": "https://www.kaixin7days.com", "bookSourceName": "消消乐听书", "bookSourceGroup": "听书", "bookSourceType": "AUDIO", "loginUrl": "var loginInfo = source.getLoginInfo()\nvar json = java.getResponse(\"https://www.kaixin7days.com/login@\" + loginInfo).body()\nvar loginRes = JSON.parse(json)\nvar header = null\nif (loginRes.statusCode == 200) {\n var accessToken = {\n Authorization: \"Bearer \" + loginRes.content.accessToken\n }\n header = JSON.stringify(accessToken)\n source.putLoginHeader(header)\n}\nheader", "loginUi": "[{\"name\": \"telephone\",\"type\": \"text\"},{\"name\": \"password\",\"type\": \"password\"},{\"type\": \"button\",\"name\": \"注册\", \"action\": \"http://www.yooike.com/xiaoshuo/#/register?title=%E6%B3%A8%E5%86%8C\"}]", "loginCheckJs": "var strRes = result\nvar c = JSON.parse(result.body())\nif (c.statusCode == 301) {\n var loginInfo = source.getLoginInfo()\n var dl = null\n if (loginInfo) {\n dl = java.getResponse(\"https://www.kaixin7days.com/login@\" + loginInfo).body()\n } else {\n dl = java.getResponse('https://www.kaixin7days.com/visitorLogin@{\"deviceId\":\"'+java.androidId()+'\"}').body()\n }\n c = JSON.parse(dl)\n var accessToken = {\n Authorization: \"Bearer \" + c.content.accessToken\n }\n var header = JSON.stringify(accessToken)\n source.putLoginHeader(header)\n strRes = java.getResponse(\"@Header:\" + header + url)\n}\nstrRes", "serialNumber": -100, "enable": true, "ruleFindUrl": "@js:var header = source.getLoginHeader()\nvar json = \"\"\nvar j = null\nif (header != null) {\n json = java.getResponse(\"@Header:\" + header + \"https://www.kaixin7days.com/book-service/bookMgt/getBookCategroy@{}\").body()\n j = JSON.parse(json)\n}\nif (j == null || j.statusCode != 200) {\n json = java.getResponse(\"https://www.kaixin7days.com/visitorLogin@{}\").body()\n j = JSON.parse(json)\n var accessToken = {\n Authorization: \"Bearer \" + j.content.accessToken\n }\n header = JSON.stringify(accessToken)\n source.putLoginHeader(header)\n json = java.getResponse(\"@Header:\" + header + \"https://www.kaixin7days.com/book-service/bookMgt/getBookCategroy@{}\").body()\n j = JSON.parse(json)\n}\nvar fls = j.content\nvar fx = \"\"\nfor (var i = 0; i < fls.length; i++) {\n fx = fx + fls[i].categoryName + '::/book-service/bookMgt/getAllBookByCategroyId@{\"categoryIds\": \"' + fls[i].associationCategoryIDs + '\",\"pageNum\": {{searchPage}},\"pageSize\": 100}&&'\n}\nfx", "ruleFindList": "$.content.content", "ruleFindName": "$.title", "ruleFindAuthor": "$.author", "ruleFindKind": "", "ruleFindIntroduce": "$.desc", "ruleFindLastChapter": "$.newestChapter", "ruleFindCoverUrl": "$.cover@js:var cover = JSON.parse(result);'https://www.shuidi.online:9021/fileMgt/getPicture?filePath='+cover.storeFilePath", "ruleFindNoteUrl": "$.id@js:java.put('bookId', result);'https://www.kaixin7days.com/book-service/bookMgt/getAllChapterByBookId@{\"bookId\": \"'+result+'\",\"pageNum\": 1,\"pageSize\": 100000}'", "ruleSearchUrl": "https://www.kaixin7days.com/book-service/bookMgt/findBookName@{\"title\": \"searchKey\",\"pageNum\": {{searchPage}},\"pageSize\": 100}", "ruleSearchList": "$.content.content", "ruleSearchName": "$.title", "ruleSearchAuthor": "$.author", "ruleSearchIntroduce": "$.desc", "ruleSearchLastChapter": "$.newestChapter", "ruleSearchCoverUrl": "$.cover@js:var cover = JSON.parse(result);'https://www.shuidi.online:9021/fileMgt/getPicture?filePath='+cover.storeFilePath", "ruleSearchNoteUrl": "$.id@js:java.put('bookId', result);'https://www.kaixin7days.com/book-service/bookMgt/getAllChapterByBookId@{\"bookId\": \"'+result+'\",\"pageNum\": 1,\"pageSize\": 100000}'", "ruleChapterList": "$.content.content", "ruleChapterName": "$.chapterTitle", "ruleChapterVip": "$.isFree@js:var vip = false; if (result == '0') { vip = true } vip", "ruleChapterPay": "$.isPay", "ruleContentUrl": "$.id@js:\"https://www.shuidi.online:9021/fileMgt/getAudioByChapterId?bookId=\" + java.getString(\"$.bookId\") + \"&chapterId=\" + result + \"&pageNum=1&pageSize=50&{{var header = JSON.parse(source.getLoginHeader());var reg = /&chapterId=(.*?)&/;var chapterId = reg.exec(result)[1];var keyId = '1632746188011002';var ks = java.md5Encode(keyId + chapterId + header.Authorization);'Authorization=' + header.Authorization + '&keyId=' + keyId + '&keySecret=' + ks}\" + \"}\"", "ruleBookContent": "", "payAction": "var header = JSON.parse(source.getLoginHeader())\nvar chapterUrl = chapter.getDurChapterUrl(); var reg = /&chapterId=(.*?)&/; var chapterId = reg.exec(chapterUrl)[1]\n'http://www.shuidi.online/?name='+book.getName()+'&type=2&cover=' + book.getCoverPath() + '&chapterId=' + chapterId + '&chapter=203&allNumber=' + book.getChapterListSize()+'&bookId=' + book.getVariableMap().get('bookId') + '&chapterIds=' + chapterId + '&number=' + chapter.getDurChapterIndex() + '&accessToken=' + header.Authorization.substring(7) + '#/pay'" } ================================================ FILE: app/src/main/assets/disclaimer.md ================================================ # 免责声明(Disclaimer) * 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 * 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。 各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。 任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。 第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。 您应该对使用搜索引擎的结果自行承担风险。 * 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。 因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。 阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。 * 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。 阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。 阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。 任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。 阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。 ================================================ FILE: app/src/main/assets/txtChapterRule.json ================================================ [ { "enable": true, "name": "目录", "rule": "^[  \\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", "serialNumber": 0 }, { "enable": false, "name": "目录(去空白)", "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", "serialNumber": 1 }, { "enable": false, "name": "目录(去简介)", "rule": "(?<=[ \\s])(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", "serialNumber": 2 }, { "enable": false, "name": "目录(古典、轻小说备用)", "rule": "^[  \\t]{0,4}(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", "serialNumber": 3 }, { "enable": false, "name": "数字(纯数字标题)", "rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$", "serialNumber": 4 }, { "enable": true, "name": "数字 分隔符 标题名称", "rule": "^[  \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", "serialNumber": 5 }, { "enable": true, "name": "正文 标题/序号", "rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$", "serialNumber": 6 }, { "enable": true, "name": "Chapter/Section/Part/Episode 序号 标题", "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$", "serialNumber": 7 }, { "enable": false, "name": "Chapter(去简介)", "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$", "serialNumber": 8 }, { "enable": true, "name": "特殊符号 序号 标题", "rule": "(?<=[\\s ]{0,4}).{1,3}(?:第|卷|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节]?[\\.:: \f\t].{0,20}$", "serialNumber": 9 }, { "enable": false, "name": "特殊符号 标题(成对)", "rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", "serialNumber": 10 }, { "enable":true, "name": "特殊符号 标题(单个)", "rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", "serialNumber": 11 }, { "enable": true, "name": "章/卷 序号 标题", "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", "serialNumber": 12 }, { "enable":false, "name": "顶格标题", "rule": "^\\S.{1,20}$", "serialNumber": 13 }, { "enable":false, "name": "双标题(前向)", "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", "serialNumber": 14 }, { "enable":false, "name": "双标题(后向)", "rule": "(?m)(?<=[ \\t ]{0,4}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", "serialNumber": 15 } ] ================================================ FILE: app/src/main/assets/updateLog.md ================================================ # 更新日志 * 2.0版本以停止更新,请关注公众号[开源阅读]()获取3.0版本。 **2022/04/01** * 更新到SDK31 * 更新一些库 **2021/10/24** * 字体和备份适配android 11 **2021/10/18** * 去除更新失败提醒 * 第二次换源先取消原先换源 **2021/10/17** * 修复状态栏和导航栏问题 **2021/10/01** * 使用exoPlayer支持更多音频格式 * 添加一个默认听书源 * 添加登录界面规则,登录检查规则,添加的默认书源可作为学习用 * 大量优化 **2021/07/26** * 更新一些库文件 **2021/02/16** * 详情页目录添加跳转功能 * 优化发现界面文字可能因为主题看不请的bug **2021/01/04** * 修复本地导入崩溃bug **2020/12/26** * 更新库,更新为viewBinding * 书籍信息界面添加目录按钮 **2020/12/07** * 修改风险 **2020/11/07** * 在书架查看书籍的界面,增加复制小说网址的菜单。 * 在书源列表界面,增加检查书源是否包含发现规则,并增加/删除“发现”标签的功能。 * 在阅读界面,增加长按按钮“广告”,可以快速把正文添加到命名为“广告话术-xxx网址”的规则中去。针对此规则,使用了专门的增强算法。只需要多次标记不想看的文字,就可以获得较好的广告替换效果。用户不需要熟悉正则语法,不需要维护复杂的规则。同一个网站的广告话术自动保存在一个规则内,避免了“替换净化”列表里有太多多的规则。 * 在阅读界面,优化长按按钮“替换”,自动添加书名、网址到应用范围中。 * 进一步优化自动分段算法 * 长按选择文本的高亮色块的形状由弧边改为圆角矩形 **2020/10/28** * 增加“分享书籍”功能。 * 优化阅读3.0书源转2.0的功能。 * by [tumuyan](https://github.com/tumuyan) **2020/10/24** * 增加自动重分段落的功能。部分网站错字、错误标点、错误换行极度严重。使用此功能可以一定程度上矫正引号和换行,从而改善阅读体验。 * 修复书签列表删除书签时UI不实时刷新的异常 * by [tumuyan](https://github.com/tumuyan) **2020/10/16** * 添加js方法实现重定向拦截,同3.0 **2020/10/02** * 改善添加网址功能 by [tumuyan] **2020/09/02** * 添加正文合并后替换规则 **2020/08/28** * 应用被杀死时停止朗读 **2020/08/07** * 修复一些网络问题 **2020/08/04** * 提高搜索结果的准确性,过滤掉书名+作者与搜索关键词不匹配的搜索结果。(在设置页面增加了相关选项)by [tumuyan](https://github.com/tumuyan) **2020/07/19** * 导入书源时系统文件选择器可以选择json文件 * 换源时保留是否更新 **2020/07/04** * 更新一些库文件 * 优化一些代码 **2020/06/07** * 升级到android studio 4.0 * sdk升级到29 * 升级一些库文件 **2020/05/17** * 修复jsonpath解析的bug **2020/05/01** * UA用作header,支持Map **2020/04/25** * 修复jsonpath解析的bug **2020/04/11** * 修复一些bug * 更新一些库 **2020/03/20** * 自动备份文件和手动备份文件分开 * 更新一些库文件 **2020/03/12** * 更新txt解析目录 by 52fisher * web看书加翻页 by Celeter **2020/02/26** * 更新默认txt目录规则,由https://github.com/52fisher提供 * 新版本快上架了,基本功能以完成,上架后再慢慢优化 * 别人说加广告一天能有几千收入,我不加广告大家给点力多看看公众号广告啊 **2020/02/15** * 修复低版本手机无法联网的问题 * 优化备份恢复 **2020/02/10** * 优化备份恢复 * 朗读定时会记住 **2020/02/05** * 修复bug **2020/02/04** * 修复bug * 更换默认封面,默认封面绘制书名作者 **2020/01/24** * 修复备份bug * 修复切换图标后不在后台显示的bug **2020/01/21** * 将规则解析后得到的字符串反转义(unescape) * 在正文规则使用all可查看源码,例:`tag.body@all` * 添加备份时选择备份文件夹--适配安卓10 **2020/01/09** * 修复按键码为0时翻页的bug * 去除tts中文检测判断 **2019/12/30** * 修复一些会崩溃BUG **2019/11/29** * 增加耳机上一首下一首控制 * 自定义按键增加上一页配置 * 关注公众号【小说拾遗】为你推荐好看的小说 **2019/11/16** * 自动翻页由N秒/页改为字数每分钟(CPM) * 添加自定义翻页按键 **2019/10/30** * 修复从搜索界面打开书籍,是否在上架不对的bug * 封面换源不显示已经删除的书源 **2019/10/23** * 繁体-翻译者:Cello琴弦之間 * 封面换源显示封面图片 **2019/10/15** * 添加繁体语言 * 修复换源界面编辑书源等无法保存的bug * 其它一些优化 **2019/10/05** * 搜索时显示书架里的书 **2019/09/25** * 网络导入书源的记录添加了删除按钮 **2019/09/24** * 网络导入书源可以记住多个导入网址,方便书源更新 **2019/09/14** * 阅读界面设置添加微调 * 选择图片在有些手机上可能会崩溃的bug **2019/09/09** * 网格书架增加3列4列的选择 * 正文规则为空时内容为章节url * merged commit cac2689, 更新阅读设置界面,增加一些自带背景 **2019/08/29** * 修复导入本地不显示文件的bug * 优化书架网格界面 **2019/08/22** * 给扩展到刘海屏加了个开关 **2019/08/21** * 修复一些bug * 口袋阅书架可以显示了 * 刘海屏阅读界面隐藏状态栏不显示黑条 **2019/08/15** * 修复拷贝书源问题 **2019/08/13** * 在退出软件之前会记住搜索分组 **2019/08/12** * 书源管理分组添加显示勾选 * 详情页点击书名可搜索 * 优化web写源加载速度 * 修复text没了换行的bug * 修复阅读界面点击章节名称可编辑书源换源不变的bug **2019/08/10** * 修复正则写的不对会崩溃的BUG * 修复正则书源会返回null书籍的BUG * 阅读界面点击章节名称可编辑书源 * 添加替换净化默认启用关闭的配置 **2019/07/30** * 修复更新详情页不保存的bug * 高级功能改为一个月点一次 * 修复web写源会丢音频标志的bug **2019/07/25** * 修复一些bug **2019/07/20** * 阅读界面菜单添加是否启用替换净化,默认禁用 * Android O以上系统支持正则命名分组 * 其它一些优化 **2019/07/15** * 优化搜索列表正则AllInOne效率; 增强目录列表正则AllInOne兼容性 by Antecer * web写源添加一个web书架入口 * 调整书源管理菜单 **2019/07/13** * 修复BUG * 修复因加入长按复制导致有声书源崩溃的问题 **2019/07/8** * 参考搜神添加了长按选择,需主动开启 **2019/07/5** * 发现添加单个清除缓存 * 尝试修复换源界面禁用错位的问题 **2019/07/4** * 换源不再会改变书名和作者,防止换到不好的源之后不能再换源 * 规则添加了 ##替换内容##替换为 * 修复了详情页init规则的报错 **2019/07/3** * 修复一个会导致崩溃的问题 * 修复----在发现或搜索里,点击书籍进入详情页,然后点击换源,选择要换的源,第二次再点击换源,仍然选择之前的那个源,这时候加入书架会变成删除书籍,实际上并没有加入到书架中的问题 **2019/07/1** * 修复搜索直达详情页的正则处理 **2019/06/30** * 修复搜索直达详情页的正则处理 * 修复一个会导致崩溃的问题 **2019/06/26** * 修复书源全选会导致排序混乱的问题 * 修复提示缓存被删除的问题 **2019/06/23** * 书源规则id节点可以获取多个 * 修复换源后首次不能下载的BUG * 修复章节可能空白的问题 * 修复没有加入书架不能下载的问题 **2019/06/16** * 修复文件选择器,路径不能朗读的bug **2019/06/14** * 加入kotlin库,安装包又大了一些,后续一些优化会使用kotlin来写 * 重写权限获取,修复没有存储权限时不提示的问题 **2019/06/10** * 修复书架文字显示不全的问题 * 其它一些优化 **2019/06/08** * 修复换源界面宽度显示不对的BUG **2019/06/06** * 修复对话框内的一些问题 **2019/06/01** * js库升级到Rhino 1.7.11 **2019/05/31** * 侧边栏背景跟随主题背景 * 其它一些优化 **2019/05/22** * 完成整理书架 * 修复一个阅读页面空白的bug **2019/05/20** * 修复一键缓存崩溃的问题 * 将切换图标移到主题设置里 * 将清空缓存移到设置里 * 解决校验书源和搜索卡死的问题 **2019/05/19** * 这是一个比较稳定的版本,没什么大的BUG * 翻页BUG据反馈已经没有了 **2019/05/18** * 尝试修复一些翻页问题 * 搜索界面添加简介显示 **2019/05/16** * 修复部分手机书源编辑界面无法编辑的问题 * 尝试修复可能会出现的翻不了页问题 **2019/05/15** * 搜索页和详情页为同一网址时不再重复获取网页 * 详情页和目录页为同一网址时不再重复获取网页 * 优化翻页动画,尝试解决一些手机翻页不变的问题 * 书源规则增加一些字段,发现规则独立出来 * 重写书源编辑界面 * 其它一些优化,修复一些bug **2019/05/13** * 并行解析多页目录列表,提高解析速度 * 增加js方法,java.put(key, value) java.get(key) * 搜索增加按书源分组 * 章节绝对url放到访问时再组合,提高解析速度 * 音频播放结束自动下一章 **2019/05/12** * 修复bug **2019/05/11** * 添加有声阅读功能,正文内容返回mp3地址可播放 * 优化解析逻辑,大幅提高解析速度,书源有一些新规则后续会放出说明 * 感谢大佬mabDc提出的优化方案 * 修复bug **2019/05/06** * 电量显示放到电池图标内 * url添加()作为保留字符,不编码 * 修改目录加载失败时的报错 **2019/05/05** * 正文规则$开头使用webView加载网页时js规则会在webView内执行,js会每秒执行一次直到返回值不为空 **2019/05/04** * 添加txt目录正则管理 * 其它一些优化 **2019/04/30** * 添加书架的web接口 **2019/04/29** * 应该解决部分手机翻页时停在中间不执行动画的问题 * 更新build gradle 和一些库文件 * 解决时间可能不刷新的问题 **2019/04/26** * 优化翻页动画 * 其它一些bug修复 **2019/04/23** * 修复一个备份恢复bug * 修改发现界面,解决部分机型卡顿的问题 **2019/04/22** * web写源功能全部完成 * 修复一个搜索时可能崩溃的bug **2019/04/20** * web写书源功能基本完成,主界面菜单开启,感谢Antecer mabDc 的贡献 * 修复网址加书的一个bug * js发现添加缓存 * wifi分享书源完成,接收书源使用网络导入输入分享IP **2019/04/18** * 领红包开启高级功能保持7天 **2019/04/17** * Modificator commented 侧边栏-主题,添加 E-Ink 模式 * 减少调试日志打印量,目录正文下一页不打印日志 **2019/04/16** * 重写书源调试,调试更方便直观 * 书源调试可以输入url,添加扫描二维码按钮 * 修复网格视图下拖动排序和切换发现页冲突的bug * 效验书源改为用搜索和发现效验,搜索使用关键词"我的" * 效验书源搜索和发现都不可用时才标识失效并禁用 **2019/04/15** * xpath添加&&,||规则 * 修复部分手机动画问题 **2019/04/14** * 添加{{js}}规则写法会替换为js执行结果 * 修复手动排序置顶失效的问题 **2019/04/12** * 修复设置夜间模式会多次Recreate的问题 * WebDav恢复时添加存储权限检查 * 书源调试添加时间戳 * 增加自定义js方法 **2019/04/10** * 发现规则支持js生成规则文本, \\ **2019/04/09** * 阅读时如果原书源被删除会自动换源 * 修改JSoup解析文件,修复css规则解析 by mabDc * 其它一些优化 **2019/04/08** * 优化内存使用 * 优化替换净化速度 **2019/04/01** * 修复耳机键不能唤起阅读的BUG * 搜索和发现可以不写baseUrl * 添加字间距调整 **2019/03/30** * 仿真翻页背面加回 * 修复在有些手机上切换夜间模式,书架不跟着切换的bug **2019/03/29** * 添加Tip边距调节 * 更新fireBase版本 **2019/03/26** * 添加RxJavaPlugins.setErrorHandler(Functions.emptyConsumer());防止rxJava内部错误导致崩溃 * 去除打开软件时的权限获取提示 * fix SourceDebug variable missing bug by mabDc was merged a day ago **2019/03/21** * 同时解决了背景色差和翻页问题 **2019/03/20** * 修复了背景色差的问题 **2019/03/19** * url编码之前判断是否已经编码 * 添加缩进设置 * 修改默认下载文件夹为Files,原cache文件夹会被清理软件清理 * 修复bug **2019/03/16** * 搜索URL支持JS语法 * 字符串基本上都写到values\strings,方便制作多语言文件 * 修复离线下载只能输入4位数的问题 * 修复一些可能导致崩溃的BUG * 修复上版本判断url正则错误的bug * 修复一些WebDav不显示文件名的bug **2019/03/14** * 修复bug **2019/03/13** * 修复@header{}写到前面会导致结果出现BaseUrl + url的bug * webView添加25秒超时,防止一直加载 * 修复trim()去不掉全角空格引起的导入书源失败问题 **2019/03/12** * 修复bug * 优化动态网站的内容获取,可以更好地判断网站是否加载完成 **2019/03/10** * 修复bug **2019/03/08** * 添加\\规则,可以写在前面或后面 **2019/03/06** * 软件崩溃时会复制错误报告到剪贴板 **2019/03/05** * 优化数字选择器 * 边距调整上限改为100 * 其它一些细节优化 **2019/03/02** * 修复几个崩溃BUG * 减小防误触区域 **2019/03/01** * 添加一个书架ToolBar自动隐藏开关 * 目录去除倒序按钮,添加滚到底部和滚到顶部按钮 **2019/02/28** * 合并Antecer提交的代码 * 优化Table表格结构标签元素的选择结果,补全Table结构,否则会因为html标准丢失独立于table结构以外的tr和td标签. * 删除XPath匹配规则的 "," 逗号过滤代码,因为JsoupXpath库已经修复了这个BUG. **2019/02/27** * 修复一些本地txt问题 * 修复使用阅读打开本地TXT,EPUB显示空白的问题 * 更新JsoupXpath库,修复部分解析规则选择不到元素的BUG **2019/02/26** * 修复一个EPUB乱码的BUG **2019/02/24** * 合并Antecer和tumuyan提交的代码 * 改善导入功能: 1.由添加单个网址改善为批量添加网址(使用换行符分割) 2.如果从其他软件分享链接到阅读,阅读自动导入;如果不包含网址,使用原有的搜索。 * 修复Xpath规则匹配章节目录时候,匹配结果没有子元素时,获取目录列表失败的BUG。 **2019/02/20** * 修复一些WebDav的兼容问题 **2019/02/18** * WebDav备份与恢复 **2019/02/15** * 更新XPath库,修复XPath排序混乱的问题 * 其它一些优化 **2019/02/12** * 换源界面添加搜索框 **2019/02/10** * 尝试修复上版本部分手机闪退的问题 **2019/02/09** * 添加setCookie支持 * 合并Antecer提交的代码,解决XPath规则匹配的BUG,xpath可以不写@xpath:开头 **2019/01/29** * 添加文字操作里搜索菜单的设置 * AJAX动态网站延时两秒获取网站代码 * 年前最后一次更新,过年休息暂停更新 **2019/01/21** * JsonPath获取一个列表转成字符会自动添加换行符 **2019/01/20** * 修复一个编码识别BUG **2019/01/19** * 隐藏侧边栏滚动条 * 继续优化本地文件编码识别 * 修复朗读时加载错误一直翻页的BUG **2019/01/18** * 更换了侧边栏图片,不知道哪个大神制作的 * 更换一个编码识别库,优化本地文件编码识别 **2019/01/16** * 升级到Android Studio 3.3 * js添加两个方法,java.ajax(String) java.base64Decoder(String) **2019/01/14** * 修复屏幕常亮无效的BUG **2019/01/12** * 分类规则支持多个结果用&&连接多个规则,可添加字数的规则 **2019/01/11** * 完善主题 * 修复@get:{key}获取不到值的BUG * 由于2群被封,修改了入群信息 **2019/01/10** * 完善主题,改不了颜色的暂时都变成了黑白色 * 添加了恢复默认主题 **2019/01/08** * 支持@Header:{key:value, key:value}定义http头 * 支持@put:{key:rule, key:rule}存储变量 * 支持@get:{key}获取变量 * 所有自定义变量类型必须为String * 修复部分书源无法使用的问题 **2019/01/07** * 继续完善主题 * 基本上所有地址都支持搜索地址的规则了,可以写POST **2019/01/06** * 继续完善主题,添加自定义背景色 **2019/01/05** * 继续完善主题,修复了阅读界面换源不显示的BUG **2019/01/04** * 主题功能实现切换ToolBar颜色 **2019/01/01** * 祝大家元旦快乐 * 修复不显示再按一次退出程序的BUG * 支持不写规则用js直接处理网页内容 **2018/12/30** * 目录和书签可以滑动切换 * 修复亮度调节的问题 **2018/12/29** * 更换颜色选择器,可以直接输入颜色值 * 细节优化 **2018/12/28** * 修复菜单图标消失的bug **2018/12/27** * 项目迁移到androidX * 修复一些bug **2018/12/26** * 发现页长按书源名称增加弹出菜单,编辑 置顶 删除 * json书源规则添加|| && %%分隔符,和原书源规则一样,如@JSon:$.minorCate||$.cat * 其它一些优化 **2018/12/24** * 发现页支持样式切换,新样式卡顿可切换老样式 * 修复4.4报错的问题 **2018/12/23** * 细节优化 **2018/12/21** * 发现页面改版 **2018/12/19** * jsonPath获取字符支持此种写法xxx{$._id}yyy{$.chapter}zzz **2018/12/18** * 书源编辑界面添加常用工具条,可快速输入常用字符 **2018/12/17** * 书源导入添加从二维码导入,可以是书源和网址,网址和从网络导入一个效果 * 阅读界面的菜单可以直接领红包,高级功能失效时显示. * 修复删除粒子动画 * 修复一些BUG **2018/12/15** * 添加书源调试,检查书源更方便 **2018/12/14** * 修复优化背景图读取引起滚动背景不对的bug **2018/12/13** * 优化背景图读取,防止oom * 编辑书源界面改成中文 **2018/12/12** * 添加登陆功能,可以阅读一些需要登陆的网站,需要配置一下书源的登陆地址,可以在书源菜单里登陆,也可以在阅读菜单里登陆 * 将当前网址传到了js里,变量名为baseUrl **2018/12/11** * 合并**refgd**提交的代码,优化规则解析,使用LinkedHashSet去重提升速度 * 一些BUG修复 **2018/12/10** * 修复一些书源不对会引起崩溃的bug * 添加一个进入书架的快捷方式,防止直接进入最后阅读反复崩溃的问题 * 减小安装包体积 * 感谢**沚水**给书源规则网站做了美化 **2018/12/09** * 修复一些BUG * 需要js处理时再加载js引擎,解决了刷新速度变慢的问题 * 给目录添加了去重处理,保留后面,比如有些源,最前面有最新章节会和后面重复,导致bug **2018/12/08** * 支持XPath语法,以@XPath:开头,语法见 http://www.w3school.com.cn/xpath/index.asp * 支持JSonPath语法,以@JSon:开头,语法见 https://blog.csdn.net/koflance/article/details/63262484 * 支持用js处理结果,以@js:开头,结果变量为result 如 "@JSon:$.link@js:"http://chapterup.zhuishushenqi.com/chapter/" + encodeURIComponent(result)" * **注意** #替换规则在新语法下无法使用,新的语法用js处理结果,原有的规则不变 * 去掉了隐藏书源,隐藏书源已经可以用JSonPath语法写出来 * 新增的语法作为高级功能需要领红包才能开启 **2018/12/07** * 修复一下朗读时手动翻页可能会翻两页的情况,概率没那么大了 **2018/12/05** * 修复简介显示不全又不能滚动的bug * 合并M17764017422提交的代码,TTS初始化失败自动打开TTS设置 **2018/12/04** * 为防止侵权,去掉了默认书源,需要书源请自行想办法 **2018/12/03** * 修复本地txt文件丢失第一章节的BUG **2018/12/02** * 正在朗读的文字颜色变化 **2018/12/01** * 下载通知合并为一个 * 其它一些bug修复 **2018/11/30** * 修复书籍详情页面在横屏显示不全时简介不能滚动的问题 * 修复其它一些BUG * 合并atbest提交的代码 * tag和class的位置可以使用负数,表示倒数 * 添加正则表达式替换:split("#") 第3项 * lastRule添加ownText:只显示Element自己的文字,不显示child的文字 * 添加Element过滤规则:用">"分隔,只选择含有特定class、id、tag或者text的Element * 判断搜索结果是否重定向到书籍页面,如果是的话就书籍规则获取内容 **2018/11/29** * 修复一些BUG * 书籍详情页面在横屏显示不全时可滚动 * 修复一个length=0;index=-1的报错 **2018/11/24** * 修复一个背景问题导致的崩溃bug * %不再代表列表的最后一个,列表排除后面的用负值表示,-1表示倒数第一个,-2表示倒数第2个 * 用%分隔列表规则,这些列表规则将会依次取数,如三个列表,先取列表1的第一个,再取列表2的第一个,再取列表3的第一个,再取列表1的第2个 **2018/11/23** * 修复一些BUG * 发现页长按分组可编辑书源 * 书籍详情菜单添加编辑书源 * 搜索规则|char=escape 会模拟js escape方法进行编码 **2018/11/21** * 按章节朗读,解决分页朗读时翻页处朗读不畅的问题 * 语音引擎如果支持朗读进度播报,则能及时翻页,如不支持会等朗读完一段再判断是否翻页 **2018/11/20** * 修复因添加规则引起的一些问题 * 支持内容分页全是下一章的内容获取 **2018/11/19** * 下一章节URL支持同时获取多个,如果有多个会依次获取,如果只有一个同以前 * 搜索记录最多显示50条,可滚动 * 书籍详情页面添加菜单按钮,不常用功能收集到菜单 **2018/11/18** * 修复一个closed的bug * 书架不再显示进度,改为显示未读章节数,红色有更新,灰色无更新 **2018/11/17** * 优化界面 * 合并 atbest 的代码,书架界面修改 * 合并 Invinciblelee 的代码,下载 **2018/11/15** * 书源管理界面搜索的字调小了一些,可以显示全书源数量 * 更换一些图标,感谢**爱发猫表情的新奥尔良烤鲟鱼堡** * 修复EPUB内容与章节不对应的问题 * 修复一些BUG **2018/11/14** * 书架添加全部书籍分类 * 更换一些图标,由**爱发猫表情的新奥尔良烤鲟鱼堡**制作 * 添加了一个导航栏变色开关,有些手机不好适配,麻烦 * 关于里添加了一个分享软件的按钮,可以二维码分享 * 合并 atbest 代码,添加EPUB封面 **2018/11/13** * 修复换源搜索所有源的问题 * 换源下拉刷新还是会重新搜索 * 合并 atbest 代码,修复拼音排序 * 解决一些导致崩溃的BUG * 修复换源里搜索不到的问题 **2018/11/12** * 合并 atbest 代码 * 全新的滚动模式 * 独立本地书架 * 优化朗读时淡入淡出 * Epub滚动模式显示封面 * 修复Epub乱码问题 * 支持使用高清背景图片 * 其他 **2018/11/11** * 换源时刷新不会删除已经搜索到的源,只会更新并搜索没有搜索到的书源 * 换源添加长按菜单 * 修复亮度问题 **2018/11/10** * 一些优化,和bug修复 * 书源添加排序选项,可选择手动排序,智能排序,拼音排序 * 目录拷贝 Invinciblelee 的代码,修改了界面,添加倒序 * 版本号改用日期,首段大版本号,中间年份,尾段日期,精确到小时 * 封面全采用圆角,调整颜色 **2018/11/7** * 添加后台自动更新换源里的最新章节 **2018/11/6** * 修复音量键翻页没有动画的bug * 修复缓存路径自动变为默认的bug **2018/11/5** * 本地书籍加入编码设置,如遇乱码可自行设置编码 * 优化EPUB目录读取,可以读取多级目录了 * 解决本地TXT章节丢失的问题 * 其它一些优化 **2018/11/4** * 修复设置有时弹不出来的bug * 修改TXT目录正则后自动刷新目录 **2018/11/3** * 解决一些BUG * 加入自定义TXT目录正则 * 拷贝https://github.com/Invinciblelee的代码增加EPUB文件支持 * 优化阅读设置,解决隐藏虚拟操作栏下面显示空白的问题 **2018/11/2** * 解决一些BUG * 如何查看书籍详情? * 按阅读时间或更新时间排序的,长按书籍;手动排序的,列表视图单击或长按书籍封面,网格视图单击书籍名称。 * 阅读界面菜单添加更新目录 **2018/11/1** * 解决替换影响速度的问题 **2018/10/31** * 解析章节放到后台,进一步提升翻页流畅度 * 合并 https://github.com/atbest 提交的代码 * 净化替换规则可选是否为正则表达式 * 书籍详情界面显示剩余章节数 * 恢复备份以后不重复显示更新日志 * 朗读时淡入淡出 * 仿真翻页背景虚化 * 其他Bug修复及优化 **2018/10/26** * 优化翻页,更流畅 **2018/10/23** * 合并 https://github.com/atbest 提交的代码, 修复恢复设置不能恢复阅读背景之类的 * 添加记住书架分组,重新打开时自动进入退出时的分组 * 添加显示所有发现的设置 * 优化书籍详情,采用自适应布局 * 其它一些优化 **2018/10/20** * 修复点击返回按钮闪退的BUG * 禁用预解析下一章内容,看还有没有翻页时窜章的,如果有请反馈,我好确认问题所在 * 修复从发现搜索时不直接显示搜索结果的问题 * 添加滚动时也可以点击翻页 * 修复目录已下载加粗关闭重开会消失的问题 * 其它一些优化 **2018/10/19** * 升级SDK * 修复升级SDK引起的一些问题 * 优化分区切换 * 关于里添加常见问题 * 添加自动备份 * 解决发现滚动不触发动画滚动不到底的问题 * 合并代码,其它一些优化 **2018/10/17** * 优化书架分区切换 * 合并代码,其它一些优化 **2018/10/16** * 合并 大古队员 的代码 * 界面改版 **2018/10/15** * 合并 https://github.com/atbest 提交的代码 * 缓存文件添加目录,修改缓存文件名,方便需要合并文件的人 * 添加禁止更新功能 **2018/10/14** * 合并 https://github.com/atbest 提交的代码 * 添加tip是否跟随边距调整的开关 * 快速滚动条修改 * 修复目录加粗可能在部分机型上不起作用的BUG * 复制了一些 https://github.com/Invinciblelee 的代码 **2018/10/13** * 合并 https://github.com/atbest 提交的代码,修复一些BUG,滚动模式背景不随内容滚动 * 稍微修改一下界面 * 领支付宝红包彩蛋修改为持续3天 **2018/10/10** * 修复可能引起的跳章的问题 * 电量添加百分比 **2018/10/08** * 修复音量翻页开关 * 合并 https://github.com/atbest 提交的代码 **2018/10/03** * 添加朗读时音量翻页开关 * 关闭滚动模式的惯性滚动 * 更新前先删除原有目录,看能不能解决双目录问题 **2018/10/02** * 添加更新检查,可直接下载最新版本 * 朗读时自动关闭音量翻页 * 修复一些BUG **2018/10/01** * 添加一个自动翻页倒计时条 * 本地导入书源可以选择.json文件 * 修复一些BUG **2018/09/29** * 修复一些BUG **2018/09/26** * 为当前使用的字体添加标志 * 阅读界面菜单添加禁用书源 * 添加自定义缓存路径 * 其它一些优化 **2018/09/25** * 换源时智能选择对应的章节 * 其它一些优化和BUG修复 * 感谢 https://github.com/atbest 提交的代码 **2018/09/23** * 修复下载时线程过多导致的崩溃 * 修复无网络一直加载的问题 **2018/09/21** * 修复一些BUG * 书源管理添加选择分组功能 **2018/09/20** * 优化html取值 **2018/09/19** * 修复一些BUG * 取值规则添加&分隔符,合并所有取值 * 取值规则添加children,获取所有子标签 * 取值规则添加html,获取所有文本 **2018/09/17** * 解决更换字体时文字有可能不对齐的问题 **2018/09/14** * 添加内容分页获取规则 **2018/09/13** * 内容可以用TalkBack朗读 * 修复一个BUG,String input must not be null **2018/09/12** * 替换添加作用于,可填写书名和书源url,不填应用到所有 **2018/09/10** * 修复搜索时卡顿 * 添加直接打开最近阅读设置 **2018/09/07** * 添加本地时点路径可以选择SD卡 **2018/09/06** * 点击作者会搜索作者 * 修复切换手动排序长按封面可能不出书籍信息的BUG * 其它一些优化 **2018/09/04** * 添加自定义封面 **2018/08/31** * 添加自动翻页功能 **2018/08/29** * 尝试修复选择图片背景闪退问题 * 尝试修复搜索闪退问题 **2018/08/27** * 去除了获取内容时的空格替换 * 搜索添加停止按钮 * 修复一些bug **2018/08/23** * 修复了本地书籍双目录的bug * 其它一些修复 **2018/08/20** * 添加段距调整 * 修复bug * 无动画时手指拿起才会翻页 **2018/08/14** * 背景图不再放到缓存,背景图经过处理减少卡顿 * 备份可以备份设置,备份文件夹改变为YueDu **2018/08/14** * 更改了选择字体的方式 * 修复一些BUG **2018/08/13** * 优化启动速度,启动时不读取详细目录 * 修复一些BUG **2018/08/09** * 优化修复bug **2018/08/09** * 修改书架界面 * 修改本地导入界面 * 修复BUG **2018/08/08** * 修复BUG,本地书籍乱码,点击翻页选项无效等问题 **2018/08/06** * 重写阅读界面代码,实现多种翻页模式 ================================================ FILE: app/src/main/assets/web/bookshelf.css ================================================ html, body { height: 100%; margin: 0; } .hide { display: none; } .top, .showchapter, .hidebooks { width: 60px; height: 50px; position: absolute; right: 30px; bottom: 30px; color: black; font-size: 28px; background-color: #ddd; opacity: 0.85; } .top { bottom: 150px; } .showchapter { bottom: 90px; bottom: 90px; } .address { width: 270px; } .nav { border-bottom: solid 1px #ccc; } input, button { width: 110px; line-height: 34px; background-color: #eee; color: #555; border: none; margin: 10px 5px; font-weight: 500; border-radius: 2px; outline: none; cursor: pointer; } input { padding: 0 10px; cursor: text; } input:hover, button:hover { border-color: #aaa; background-color: #efefef; color: #222; outline: solid 1px #ccc; } .allcontent { height: calc(100% - 60px); } .allscreen { height: 100% } .books > div { display: inline-block; margin: 10px; vertical-align: top; border: solid 1px #ddd; } .read > .books { width: 420px; float: left; height: 100%; overflow: auto; border-right: solid 1px #ccc; } .read > .books > div { margin-right: 0; border-right: none; } .more { overflow-y: auto; height: 100%; display: none; } .read .more { display: block; } .books > div > img { width: 120px; height: 180px; float: left; margin-right: 10px; cursor: pointer; } .info { padding: 10px 20px 0 20px; width: 600px; margin: 0 auto; } .info > img { width: 600px; height: 900px; } .info p { line-height: 1.5; text-align: justify; margin: 0; } .books tr:nth-child(n+2) td { border-top: solid 1px #999; } .books td:nth-child(1) { vertical-align: top; width: 50px; } .books td:nth-child(2) { vertical-align: top; width: 200px; } .clear { clear: both; } .chapter { margin: 10px; max-height: 500px; overflow-y: auto; border-top: solid 1px #333; border-bottom: solid 1px #333; } .chapter button { width: 230px; text-align: left; text-indent: 14px; margin: 10px 4px; } .content { padding: 20px; text-align: justify; min-height: 1000px; padding-bottom: 200px; } .content h2 { font-family: "Microsoft YaHei",微软雅黑,"MicrosoftJhengHei",华文细黑,STHeiti,MingLiu; font-weight: 500; text-align: center; line-height: 100px; font-size: 40px; margin: 0; } ================================================ FILE: app/src/main/assets/web/bookshelf.html ================================================  阅读书架
================================================ FILE: app/src/main/assets/web/bookshelf.js ================================================ var $ = document.querySelector.bind(document) , $$ = document.querySelectorAll.bind(document) , $c = document.createElement.bind(document) , randomImg = "http://api.mtyqx.cn/api/random.php" , randomImg2 = "http://img.xjh.me/random_img.php" , books ; var now_chapter = -1; var sum_chapter = 0; var formatTime = value => { return new Date(value).toLocaleString('zh-CN', { hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }).replace(/\//g, "-"); }; var apiMap = { "getBookshelf": "/getBookshelf", "getChapterList": "/getChapterList", "getBookContent": "/getBookContent", "saveBook": "/saveBook" }; var apiAddress = (apiName, url) => { let address = $('#address').value || window.location.host; if (!(/^http|^\/\//).test(address)) { address = "//" + address; } if (!(/:\d{4,}/).test(address.split("//")[1].split("/")[0])) { address += ":1122"; } localStorage.setItem('address', address); return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : ""); }; var init = () => { $('#allcontent').classList.remove("read"); $('#books').innerHTML = ""; fetch(apiAddress("getBookshelf"), { mode: "cors" }) .then(res => res.json()) .then(data => { if (!data.isSuccess) { alert(getBookshelf.errorMsg); return; } books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber); books.forEach((book, i) => { let bookDiv = $c("div"); let img = $c("img"); img.src = book.bookInfoBean.coverUrl || randomImg; img.setAttribute("data-series-num", i); bookDiv.appendChild(img); bookDiv.innerHTML += `
书名:${book.bookInfoBean.name}
作者:${book.bookInfoBean.author}
阅读:${book.durChapterName}
${formatTime(book.finalDate)}
更新:${book.lastChapterName}
${formatTime(book.finalRefreshData)}
来源:${book.bookInfoBean.origin}
`; $('#books').appendChild(bookDiv); }); $$('#books img').forEach(bookImg => bookImg.addEventListener("click", () => { now_chapter = -1; sum_chapter = 0; $('#allcontent').classList.add("read"); var book = books[bookImg.getAttribute("data-series-num")]; $("#info").innerHTML = `

  来源:${book.bookInfoBean.origin}

  书名:${book.bookInfoBean.name}

  作者:${book.bookInfoBean.author}

阅读章节:${book.durChapterName}

阅读时间:${formatTime(book.finalDate)}

最新章节:${book.lastChapterName}

检查时间:${formatTime(book.finalRefreshData)}

  简介:${book.bookInfoBean.introduce.trim().replace(/\n/g, "
")}

`; window.location.hash = ""; window.location.hash = "#info"; $("#content").innerHTML = "章节列表加载中..."; $("#chapter").innerHTML = ""; fetch(apiAddress("getChapterList", book.noteUrl), { mode: "cors" }) .then(res => res.json()) .then(data => { if (!data.isSuccess) { alert(data.errorMsg); $("#content").innerHTML = "章节列表加载失败!"; return; } data.data.forEach(chapter => { let ch = $c("button"); ch.setAttribute("data-url", chapter.durChapterUrl); ch.setAttribute("data-index", chapter.durChapterIndex); ch.setAttribute("title", chapter.durChapterName); ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + "..." : chapter.durChapterName; $("#chapter").appendChild(ch); }); sum_chapter = data.data.length; $('#chapter').scrollTop = 0; $("#content").innerHTML = "章节列表加载完成!"; }); })); }); }; $("#back").addEventListener("click", () => { if (window.location.hash === "#content") { window.location.hash = "#chapter"; } else if (window.location.hash === "#chapter") { window.location.hash = "#info"; } else { $('#allcontent').classList.remove("read"); } }); $("#refresh").addEventListener("click", init); $('#hidebooks').addEventListener("click", () => { $("#books").classList.toggle("hide"); $(".nav").classList.toggle("hide"); $("#allcontent").classList.toggle("allscreen"); }); $('#top').addEventListener("click", () => { window.location.hash = ""; window.location.hash = "#info"; }); $('#showchapter').addEventListener("click", () => { window.location.hash = ""; window.location.hash = "#chapter"; }); $('#up').addEventListener('click', e => { if (now_chapter > 0) { now_chapter--; let clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent("click", true, false); $('[data-index="' + now_chapter + '"]').dispatchEvent(clickEvent); } else if (now_chapter == 0) { alert("已经是第一章了^_^!") } else { } }); $('#down').addEventListener('click', e => { if (now_chapter > -1) { if (now_chapter < sum_chapter - 1) { now_chapter++; let clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent("click", true, false); $('[data-index="' + now_chapter + '"]').dispatchEvent(clickEvent); } else { alert("已经是最后一章了^_^!") } } }); $('#chapter').addEventListener("click", (e) => { if (e.target.tagName === "BUTTON") { var url = e.target.getAttribute("data-url"); var index = e.target.getAttribute("data-index"); var name = e.target.getAttribute("title"); if (!url) { alert("未取得章节地址"); } now_chapter = parseInt(index); $("#content").innerHTML = "

" + name + " 加载中...

"; fetch(apiAddress("getBookContent", url), { mode: "cors" }) .then(res => res.json()) .then(data => { if (!data.isSuccess) { alert(data.errorMsg); $("#content").innerHTML = "

" + name + " 加载失败!

"; return; } var content = data.data.trim().split("\n\n"); if (content.length === 2) { $("#content").innerHTML = `

${content[0]}

  (全文 ${content[1].length} 字)

  ` + content[1].trim().replace(/\n/g, "

"); } else { $("#content").innerHTML = `

${name || e.target.innerHTML}

  (全文 ${data.data.length} 字)

  ` + data.data.trim().replace(/\n/g, "

"); } window.location.hash = ""; window.location.hash = "#content"; }); } }); $('#address').setAttribute("placeholder", "阅读APP地址或IP:" + window.location.host); if (!$('#address').value && typeof localStorage && localStorage.getItem('address')) { $('#address').value = localStorage.getItem('address'); } init(); ================================================ FILE: app/src/main/assets/web/index.css ================================================ body { margin: 0; } .editor { display: flex; align-items: stretch; } .setbox, .menu, .outbox { flex: 1; display: flex; flex-flow: column; max-height: 100vh; overflow-y: auto; } .menu { justify-content: center; max-width: 90px; margin: 0 5px; } .menu .button { width: 90px; height: 30px; min-height: 30px; margin: 5px 0px; cursor: pointer; } @keyframes stroker { 0% { stroke-dashoffset: 0 } 100% { stroke-dashoffset: -240 } } .button rect { width: 100%; height: 100%; fill: transparent; stroke: #666; stroke-width: 2px; } .button rect.busy { stroke: #fD1850; stroke-dasharray: 30 90; animation: stroker 1s linear infinite; } .button text { text-anchor: middle; dominant-baseline: middle; } .setbox { min-width: 40em; } .rules, .tabbox { flex: 1; display: flex; flex-flow: column; } .rules>* { display: flex; margin: 2px 0; } .rules textarea { flex: 1; margin-left: 5px; } .rules>*, .rules>*>div, .rules textarea { min-height: 1em; } textarea { word-break: break-all; } .tabtitle { display: flex; z-index: 1; justify-content: flex-end; } .tabtitle>div { cursor: pointer; padding: 1px 10px 0 10px; border-bottom: 3px solid transparent; font-weight: bold; } .tabtitle>.this { color: #4f9da6; border-bottom-color: #4EBBE4; } .tabbody { flex: 1; display: flex; margin-top: -1px; border: 1px solid #A9A9A9; height: 0; } .tabbody>* { flex: 1; flex-flow: column; display: none; } .tabbody>.this { display: flex; } .tabbody>*>.titlebar{ display: flex; } .tabbody>*>.titlebar>*{ flex: 1; margin: 1px 1px 1px 1px; } .tabbody>*>.context { flex: 1; flex-flow: column; border: 0; padding: 5px; overflow-y: auto; } .tabbody>*>.inputbox{ border: 0; border-bottom: #A9A9A9 solid 1px; height: 15px; text-align:center; } .link>* { display: flex; margin: 5px; border-bottom: 1px solid; text-decoration: none; } #RuleList>label>* { background: #eee; padding-left: 3px; margin: 2px 0; cursor: pointer; } #RuleList input[type=radio] { display: none; } #RuleList input[type="radio"]:checked+* { background: #15cda8; } .isError { color: #FF0000; } ================================================ FILE: app/src/main/assets/web/index.html ================================================ 书源编辑器v3.8
书源基础信息
书源名称:
书源分组:
书源类型:
书源域名:
登录网页:
书籍发现规则
发现菜单:
结果列表:
书籍名称:
书籍作者:
书籍分类:
最新章节:
简介内容:
封面链接:
详情链接:
书籍搜索规则
搜索网址:
结果验证:
结果列表:
书籍名称:
书籍作者:
书籍分类:
最新章节:
简介内容:
封面链接:
详情链接:
书籍详情规则
页面处理:
书籍名称:
书籍作者:
书籍分类:
最新章节:
简介内容:
封面链接:
目录链接:
目录列表规则
目录翻页:
目录列表:
章节名称:
章节链接:
正文阅读规则
章节正文:
正文翻页:
其它规则
浏览标识:
排序编号:
搜索权重:
是否启用:
编辑书源
调试书源
书源列表
帮助信息
================================================ FILE: app/src/main/assets/web/index.js ================================================ // 简化js原生选择器 function $(selector) { return document.querySelector(selector); } function $$(selector) { return document.querySelectorAll(selector); } // 读写Hash值(val未赋值时为读取) function hashParam(key, val) { let hashstr = decodeURIComponent(window.location.hash); let regKey = new RegExp(`${key}=([^&]*)`); let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null; if (val == undefined) return getVal; if (hashstr == '' || hashstr == '#') { window.location.hash = `#${key}=${val}`; } else { if (getVal) window.location.hash = hashstr.replace(getVal, val); else { window.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`; } } } // 创建书源规则容器对象 const RuleJSON = (() => { let ruleJson = {}; $$('.rules textarea').forEach(item => ruleJson[item.id] = ''); // for (let item of $$('.rules textarea')) ruleJson[item.id] = ''; ruleJson.serialNumber = 0; ruleJson.weight = 0; ruleJson.enable = true; return ruleJson; })(); // 选项卡Tab切换事件处理 function showTab(tabName) { $$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); }); $$('.tabbody>*').forEach(node => { node.className = node.className.replace(' this', ''); }); $(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += ' this'; $(`.tabtitle>*[name=${tabName}]`).className += ' this'; hashParam('tab', tabName); } // 书源列表列表标签构造函数 function newRule(rule) { return ``; } // 缓存规则列表 var RuleSources = []; if (localStorage.getItem('RuleSources')) { RuleSources = JSON.parse(localStorage.getItem('RuleSources')); let ruleListArray = []; RuleSources.forEach(item => { ruleListArray.push(newRule(item)); }); $('#RuleList').innerHTML += ruleListArray.join(''); } // 页面加载完成事件 window.onload = () => { $$('.tabtitle>*').forEach(item => { item.addEventListener('click', () => { showTab(item.innerHTML); }); }); if (hashParam('tab')) showTab(hashParam('tab')); } // 获取数据 function HttpGet(url) { return fetch(hashParam('domain') ? hashParam('domain') + url : url) .then(res => res.json()).catch(err => console.error('Error:', err)); } // 提交数据 function HttpPost(url, data) { return fetch(hashParam('domain') ? hashParam('domain') + url : url, { body: JSON.stringify(data), method: 'POST', mode: "cors", headers: new Headers({ 'Content-Type': 'application/json;charset=utf-8' }) }).then(res => res.json()).catch(err => console.error('Error:', err)); } // 将书源表单转化为书源对象 function rule2json() { Object.keys(RuleJSON).forEach((key) => RuleJSON[key] = $('#' + key).value); RuleJSON.bookSourceType = RuleJSON.bookSourceType.toUpperCase(); RuleJSON.serialNumber = RuleJSON.serialNumber == '' ? 0 : parseInt(RuleJSON.serialNumber); RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight); RuleJSON.enable = RuleJSON.enable == '' || RuleJSON.enable.toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true'; let TempRules = RuleSources.filter(item => (item['bookSourceUrl'] == RuleJSON['bookSourceUrl'] ? item : null)); if (TempRules.length > 0) { Object.keys(RuleJSON).forEach(key => TempRules[0][key] = RuleJSON[key]); return TempRules[0]; } return RuleJSON; } // 将书源对象填充到书源表单 function json2rule(RuleEditor) { Object.keys(RuleJSON).forEach((key) => $("#" + key).value = RuleEditor[key] ? RuleEditor[key] : ''); } // 记录操作过程 var course = { "old": [], "now": {}, "new": [] }; if (localStorage.getItem('course')) { course = JSON.parse(localStorage.getItem('course')); json2rule(course.now); } else { course.now = rule2json(); window.localStorage.setItem('course', JSON.stringify(course)); } function todo() { course.old.push(Object.assign({}, course.now)); course.now = rule2json(); course.new = []; if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小 localStorage.setItem('course', JSON.stringify(course)); } function undo() { course = JSON.parse(localStorage.getItem('course')); if (course.old.length > 0) { course.new.push(course.now); course.now = course.old.pop(); localStorage.setItem('course', JSON.stringify(course)); json2rule(course.now); } } function redo() { course = JSON.parse(localStorage.getItem('course')); if (course.new.length > 0) { course.old.push(course.now); course.now = course.new.pop(); localStorage.setItem('course', JSON.stringify(course)); json2rule(course.now); } } function setRule(editRule) { let checkRule = RuleSources.find(x => x.bookSourceUrl == editRule.bookSourceUrl); if ($(`input[id="${editRule.bookSourceUrl}"]`)) { Object.keys(checkRule).forEach(key => { checkRule[key] = editRule[key]; }); $(`input[id="${editRule.bookSourceUrl}"]+*`).innerHTML = `${editRule.bookSourceName}
${editRule.bookSourceUrl}`; } else { RuleSources.push(editRule); $('#RuleList').innerHTML += newRule(editRule); } } $$('input').forEach((item) => { item.addEventListener('change', () => { todo() }) }); $$('textarea').forEach((item) => { item.addEventListener('change', () => { todo() }) }); // 处理按钮点击事件 $('.menu').addEventListener('click', e => { let thisNode = e.target; thisNode = thisNode.parentNode.nodeName == 'svg' ? thisNode.parentNode.querySelector('rect') : thisNode.nodeName == 'svg' ? thisNode.querySelector('rect') : null; if (!thisNode) return; if (thisNode.getAttribute('class') == 'busy') return; thisNode.setAttribute('class', 'busy'); switch (thisNode.id) { case 'push': $$('#RuleList>label>div').forEach(item => { item.className = ''; }); (async () => { await HttpPost(`/saveSources`, RuleSources).then(json => { if (json.isSuccess) { console.log('批量推送书源:', RuleSources); let okData = json.data; if (Array.isArray(okData)) { let failMsg = ``; if (RuleSources.length > okData.length) { RuleSources.forEach(item => { if (okData.find(x => x.bookSourceUrl == item.bookSourceUrl)) { } else { $(`#RuleList #${item.bookSourceUrl}+*`).className += 'isError'; } }); failMsg = '\n推送失败的书源将用红色字体标注!'; } alert(`批量推送书源到「阅读APP」\n共计: ${RuleSources.length} 条\n成功: ${okData.length} 条\n失败: ${RuleSources.length - okData.length} 条${failMsg}`); } else { alert(`批量推送书源到「阅读APP」成功!\n共计: ${RuleSources.length} 条`); } } else { alert(`批量推送书源失败!\nErrorMsg: ${json.errorMsg}`); } }).catch(err => { alert(`批量推送书源失败,无法连接到「阅读APP」!\n${err}`); }); thisNode.setAttribute('class', ''); })(); return; case 'pull': showTab('书源列表'); (async () => { await HttpGet(`/getSources`).then(json => { if (json.isSuccess) { console.log('批量拉取书源:', RuleSources); $('#RuleList').innerHTML = '' localStorage.setItem('RuleSources', JSON.stringify(RuleSources = json.data)); let ruleListArray = []; RuleSources.forEach(item => { ruleListArray.push(newRule(item)); }); $('#RuleList').innerHTML += ruleListArray.join(''); alert(`成功拉取 ${RuleSources.length} 条书源`); } else { alert(`批量拉取书源失败!\nErrorMsg: ${json.errorMsg}`); } }).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读APP」!\n${err}`); }); thisNode.setAttribute('class', ''); })(); return; case 'editor': if ($('#RuleJsonString').value == '') break; try { json2rule(JSON.parse($('#RuleJsonString').value)); todo(); } catch (error) { console.log(error); alert(error); } break; case 'conver': showTab('编辑书源'); $('#RuleJsonString').value = JSON.stringify(rule2json(), null, 4); break; case 'initial': $$('.rules textarea').forEach(item => { item.value = '' }); todo(); break; case 'undo': undo() break; case 'redo': redo() break; case 'debug': showTab('调试书源'); let wsOrigin = (hashParam('domain') || location.origin).replace(/^.*?:/, 'ws:').replace(/\d+$/, (port) => (parseInt(port) + 1)); let DebugInfos = $('#DebugConsole'); function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; } let saveRule = [rule2json()]; HttpPost(`/saveSources`, saveRule).then(sResult => { if (sResult.isSuccess) { let sKey = DebugKey.value ? DebugKey.value : '我的'; $('#DebugConsole').value = `书源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`; let ws = new WebSocket(`${wsOrigin}/sourceDebug`); ws.onopen = () => { ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`); }; ws.onmessage = (msg) => { DebugPrint(msg.data == 'finish' ? `\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data); if (msg.data == 'finish') setRule(saveRule[0]); }; ws.onerror = (err) => { throw `${err.data}`; } ws.onclose = () => { thisNode.setAttribute('class', ''); DebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`); } } else throw `${sResult.errorMsg}`; }).catch(err => { DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`); thisNode.setAttribute('class', ''); }); return; case 'accept': (async () => { let saveRule = [rule2json()]; await HttpPost(`/saveSources`, saveRule).then(json => { alert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`); setRule(saveRule[0]); }).catch(err => { alert(`保存书源失败,无法连接到「阅读APP」!\n${err}`); }); thisNode.setAttribute('class', ''); })(); return; default: } setTimeout(() => { thisNode.setAttribute('class', ''); }, 500); }); $('#DebugKey').addEventListener('keydown', e => { if (e.keyCode == 13) { let clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent("click", true, false); $('#debug').dispatchEvent(clickEvent); } }); // 列表规则更改事件 $('#RuleList').addEventListener('click', e => { let editRule = null; if (e.target && e.target.getAttribute('name') == 'rule') { editRule = rule2json(); json2rule(RuleSources.find(x => x.bookSourceUrl == e.target.id)); } else return; if (editRule.bookSourceUrl == '') return; if (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\/\/|\/.*/g, ''); setRule(editRule); localStorage.setItem('RuleSources', JSON.stringify(RuleSources)); }); // 处理列表按钮事件 $('.tab3>.titlebar').addEventListener('click', e => { let thisNode = e.target; if (thisNode.nodeName != 'BUTTON') return; switch (thisNode.id) { case 'Import': let fileImport = document.createElement('input'); fileImport.type = 'file'; fileImport.accept = '.json'; fileImport.addEventListener('change', () => { let file = fileImport.files[0]; let reader = new FileReader(); reader.onloadend = function (evt) { if (evt.target.readyState == FileReader.DONE) { let fileText = evt.target.result; try { let fileJson = JSON.parse(fileText); let newSources = []; newSources.push(...fileJson); if (window.confirm(`如何处理导入的书源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)`)) { localStorage.setItem('RuleSources', JSON.stringify(RuleSources = newSources)); $('#RuleList').innerHTML = ''; } else { newSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl)); RuleSources.push(...newSources); localStorage.setItem('RuleSources', JSON.stringify(RuleSources)); } let ruleListArray = []; RuleSources.forEach(item => { ruleListArray.push(newRule(item)); }); $('#RuleList').innerHTML += ruleListArray.join(''); alert(`成功导入 ${newSources.length} 条书源`); } catch (err) { alert(`导入书源文件失败!\n${err}`); } } }; reader.readAsText(file); }, false); fileImport.click(); break; case 'Export': let fileExport = document.createElement('a'); fileExport.download = `Rules${Date().replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3').replace(/:/g, '')}.json`; let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: "application/json" }); fileExport.href = window.URL.createObjectURL(myBlob); fileExport.click(); break; case 'Delete': let selectRule = $('#RuleList input:checked'); if (!selectRule) { alert(`没有书源被选中!`); return; } if (confirm(`确定要删除选定书源吗?\n(同时删除APP内书源)`)) { let selectRuleUrl = selectRule.id; let deleteSources = RuleSources.filter(item => item.bookSourceUrl == selectRuleUrl); // 提取待删除的书源 let laveSources = RuleSources.filter(item => !(item.bookSourceUrl == selectRuleUrl)); // 提取待留下的书源 HttpPost(`/deleteSources`, deleteSources).then(json => { if (json.isSuccess) { let selectNode = document.getElementById(selectRuleUrl).parentNode; selectNode.parentNode.removeChild(selectNode); localStorage.setItem('RuleSources', JSON.stringify(RuleSources = laveSources)); if ($('#bookSourceUrl').value == selectRuleUrl) { $$('.rules textarea').forEach(item => { item.value = '' }); todo(); } console.log(deleteSources); console.log(`以上书源已删除!`) } }).catch(err => { alert(`删除书源失败,无法连接到「阅读APP」!\n${err}`); }); } break; case 'ClrAll': if (confirm(`确定要清空当前书源列表吗?\n(不会删除APP内书源)`)) { localStorage.setItem('RuleSources', JSON.stringify(RuleSources = [])); $('#RuleList').innerHTML = '' } break; default: } }); ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/DbHelper.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import com.github.yuweiguocn.library.greendao.MigrationHelper; import com.kunfei.bookshelf.dao.BookChapterBeanDao; import com.kunfei.bookshelf.dao.BookInfoBeanDao; import com.kunfei.bookshelf.dao.BookShelfBeanDao; import com.kunfei.bookshelf.dao.BookSourceBeanDao; import com.kunfei.bookshelf.dao.BookmarkBeanDao; import com.kunfei.bookshelf.dao.CookieBeanDao; import com.kunfei.bookshelf.dao.DaoMaster; import com.kunfei.bookshelf.dao.DaoSession; import com.kunfei.bookshelf.dao.ReplaceRuleBeanDao; import com.kunfei.bookshelf.dao.SearchHistoryBeanDao; import com.kunfei.bookshelf.dao.TxtChapterRuleBeanDao; import org.greenrobot.greendao.database.Database; import java.util.Locale; public class DbHelper { private static DbHelper instance; private final SQLiteDatabase db; private final DaoSession mDaoSession; private DbHelper() { DaoOpenHelper mHelper = new DaoOpenHelper(MApplication.getInstance(), "monkebook_db", null); db = mHelper.getWritableDatabase(); db.setLocale(Locale.CHINESE); // 注意:该数据库连接属于 DaoMaster,所以多个 Session 指的是相同的数据库连接。 DaoMaster mDaoMaster = new DaoMaster(db); mDaoSession = mDaoMaster.newSession(); } public static DbHelper getInstance() { if (null == instance) { synchronized (DbHelper.class) { if (null == instance) { instance = new DbHelper(); } } } return instance; } public static DaoSession getDaoSession() { return getInstance().mDaoSession; } public static SQLiteDatabase getDb() { return getInstance().db; } public static class DaoOpenHelper extends DaoMaster.OpenHelper { DaoOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) { super(context, name, factory); } @Override @SuppressWarnings("unchecked") public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { MigrationHelper.migrate(db, new MigrationHelper.ReCreateAllTableListener() { @Override public void onCreateAllTables(Database db, boolean ifNotExists) { DaoMaster.createAllTables(db, ifNotExists); } @Override public void onDropAllTables(Database db, boolean ifExists) { DaoMaster.dropAllTables(db, ifExists); } }, BookShelfBeanDao.class, BookInfoBeanDao.class, BookChapterBeanDao.class, SearchHistoryBeanDao.class, BookSourceBeanDao.class, ReplaceRuleBeanDao.class, BookmarkBeanDao.class, CookieBeanDao.class, TxtChapterRuleBeanDao.class ); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/MApplication.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Build; import android.text.TextUtils; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatDelegate; import androidx.multidex.MultiDex; import com.kunfei.bookshelf.constant.AppConstant; import com.kunfei.bookshelf.help.AppFrontBackHelper; import com.kunfei.bookshelf.help.CrashHandler; import com.kunfei.bookshelf.help.FileHelp; import com.kunfei.bookshelf.model.UpLastChapterModel; import com.kunfei.bookshelf.utils.theme.ThemeStore; import java.io.File; import java.util.Arrays; import java.util.Objects; import java.util.concurrent.TimeUnit; import io.reactivex.internal.functions.Functions; import io.reactivex.plugins.RxJavaPlugins; import timber.log.Timber; public class MApplication extends Application { public final static String channelIdDownload = "channel_download"; public final static String channelIdReadAloud = "channel_read_aloud"; public final static String channelIdWeb = "channel_web"; public static String downloadPath; public static boolean isEInkMode; public static String SEARCH_GROUP = null; private static MApplication instance; private static String versionName; private static int versionCode; private SharedPreferences configPreferences; private boolean donateHb; public static MApplication getInstance() { return instance; } public static int getVersionCode() { return versionCode; } public static String getVersionName() { return versionName; } public static Resources getAppResources() { return getInstance().getResources(); } @Override public void onCreate() { super.onCreate(); instance = this; CrashHandler.getInstance().init(this); Timber.plant(new Timber.DebugTree()); RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); try { versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; } catch (PackageManager.NameNotFoundException e) { versionCode = 0; versionName = "0.0.0"; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createChannelId(); } configPreferences = getSharedPreferences("CONFIG", 0); downloadPath = configPreferences.getString(getString(R.string.pk_download_path), ""); if (TextUtils.isEmpty(downloadPath) | Objects.equals(downloadPath, FileHelp.getCachePath())) { setDownloadPath(null); } initNightTheme(); if (!ThemeStore.isConfigured(this, versionCode)) { upThemeStore(); } AppFrontBackHelper.getInstance().register(this, new AppFrontBackHelper.OnAppStatusListener() { @Override public void onFront() { donateHb = System.currentTimeMillis() - configPreferences.getLong("DonateHb", 0) <= TimeUnit.DAYS.toMillis(30); } @Override public void onBack() { UpLastChapterModel.destroy(); } }); upEInkMode(); } @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } public void initNightTheme() { if (isNightTheme()) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); } else { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); } } /** * 初始化主题 */ public void upThemeStore() { if (isNightTheme()) { ThemeStore.editTheme(this) .primaryColor(configPreferences.getInt("colorPrimaryNight", getResources().getColor(R.color.md_grey_800))) .accentColor(configPreferences.getInt("colorAccentNight", getResources().getColor(R.color.md_pink_800))) .backgroundColor(configPreferences.getInt("colorBackgroundNight", getResources().getColor(R.color.md_grey_800))) .apply(); } else { ThemeStore.editTheme(this) .primaryColor(configPreferences.getInt("colorPrimary", getResources().getColor(R.color.md_grey_100))) .accentColor(configPreferences.getInt("colorAccent", getResources().getColor(R.color.md_pink_600))) .backgroundColor(configPreferences.getInt("colorBackground", getResources().getColor(R.color.md_grey_100))) .apply(); } } public boolean isNightTheme() { return configPreferences.getBoolean("nightTheme", false); } /** * 设置下载地址 */ public void setDownloadPath(String path) { if (TextUtils.isEmpty(path)) { downloadPath = FileHelp.getFilesPath(); } else { downloadPath = path; } AppConstant.BOOK_CACHE_PATH = downloadPath + File.separator + "book_cache" + File.separator; configPreferences.edit() .putString(getString(R.string.pk_download_path), path) .apply(); } public static SharedPreferences getConfigPreferences() { return getInstance().configPreferences; } public boolean getDonateHb() { return donateHb || BuildConfig.DEBUG; } public void upDonateHb() { configPreferences.edit() .putLong("DonateHb", System.currentTimeMillis()) .apply(); donateHb = true; } public void upEInkMode() { MApplication.isEInkMode = configPreferences.getBoolean("E-InkMode", false); } /** * 创建通知ID */ @RequiresApi(Build.VERSION_CODES.O) private void createChannelId() { NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); //用唯一的ID创建渠道对象 NotificationChannel downloadChannel = new NotificationChannel(channelIdDownload, getString(R.string.download_offline), NotificationManager.IMPORTANCE_LOW); //初始化channel downloadChannel.enableLights(false); downloadChannel.enableVibration(false); downloadChannel.setSound(null, null); //用唯一的ID创建渠道对象 NotificationChannel readAloudChannel = new NotificationChannel(channelIdReadAloud, getString(R.string.read_aloud), NotificationManager.IMPORTANCE_LOW); //初始化channel readAloudChannel.enableLights(false); readAloudChannel.enableVibration(false); readAloudChannel.setSound(null, null); //用唯一的ID创建渠道对象 NotificationChannel webChannel = new NotificationChannel(channelIdWeb, getString(R.string.web_service), NotificationManager.IMPORTANCE_LOW); //初始化channel webChannel.enableLights(false); webChannel.enableVibration(false); webChannel.setSound(null, null); //向notification manager 提交channel if (notificationManager != null) { notificationManager.createNotificationChannels(Arrays.asList(downloadChannel, readAloudChannel, webChannel)); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/BaseDialogFragment.kt ================================================ package com.kunfei.bookshelf.base import android.os.Bundle import android.view.View import androidx.annotation.LayoutRes import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import com.kunfei.bookshelf.utils.theme.ThemeStore import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlin.coroutines.CoroutineContext abstract class BaseDialogFragment(@LayoutRes layoutID: Int) : DialogFragment(layoutID), CoroutineScope by MainScope() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.setBackgroundColor(ThemeStore.backgroundColor(requireContext())) onFragmentCreated(view, savedInstanceState) observeLiveBus() } abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) override fun show(manager: FragmentManager, tag: String?) { kotlin.runCatching { //在每个add事务前增加一个remove事务,防止连续的add manager.beginTransaction().remove(this).commit() super.show(manager, tag) } } override fun onDestroy() { super.onDestroy() cancel() } fun execute( scope: CoroutineScope = this, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ) = Coroutine.async(scope, context) { block() } open fun observeLiveBus() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/BaseFragment.kt ================================================ package com.kunfei.bookshelf.base import android.annotation.SuppressLint import android.content.res.Configuration import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.annotation.LayoutRes import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel @Suppress("MemberVisibilityCanBePrivate") abstract class BaseFragment(@LayoutRes layoutID: Int) : Fragment(layoutID), CoroutineScope by MainScope() { var supportToolbar: Toolbar? = null private set val menuInflater: MenuInflater @SuppressLint("RestrictedApi") get() = SupportMenuInflater(requireContext()) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) onMultiWindowModeChanged() onFragmentCreated(view, savedInstanceState) observeLiveBus() } abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) { super.onMultiWindowModeChanged(isInMultiWindowMode) onMultiWindowModeChanged() } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) onMultiWindowModeChanged() } private fun onMultiWindowModeChanged() { } override fun onDestroy() { super.onDestroy() cancel() } open fun observeLiveBus() { } open fun onCompatCreateOptionsMenu(menu: Menu) { } open fun onCompatOptionsItemSelected(item: MenuItem) { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/BaseModelImpl.java ================================================ package com.kunfei.bookshelf.base; import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.webkit.CookieManager; import android.webkit.WebView; import android.webkit.WebViewClient; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.bean.CookieBean; import com.kunfei.bookshelf.help.EncodeConverter; import com.kunfei.bookshelf.help.SSLSocketClient; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl; import com.kunfei.bookshelf.model.impl.IHttpGetApi; import com.kunfei.bookshelf.model.impl.IHttpPostApi; import org.apache.commons.lang3.StringEscapeUtils; import java.util.ArrayList; import java.util.Collections; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import okhttp3.ConnectionSpec; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; public class BaseModelImpl { private static OkHttpClient httpClient; public static BaseModelImpl getInstance() { return new BaseModelImpl(); } public Observable> getResponseO(AnalyzeUrl analyzeUrl) { switch (analyzeUrl.getUrlMode()) { case POST: if (analyzeUrl.getJsonBody() != null) { return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode()) .create(IHttpPostApi.class) .postJson(analyzeUrl.getPath(), analyzeUrl.getPostBody(), analyzeUrl.getHeaderMap()); } else { return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode()) .create(IHttpPostApi.class) .postMap(analyzeUrl.getPath(), analyzeUrl.getQueryMap(), analyzeUrl.getHeaderMap()); } case GET: return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode()) .create(IHttpGetApi.class) .getMap(analyzeUrl.getPath(), analyzeUrl.getQueryMap(), analyzeUrl.getHeaderMap()); default: return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode()) .create(IHttpGetApi.class) .get(analyzeUrl.getPath(), analyzeUrl.getHeaderMap()); } } public Retrofit getRetrofitString(String url) { return new Retrofit.Builder().baseUrl(url) //增加返回值为字符串的支持(以实体类返回) .addConverterFactory(EncodeConverter.create()) //增加返回值为Observable的支持 .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(getClient()) .build(); } public Retrofit getRetrofitString(String url, String encode) { return new Retrofit.Builder().baseUrl(url) //增加返回值为字符串的支持(以实体类返回) .addConverterFactory(EncodeConverter.create(encode)) //增加返回值为Observable的支持 .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(getClient()) .build(); } synchronized public static OkHttpClient getClient() { if (httpClient == null) { ArrayList specs = new ArrayList<>(); specs.add(ConnectionSpec.MODERN_TLS); specs.add(ConnectionSpec.COMPATIBLE_TLS); specs.add(ConnectionSpec.CLEARTEXT); httpClient = new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.createTrustAllManager()) .hostnameVerifier(SSLSocketClient.getHostnameVerifier()) .followRedirects(true) .followSslRedirects(true) .connectionSpecs(specs) .protocols(Collections.singletonList(Protocol.HTTP_1_1)) .addInterceptor(getHeaderInterceptor()) .build(); } return httpClient; } 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); }; } protected Observable> setCookie(Response response, String tag) { return Observable.create(e -> { if (!response.raw().headers("Set-Cookie").isEmpty()) { StringBuilder cookieBuilder = new StringBuilder(); for (String s : response.raw().headers("Set-Cookie")) { String[] x = s.split(";"); for (String y : x) { if (!TextUtils.isEmpty(y)) { cookieBuilder.append(y).append(";"); } } } String cookie = cookieBuilder.toString(); if (!TextUtils.isEmpty(cookie)) { DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(tag, cookie)); } } e.onNext(response); e.onComplete(); }); } @SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"}) protected Observable getAjaxString(AnalyzeUrl analyzeUrl, String tag, String js) { final Web web = new Web("加载超时"); if (!TextUtils.isEmpty(js)) { web.js = js; } return Observable.create(e -> { Handler handler = new Handler(Looper.getMainLooper()); handler.post(() -> { Runnable timeoutRunnable; WebView webView = new WebView(MApplication.getInstance()); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setUserAgentString(analyzeUrl.getHeaderMap().get("User-Agent")); CookieManager cookieManager = CookieManager.getInstance(); Runnable retryRunnable = new Runnable() { @Override public void run() { webView.evaluateJavascript(web.js, value -> { if (!TextUtils.isEmpty(value)) { web.content = StringEscapeUtils.unescapeJson(value); e.onNext(web.content); e.onComplete(); webView.destroy(); handler.removeCallbacks(this); } else { handler.postDelayed(this, 1000); } }); } }; timeoutRunnable = () -> { if (!e.isDisposed()) { handler.removeCallbacks(retryRunnable); e.onNext(web.content); e.onComplete(); webView.destroy(); } }; handler.postDelayed(timeoutRunnable, 30000); webView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { DbHelper.getDaoSession().getCookieBeanDao() .insertOrReplace(new CookieBean(tag, cookieManager.getCookie(webView.getUrl()))); handler.postDelayed(retryRunnable, 1000); } }); switch (analyzeUrl.getUrlMode()) { case POST: webView.postUrl(analyzeUrl.getUrl(), analyzeUrl.getPostData()); break; case GET: webView.loadUrl(String.format("%s?%s", analyzeUrl.getUrl(), analyzeUrl.getQueryStr()), analyzeUrl.getHeaderMap()); break; default: webView.loadUrl(analyzeUrl.getUrl(), analyzeUrl.getHeaderMap()); } }); }); } private static class Web { private String content; private String js = "document.documentElement.outerHTML"; Web(String content) { this.content = content; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/BaseService.kt ================================================ package com.kunfei.bookshelf.base import android.app.Service import android.content.Intent import android.os.IBinder import androidx.annotation.CallSuper import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlin.coroutines.CoroutineContext abstract class BaseService : Service(), CoroutineScope by MainScope() { fun execute( scope: CoroutineScope = this, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ) = Coroutine.async(scope, context) { block() } @CallSuper override fun onCreate() { super.onCreate() } @CallSuper override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) stopSelf() } override fun onBind(intent: Intent?): IBinder? { return null } @CallSuper override fun onDestroy() { super.onDestroy() cancel() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/BaseTabActivity.java ================================================ package com.kunfei.bookshelf.base; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import java.util.List; /** * Created by newbiechen on 17-4-24. */ public abstract class BaseTabActivity extends MBaseActivity { /**************View***************/ protected TabLayout mTlIndicator; protected ViewPager mVp; /**************Adapter***************/ protected TabFragmentPageAdapter tabFragmentPageAdapter; /************Params*******************/ protected List mFragmentList; private List mTitleList; /**************abstract***********/ protected abstract List createTabFragments(); protected abstract List createTabTitles(); @Override protected void bindView() { super.bindView(); mTlIndicator = findViewById(R.id.tab_tl_indicator); mVp = findViewById(R.id.tab_vp); setUpTabLayout(); } /*****************rewrite method***************************/ private void setUpTabLayout() { mFragmentList = createTabFragments(); mTitleList = createTabTitles(); checkParamsIsRight(); tabFragmentPageAdapter = new TabFragmentPageAdapter(getSupportFragmentManager()); mVp.setAdapter(tabFragmentPageAdapter); mVp.setOffscreenPageLimit(3); mTlIndicator.setupWithViewPager(mVp); } /** * 检查输入的参数是否正确。即Fragment和title是成对的。 */ private void checkParamsIsRight() { if (mFragmentList == null || mTitleList == null) { throw new IllegalArgumentException("fragmentList or titleList doesn't have null"); } if (mFragmentList.size() != mTitleList.size()) throw new IllegalArgumentException("fragment and title size must equal"); } /******************inner class*****************/ public class TabFragmentPageAdapter extends FragmentPagerAdapter { TabFragmentPageAdapter(FragmentManager fm) { super(fm); } @NonNull @Override public Fragment getItem(int position) { return mFragmentList.get(position); } @Override public int getCount() { return mFragmentList.size(); } @Override public CharSequence getPageTitle(int position) { return mTitleList.get(position); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/BaseViewModel.kt ================================================ package com.kunfei.bookshelf.base import android.app.Application import android.content.Context import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.kunfei.bookshelf.MApplication import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext @Suppress("unused") open class BaseViewModel(application: Application) : AndroidViewModel(application) { val context: Context by lazy { this.getApplication() } fun execute( scope: CoroutineScope = viewModelScope, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ): Coroutine { return Coroutine.async(scope, context) { block() } } fun submit( scope: CoroutineScope = viewModelScope, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> Deferred ): Coroutine { return Coroutine.async(scope, context) { block().await() } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/MBaseActivity.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.base; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.annotation.Nullable; import com.google.android.material.snackbar.Snackbar; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BaseActivity; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.utils.ActivityExtensionsKt; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.SoftInputUtil; import com.kunfei.bookshelf.utils.theme.MaterialValueHelper; import com.kunfei.bookshelf.utils.theme.ThemeStore; import java.lang.reflect.Method; import java.util.ArrayList; public abstract class MBaseActivity extends BaseActivity { private static final String TAG = MBaseActivity.class.getSimpleName(); public final SharedPreferences preferences = MApplication.getConfigPreferences(); private Snackbar snackbar; @Override protected void onCreate(Bundle savedInstanceState) { initTheme(); super.onCreate(savedInstanceState); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { getWindow().getDecorView().setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS); } initImmersionBar(); } @SuppressWarnings("NullableProblems") @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } @Override protected void onDestroy() { super.onDestroy(); } @Override protected void onResume() { super.onResume(); } @Override protected void onPause() { super.onPause(); } @Override public void setSupportActionBar(@Nullable androidx.appcompat.widget.Toolbar toolbar) { if (toolbar != null) { toolbar.setBackgroundColor(ThemeStore.primaryColor(this)); } super.setSupportActionBar(toolbar); } /** * 设置MENU图标颜色 */ @Override public boolean onCreateOptionsMenu(Menu menu) { int primaryTextColor = MaterialValueHelper.getPrimaryTextColor(this, ColorUtils.isColorLight(ThemeStore.primaryColor(this))); for (int i = 0; i < menu.size(); i++) { Drawable drawable = menu.getItem(i).getIcon(); if (drawable != null) { drawable.mutate(); drawable.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_ATOP); } } return super.onCreateOptionsMenu(menu); } @SuppressLint("PrivateApi") @SuppressWarnings("unchecked") @Override public boolean onMenuOpened(int featureId, Menu menu) { if (menu != null) { //展开菜单显示图标 if (menu.getClass().getSimpleName().equalsIgnoreCase("MenuBuilder")) { try { Method method = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE); method.setAccessible(true); method.invoke(menu, true); method = menu.getClass().getDeclaredMethod("getNonActionItems"); ArrayList menuItems = (ArrayList) method.invoke(menu); if (!menuItems.isEmpty()) { for (MenuItem menuItem : menuItems) { Drawable drawable = menuItem.getIcon(); if (drawable != null) { drawable.mutate(); drawable.setColorFilter(getResources().getColor(R.color.tv_text_default), PorterDuff.Mode.SRC_ATOP); } } } } catch (Exception ignored) { } } } return super.onMenuOpened(featureId, menu); } /** * 沉浸状态栏 */ protected void initImmersionBar() { try { View actionBar = findViewById(R.id.action_bar); ActivityExtensionsKt.fullScreen(this); if (isImmersionBarEnabled()) { boolean isTransparent = getSupportActionBar() != null && actionBar != null && actionBar.getVisibility() == View.VISIBLE; ActivityExtensionsKt.setStatusBarColorAuto(this, ThemeStore.primaryColor(this), isTransparent, isTransparent); } else { if (getSupportActionBar() != null && actionBar != null && actionBar.getVisibility() == View.VISIBLE) { ActivityExtensionsKt.setStatusBarColorAuto(this, ThemeStore.statusBarColor(this), false, false); } else { ActivityExtensionsKt.setStatusBarColorAuto(this, getResources().getColor(R.color.status_bar_bag), false, false); } } } catch (Exception ignored) { } try { if (!preferences.getBoolean("navigationBarColorChange", false)) { ActivityExtensionsKt.setNavigationBarColorAuto(this, getResources().getColor(R.color.black)); } else { ActivityExtensionsKt.setNavigationBarColorAuto(this, ThemeStore.primaryColorDark(this)); } } catch (Exception ignored) { } } /** * @return 是否沉浸 */ protected boolean isImmersionBarEnabled() { return preferences.getBoolean("immersionStatusBar", false); } /** * 设置屏幕方向 */ @SuppressLint("SourceLockedOrientationActivity") public void setOrientation(int screenDirection) { switch (screenDirection) { case 0: setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); break; case 1: setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); break; case 2: setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); break; case 3: setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); break; } } /** * @return 是否夜间模式 */ public boolean isNightTheme() { return MApplication.getInstance().isNightTheme(); } protected void setNightTheme(boolean isNightTheme) { preferences.edit() .putBoolean("nightTheme", isNightTheme) .apply(); MApplication.getInstance().initNightTheme(); MApplication.getInstance().upThemeStore(); RxBus.get().post(RxBusTag.RECREATE, true); } protected void initTheme() { if (ColorUtils.isColorLight(ThemeStore.primaryColor(this))) { setTheme(R.style.CAppTheme); } else { setTheme(R.style.CAppThemeBarDark); } } public void showSnackBar(View view, String msg) { showSnackBar(view, msg, Snackbar.LENGTH_SHORT); } public void showSnackBar(View view, String msg, int length) { if (snackbar == null) { snackbar = Snackbar.make(view, msg, length); } else { snackbar.setText(msg); snackbar.setDuration(length); } snackbar.show(); } public void hideSnackBar() { if (snackbar != null) { snackbar.dismiss(); } } @Override public void startActivity(Intent intent) { super.startActivity(intent); if (MApplication.isEInkMode) { overridePendingTransition(R.anim.anim_none, R.anim.anim_none); } } @SuppressWarnings("deprecation") @Override public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { super.startActivityForResult(intent, requestCode, options); if (MApplication.isEInkMode) { overridePendingTransition(R.anim.anim_none, R.anim.anim_none); } } @Override public void finish() { SoftInputUtil.hideIMM(getCurrentFocus()); super.finish(); if (MApplication.isEInkMode) { overridePendingTransition(R.anim.anim_none, R.anim.anim_none); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/MBaseFragment.java ================================================ package com.kunfei.bookshelf.base; import android.content.SharedPreferences; import android.os.Bundle; import android.widget.Toast; import androidx.annotation.Nullable; import com.kunfei.basemvplib.BaseFragment; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.MApplication; public abstract class MBaseFragment extends BaseFragment implements IView { public final SharedPreferences preferences = MApplication.getConfigPreferences(); protected T mPresenter; @Override public void onCreate(@Nullable Bundle savedInstanceState) { mPresenter = initInjector(); attachView(); super.onCreate(savedInstanceState); } @Override public void onDestroy() { super.onDestroy(); detachView(); } /** * P层绑定 若无则返回null; */ protected abstract T initInjector(); /** * P层绑定V层 */ private void attachView() { if (null != mPresenter) { mPresenter.attachView(this); } } /** * P层解绑V层 */ private void detachView() { if (null != mPresenter) { mPresenter.detachView(); } } public void toast(String msg) { Toast.makeText(this.getActivity(), msg, Toast.LENGTH_SHORT).show(); } public void toast(int id) { Toast.makeText(this.getActivity(), getString(id), Toast.LENGTH_SHORT).show(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/README.md ================================================ # 基类 ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/VMBaseFragment.kt ================================================ package com.kunfei.bookshelf.base import androidx.lifecycle.ViewModel abstract class VMBaseFragment(layoutID: Int) : BaseFragment(layoutID) { protected abstract val viewModel: VM } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/DiffRecyclerAdapter.kt ================================================ package com.kunfei.bookshelf.base.adapter import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding /** * Created by Invincible on 2017/12/15. */ @Suppress("unused", "MemberVisibilityCanBePrivate") abstract class DiffRecyclerAdapter(protected val context: Context) : RecyclerView.Adapter() { val inflater: LayoutInflater = LayoutInflater.from(context) private val asyncListDiffer: AsyncListDiffer by lazy { AsyncListDiffer(this, diffItemCallback).apply { addListListener { _, _ -> onCurrentListChanged() } } } private val lock = Object() private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null var itemAnimation: ItemAnimation? = null abstract val diffItemCallback: DiffUtil.ItemCallback fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { itemClickListener = listener } fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) { itemLongClickListener = listener } fun bindToRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = this } fun setItems(items: List?) { synchronized(lock) { asyncListDiffer.submitList(items) } } fun setItem(position: Int, item: ITEM) { synchronized(lock) { val list = ArrayList(asyncListDiffer.currentList) list[position] = item asyncListDiffer.submitList(list) } } fun updateItem(item: ITEM) = synchronized(lock) { val index = asyncListDiffer.currentList.indexOf(item) if (index >= 0) { asyncListDiffer.currentList[index] = item notifyItemChanged(index) } } fun updateItem(position: Int, payload: Any) = synchronized(lock) { val size = itemCount if (position in 0 until size) { notifyItemChanged(position, payload) } } fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) = synchronized(lock) { val size = itemCount if (fromPosition in 0 until size && toPosition in 0 until size) { notifyItemRangeChanged( fromPosition, toPosition - fromPosition + 1, payloads ) } } fun isEmpty() = asyncListDiffer.currentList.isEmpty() fun isNotEmpty() = asyncListDiffer.currentList.isNotEmpty() fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position) fun getItems(): List = asyncListDiffer.currentList /** * grid 模式下使用 */ protected open fun getSpanSize(viewType: Int, position: Int) = 1 final override fun getItemCount() = getItems().size final override fun getItemViewType(position: Int): Int { return 0 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { val holder = ItemViewHolder(getViewBinding(parent)) @Suppress("UNCHECKED_CAST") registerListener(holder, (holder.binding as VB)) if (itemClickListener != null) { holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { itemClickListener?.invoke(holder, it) } } } if (itemLongClickListener != null) { holder.itemView.setOnLongClickListener { getItem(holder.layoutPosition)?.let { itemLongClickListener?.invoke(holder, it) ?: true } ?: true } } return holder } protected abstract fun getViewBinding(parent: ViewGroup): VB final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {} open fun onCurrentListChanged() { } @Suppress("UNCHECKED_CAST") final override fun onBindViewHolder( holder: ItemViewHolder, position: Int, payloads: MutableList ) { getItem(holder.layoutPosition)?.let { convert(holder, (holder.binding as VB), it, payloads) } } override fun onViewAttachedToWindow(holder: ItemViewHolder) { super.onViewAttachedToWindow(holder) addAnimation(holder) } override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) val manager = recyclerView.layoutManager if (manager is GridLayoutManager) { manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return getSpanSize(getItemViewType(position), position) } } } } private fun addAnimation(holder: ItemViewHolder) { itemAnimation?.let { if (it.itemAnimEnabled) { if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { startAnimation(holder, it) it.itemAnimStartPosition = holder.layoutPosition } } } } protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) { item.itemAnimation?.let { for (anim in it.getAnimators(holder.itemView)) { anim.setDuration(item.itemAnimDuration).start() anim.interpolator = item.itemAnimInterpolator } } } /** * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, * 使用getItem(holder.layoutPosition)来获取item */ abstract fun convert( holder: ItemViewHolder, binding: VB, item: ITEM, payloads: MutableList ) /** * 注册事件 */ abstract fun registerListener(holder: ItemViewHolder, binding: VB) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/ItemAnimation.kt ================================================ package com.kunfei.bookshelf.base.adapter import android.view.animation.Interpolator import android.view.animation.LinearInterpolator import com.kunfei.bookshelf.base.adapter.animations.* /** * Created by Invincible on 2017/12/15. */ @Suppress("unused") class ItemAnimation private constructor() { var itemAnimEnabled = false var itemAnimFirstOnly = true var itemAnimation: BaseAnimation? = null var itemAnimInterpolator: Interpolator = LinearInterpolator() var itemAnimDuration: Long = 300L var itemAnimStartPosition: Int = -1 fun interpolator(interpolator: Interpolator) = apply { itemAnimInterpolator = interpolator } fun duration(duration: Long) = apply { itemAnimDuration = duration } fun startPosition(startPos: Int) = apply { itemAnimStartPosition = startPos } fun animation(animationType: Int = NONE, animation: BaseAnimation? = null) = apply { if (animation != null) { itemAnimation = animation } else { when (animationType) { FADE_IN -> itemAnimation = AlphaInAnimation() SCALE_IN -> itemAnimation = ScaleInAnimation() BOTTOM_SLIDE_IN -> itemAnimation = SlideInBottomAnimation() LEFT_SLIDE_IN -> itemAnimation = SlideInLeftAnimation() RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation() } } } fun enabled(enabled: Boolean) = apply { itemAnimEnabled = enabled } fun firstOnly(firstOnly: Boolean) = apply { itemAnimFirstOnly = firstOnly } companion object { const val NONE: Int = 0x00000000 /** * Use with [.openLoadAnimation] */ const val FADE_IN: Int = 0x00000001 /** * Use with [.openLoadAnimation] */ const val SCALE_IN: Int = 0x00000002 /** * Use with [.openLoadAnimation] */ const val BOTTOM_SLIDE_IN: Int = 0x00000003 /** * Use with [.openLoadAnimation] */ const val LEFT_SLIDE_IN: Int = 0x00000004 /** * Use with [.openLoadAnimation] */ const val RIGHT_SLIDE_IN: Int = 0x00000005 fun create() = ItemAnimation() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/ItemViewHolder.kt ================================================ package com.kunfei.bookshelf.base.adapter import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding /** * Created by Invincible on 2017/11/28. */ @Suppress("MemberVisibilityCanBePrivate") class ItemViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/RecyclerAdapter.kt ================================================ package com.kunfei.bookshelf.base.adapter import android.content.Context import android.util.SparseArray import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import java.util.* /** * Created by Invincible on 2017/11/24. * * 通用的adapter 可添加header,footer,以及不同类型item */ @Suppress("unused", "MemberVisibilityCanBePrivate") abstract class RecyclerAdapter(protected val context: Context) : RecyclerView.Adapter() { val inflater: LayoutInflater = LayoutInflater.from(context) private val headerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() } private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() } private val items: MutableList = mutableListOf() private val lock = Object() private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null var itemAnimation: ItemAnimation? = null fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { itemClickListener = listener } fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) { itemLongClickListener = listener } fun bindToRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = this } fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) { synchronized(lock) { val index = headerItems.size() headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header) notifyItemInserted(index) } } fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) = synchronized(lock) { val index = getActualItemCount() + footerItems.size() footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer) notifyItemInserted(index) } fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) = synchronized(lock) { val index = headerItems.indexOfValue(header) if (index >= 0) { headerItems.remove(index) notifyItemRemoved(index) } } fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) = synchronized(lock) { val index = footerItems.indexOfValue(footer) if (index >= 0) { footerItems.remove(index) notifyItemRemoved(getActualItemCount() + index - 2) } } fun setItems(items: List?) { synchronized(lock) { if (this.items.isNotEmpty()) { this.items.clear() } if (items != null) { this.items.addAll(items) } notifyDataSetChanged() onCurrentListChanged() } } fun setItems(items: List?, diffResult: DiffUtil.DiffResult) { synchronized(lock) { if (this.items.isNotEmpty()) { this.items.clear() } if (items != null) { this.items.addAll(items) } diffResult.dispatchUpdatesTo(this) onCurrentListChanged() } } fun setItem(position: Int, item: ITEM) { synchronized(lock) { val oldSize = getActualItemCount() if (position in 0 until oldSize) { this.items[position] = item notifyItemChanged(position + getHeaderCount()) } onCurrentListChanged() } } fun addItem(item: ITEM) { synchronized(lock) { val oldSize = getActualItemCount() if (this.items.add(item)) { notifyItemInserted(oldSize + getHeaderCount()) } onCurrentListChanged() } } fun addItems(position: Int, newItems: List) { synchronized(lock) { if (this.items.addAll(position, newItems)) { notifyItemRangeInserted(position + getHeaderCount(), newItems.size) } onCurrentListChanged() } } fun addItems(newItems: List) { synchronized(lock) { val oldSize = getActualItemCount() if (this.items.addAll(newItems)) { if (oldSize == 0 && getHeaderCount() == 0) { notifyDataSetChanged() } else { notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size) } } onCurrentListChanged() } } fun removeItem(position: Int) { synchronized(lock) { if (this.items.removeAt(position) != null) { notifyItemRemoved(position + getHeaderCount()) } onCurrentListChanged() } } fun removeItem(item: ITEM) { synchronized(lock) { if (this.items.remove(item)) { notifyItemRemoved(this.items.indexOf(item) + getHeaderCount()) } onCurrentListChanged() } } fun removeItems(items: List) { synchronized(lock) { if (this.items.removeAll(items)) { notifyDataSetChanged() } onCurrentListChanged() } } fun swapItem(oldPosition: Int, newPosition: Int) { synchronized(lock) { val size = getActualItemCount() if (oldPosition in 0 until size && newPosition in 0 until size) { val srcPosition = oldPosition + getHeaderCount() val targetPosition = newPosition + getHeaderCount() Collections.swap(this.items, srcPosition, targetPosition) notifyItemMoved(srcPosition, targetPosition) } onCurrentListChanged() } } fun updateItem(item: ITEM) = synchronized(lock) { val index = this.items.indexOf(item) if (index >= 0) { this.items[index] = item notifyItemChanged(index) } onCurrentListChanged() } fun updateItem(position: Int, payload: Any) = synchronized(lock) { val size = getActualItemCount() if (position in 0 until size) { notifyItemChanged(position + getHeaderCount(), payload) } } fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) = synchronized(lock) { val size = getActualItemCount() if (fromPosition in 0 until size && toPosition in 0 until size) { notifyItemRangeChanged( fromPosition + getHeaderCount(), toPosition - fromPosition + 1, payloads ) } } fun clearItems() = synchronized(lock) { this.items.clear() notifyDataSetChanged() onCurrentListChanged() } fun isEmpty() = items.isEmpty() fun isNotEmpty() = items.isNotEmpty() /** * 除去header和footer */ fun getActualItemCount() = items.size fun getHeaderCount() = headerItems.size() fun getFooterCount() = footerItems.size() fun getItem(position: Int): ITEM? = items.getOrNull(position) fun getItemByLayoutPosition(position: Int) = items.getOrNull(position - getHeaderCount()) fun getItems(): List = items protected open fun getItemViewType(item: ITEM, position: Int) = 0 /** * grid 模式下使用 */ protected open fun getSpanSize(viewType: Int, position: Int) = 1 final override fun getItemCount() = getActualItemCount() + getHeaderCount() + getFooterCount() final override fun getItemViewType(position: Int) = when { isHeader(position) -> TYPE_HEADER_VIEW + position isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() else -> getItem(getActualPosition(position))?.let { getItemViewType(it, getActualPosition(position)) } ?: 0 } open fun onCurrentListChanged() { } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when { viewType < TYPE_HEADER_VIEW + getHeaderCount() -> { ItemViewHolder(headerItems.get(viewType).invoke(parent)) } viewType >= TYPE_FOOTER_VIEW -> { ItemViewHolder(footerItems.get(viewType).invoke(parent)) } else -> { val holder = ItemViewHolder(getViewBinding(parent)) @Suppress("UNCHECKED_CAST") registerListener(holder, (holder.binding as VB)) if (itemClickListener != null) { holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { itemClickListener?.invoke(holder, it) } } } if (itemLongClickListener != null) { holder.itemView.setOnLongClickListener { getItem(holder.layoutPosition)?.let { itemLongClickListener?.invoke(holder, it) ?: true } ?: true } } holder } } protected abstract fun getViewBinding(parent: ViewGroup): VB final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {} @Suppress("UNCHECKED_CAST") final override fun onBindViewHolder( holder: ItemViewHolder, position: Int, payloads: MutableList ) { if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { getItem(holder.layoutPosition - getHeaderCount())?.let { convert(holder, (holder.binding as VB), it, payloads) } } } override fun onViewAttachedToWindow(holder: ItemViewHolder) { super.onViewAttachedToWindow(holder) if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { addAnimation(holder) } } override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) val manager = recyclerView.layoutManager if (manager is GridLayoutManager) { manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return getSpanSize(getItemViewType(position), position) } } } } private fun isHeader(position: Int) = position < getHeaderCount() private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount() private fun getActualPosition(position: Int) = position - getHeaderCount() private fun addAnimation(holder: ItemViewHolder) { itemAnimation?.let { if (it.itemAnimEnabled) { if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { startAnimation(holder, it) it.itemAnimStartPosition = holder.layoutPosition } } } } protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) { item.itemAnimation?.let { for (anim in it.getAnimators(holder.itemView)) { anim.setDuration(item.itemAnimDuration).start() anim.interpolator = item.itemAnimInterpolator } } } /** * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, * 使用getItem(holder.layoutPosition)来获取item */ abstract fun convert( holder: ItemViewHolder, binding: VB, item: ITEM, payloads: MutableList ) /** * 注册事件 */ abstract fun registerListener(holder: ItemViewHolder, binding: VB) companion object { private const val TYPE_HEADER_VIEW = Int.MIN_VALUE private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999 } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/AlphaInAnimation.kt ================================================ package com.kunfei.bookshelf.base.adapter.animations import android.animation.Animator import android.animation.ObjectAnimator import android.view.View class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : BaseAnimation { override fun getAnimators(view: View): Array = arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f)) companion object { private const val DEFAULT_ALPHA_FROM = 0f } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/BaseAnimation.kt ================================================ package com.kunfei.bookshelf.base.adapter.animations import android.animation.Animator import android.view.View /** * adapter item 动画 */ interface BaseAnimation { fun getAnimators(view: View): Array } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/ScaleInAnimation.kt ================================================ package com.kunfei.bookshelf.base.adapter.animations import android.animation.Animator import android.animation.ObjectAnimator import android.view.View class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : BaseAnimation { override fun getAnimators(view: View): Array { val scaleX = ObjectAnimator.ofFloat(view, "scaleX", mFrom, 1f) val scaleY = ObjectAnimator.ofFloat(view, "scaleY", mFrom, 1f) return arrayOf(scaleX, scaleY) } companion object { private const val DEFAULT_SCALE_FROM = .5f } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/SlideInBottomAnimation.kt ================================================ package com.kunfei.bookshelf.base.adapter.animations import android.animation.Animator import android.animation.ObjectAnimator import android.view.View class SlideInBottomAnimation : BaseAnimation { override fun getAnimators(view: View): Array = arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f)) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/SlideInLeftAnimation.kt ================================================ package com.kunfei.bookshelf.base.adapter.animations import android.animation.Animator import android.animation.ObjectAnimator import android.view.View class SlideInLeftAnimation : BaseAnimation { override fun getAnimators(view: View): Array = arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f)) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/SlideInRightAnimation.kt ================================================ package com.kunfei.bookshelf.base.adapter.animations import android.animation.Animator import android.animation.ObjectAnimator import android.view.View class SlideInRightAnimation : BaseAnimation { override fun getAnimators(view: View): Array = arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f)) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/observer/MyObserver.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.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() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/base/observer/MySingleObserver.java ================================================ package com.kunfei.bookshelf.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) { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BaseBookBean.java ================================================ package com.kunfei.bookshelf.bean; import java.util.Map; public interface BaseBookBean { String getTag(); String getNoteUrl(); void setNoteUrl(String noteUrl); String getVariable(); void setVariable(String variable); void putVariable(String key, String value); Map getVariableMap(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BaseChapterBean.java ================================================ package com.kunfei.bookshelf.bean; public interface BaseChapterBean { String getTag(); String getDurChapterUrl(); int getDurChapterIndex(); String getNoteUrl(); String getDurChapterName(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookChapterBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; import android.content.Context; import com.google.gson.Gson; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.help.BookshelfHelp; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; import java.util.Objects; /** * 章节列表 */ @Entity public class BookChapterBean implements Cloneable, BaseChapterBean { private String tag; private String noteUrl; //对应BookInfoBean noteUrl; private int durChapterIndex; //当前章节数 @Id private String durChapterUrl; //当前章节对应的文章地址 private String durChapterName; //当前章节名称 private boolean isVip; private boolean isPay; //章节内容在文章中的起始位置(本地) private Long start; //章节内容在文章中的终止位置(本地) private Long end; public BookChapterBean() { } @Generated(hash = 922679906) public BookChapterBean(String tag, String noteUrl, int durChapterIndex, String durChapterUrl, String durChapterName, boolean isVip, boolean isPay, Long start, Long end) { this.tag = tag; this.noteUrl = noteUrl; this.durChapterIndex = durChapterIndex; this.durChapterUrl = durChapterUrl; this.durChapterName = durChapterName; this.isVip = isVip; this.isPay = isPay; this.start = start; this.end = end; } public BookChapterBean(String tag, String durChapterName, String durChapterUrl) { this.tag = tag; this.durChapterName = durChapterName; this.durChapterUrl = durChapterUrl; } public BookChapterBean(String tag, String durChapterName, String durChapterUrl, boolean isVip, boolean isPay) { this.tag = tag; this.durChapterName = durChapterName; this.durChapterUrl = durChapterUrl; this.isVip = isVip; this.isPay = isPay; } @Override protected Object clone() { try { Gson gson = new Gson(); String json = gson.toJson(this); return gson.fromJson(json, BookChapterBean.class); } catch (Exception ignored) { } return this; } @Override public boolean equals(Object obj) { if (obj instanceof BookChapterBean) { BookChapterBean bookChapterBean = (BookChapterBean) obj; return Objects.equals(bookChapterBean.durChapterUrl, durChapterUrl); } else { return false; } } @Override public int hashCode() { if (durChapterUrl == null) { return 0; } return durChapterUrl.hashCode(); } @Override public String getTag() { return this.tag; } public void setTag(String tag) { this.tag = tag; } @Override public String getDurChapterName() { return this.durChapterName; } public void setDurChapterName(String durChapterName) { this.durChapterName = durChapterName; } @Override public String getDurChapterUrl() { return this.durChapterUrl; } public void setDurChapterUrl(String durChapterUrl) { this.durChapterUrl = durChapterUrl; } @Override public int getDurChapterIndex() { return this.durChapterIndex; } public void setDurChapterIndex(int durChapterIndex) { this.durChapterIndex = durChapterIndex; } @Override public String getNoteUrl() { return this.noteUrl; } public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } public Long getStart() { return this.start; } public void setStart(Long start) { this.start = start; } public Long getEnd() { return this.end; } public void setEnd(Long end) { this.end = end; } public Boolean getHasCache(BookInfoBean bookInfoBean) { return BookshelfHelp.isChapterCached(bookInfoBean.getName(), tag, this, bookInfoBean.isAudio()); } public boolean getIsVip() { return this.isVip; } public void setIsVip(boolean isVip) { this.isVip = isVip; } public boolean getIsPay() { return this.isPay; } public void setIsPay(boolean isPay) { this.isPay = isPay; } public String getDisplayTitle(Context context) { if (!isVip) { return durChapterName; } if (isPay) { return context.getString(R.string.payed_title, durChapterName); } return context.getString(R.string.vip_title, durChapterName); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookContentBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; /** * 书本缓存内容 */ @Entity public class BookContentBean { private String noteUrl; //对应BookInfoBean noteUrl; @Id private String durChapterUrl; private Integer durChapterIndex; //当前章节 (包括番外) private String durChapterContent; //当前章节内容 private String tag; //来源 某个网站/本地 private Long timeMillis; public BookContentBean() { } @Generated(hash = 695554675) public BookContentBean(String noteUrl, String durChapterUrl, Integer durChapterIndex, String durChapterContent, String tag, Long timeMillis) { this.noteUrl = noteUrl; this.durChapterUrl = durChapterUrl; this.durChapterIndex = durChapterIndex; this.durChapterContent = durChapterContent; this.tag = tag; this.timeMillis = timeMillis; } public String getDurChapterUrl() { return durChapterUrl; } public void setDurChapterUrl(String durChapterUrl) { this.durChapterUrl = durChapterUrl; } public int getDurChapterIndex() { return durChapterIndex; } public void setDurChapterIndex(int durChapterIndex) { this.durChapterIndex = durChapterIndex; } public String getDurChapterContent() { return durChapterContent; } public void setDurChapterContent(String durChapterContent) { this.durChapterContent = durChapterContent; } public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public String getNoteUrl() { return this.noteUrl; } public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } public void setDurChapterIndex(Integer durChapterIndex) { this.durChapterIndex = durChapterIndex; } public Long getTimeMillis() { return this.timeMillis; } public void setTimeMillis(Long timeMillis) { this.timeMillis = timeMillis; } public boolean outTime() { if (timeMillis == null) { return true; } return timeMillis < System.currentTimeMillis(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookInfoBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.text.TextUtils; import com.google.gson.Gson; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.constant.BookType; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.FileHelp; import com.kunfei.bookshelf.utils.MD5Utils; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.widget.page.PageLoaderEpub; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.Transient; import java.io.File; import java.io.FileOutputStream; import java.util.Objects; /** * 书本信息 */ @Entity public class BookInfoBean implements Cloneable { private String name=""; //小说名。初始化为空字符串,以避免由于规则不完善,书名为空时,某些操作造成的一些异常(比如删除书名为空的书) private String tag; @Id private String noteUrl; //如果是来源网站,则小说根地址,如果是本地则是小说本地MD5 private String chapterUrl; //章节目录地址,本地目录正则 private long finalRefreshData; //章节最后更新时间 private String coverUrl; //小说封面 private String author="";//作者 private String introduce; //简介 private String origin; //来源 private String charset;//编码 private String bookSourceType; @Transient private String bookInfoHtml; @Transient private String chapterListHtml; public BookInfoBean() { } @Generated(hash = 906814482) public BookInfoBean(String name, String tag, String noteUrl, String chapterUrl, long finalRefreshData, String coverUrl, String author, String introduce, String origin, String charset, String bookSourceType) { this.name = name; this.tag = tag; this.noteUrl = noteUrl; this.chapterUrl = chapterUrl; this.finalRefreshData = finalRefreshData; this.coverUrl = coverUrl; this.author = author; this.introduce = introduce; this.origin = origin; this.charset = charset; this.bookSourceType = bookSourceType; } @Override protected Object clone() { try { Gson gson = new Gson(); String json = gson.toJson(this); return gson.fromJson(json, BookInfoBean.class); } catch (Exception ignored) { } return this; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public String getNoteUrl() { return noteUrl; } public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } public String getChapterUrl() { return chapterUrl; } public void setChapterUrl(String chapterUrl) { this.chapterUrl = chapterUrl; } public long getFinalRefreshData() { return finalRefreshData; } public void setFinalRefreshData(long finalRefreshData) { this.finalRefreshData = finalRefreshData; } public String getCoverUrl() { if (isEpub() && (TextUtils.isEmpty(coverUrl) || !(new File(coverUrl)).exists())) { extractEpubCoverImage(); return ""; } return coverUrl; } public void setCoverUrl(String coverUrl) { this.coverUrl = coverUrl; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = BookshelfHelp.formatAuthor(author); } public String getIntroduce() { return introduce; } public void setIntroduce(String introduce) { this.introduce = introduce; } public String getOrigin() { return TextUtils.isEmpty(origin) && tag.equals(BookShelfBean.LOCAL_TAG) ? StringUtils.getString(R.string.local) : origin; } public void setOrigin(String origin) { this.origin = origin; } public String getCharset() { return this.charset; } public void setCharset(String charset) { this.charset = charset; } private void extractEpubCoverImage() { AsyncTask.execute(new Runnable() { @Override public void run() { try { FileHelp.createFolderIfNotExists(coverUrl); Bitmap cover = BitmapFactory.decodeStream(Objects.requireNonNull(PageLoaderEpub.readBook(new File(noteUrl))).getCoverImage().getInputStream()); String md5Path = FileHelp.getCachePath() + File.separator + "cover" + File.separator + MD5Utils.strToMd5By16(noteUrl) + ".jpg"; FileOutputStream out = new FileOutputStream(new File(md5Path)); cover.compress(Bitmap.CompressFormat.JPEG, 90, out); out.flush(); out.close(); setCoverUrl(md5Path); DbHelper.getDaoSession().getBookInfoBeanDao().insertOrReplace(BookInfoBean.this); } catch (Exception ignored) { } } }); } private boolean isEpub() { return Objects.equals(tag, BookShelfBean.LOCAL_TAG) && noteUrl.toLowerCase().matches(".*\\.epub$"); } public String getBookSourceType() { return this.bookSourceType; } public void setBookSourceType(String bookSourceType) { this.bookSourceType = bookSourceType; } public boolean isAudio() { return Objects.equals(BookType.AUDIO, bookSourceType); } public String getBookInfoHtml() { return bookInfoHtml; } public void setBookInfoHtml(String bookInfoHtml) { this.bookInfoHtml = bookInfoHtml; } public String getChapterListHtml() { return chapterListHtml; } public void setChapterListHtml(String chapterListHtml) { this.chapterListHtml = chapterListHtml; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookKindBean.java ================================================ package com.kunfei.bookshelf.bean; import android.text.TextUtils; import com.kunfei.bookshelf.utils.StringUtils; import java.text.DecimalFormat; public class BookKindBean { private String wordsS; private String state; private String kind; public BookKindBean(String kindS) { if (TextUtils.isEmpty(kindS)) return; for (String kind : kindS.split("[,|\n]")) { if (StringUtils.isContainNumber(kind) && TextUtils.isEmpty(wordsS)) { if (StringUtils.isNumeric(kind)) { int words = Integer.parseInt(kind); if (words > 0) { wordsS = words + "字"; if (words > 10000) { DecimalFormat df = new DecimalFormat("#.#"); wordsS = df.format(words * 1.0f / 10000f) + "万字"; } } } else { wordsS = kind; } } else if (kind.matches(".*[连载|完结].*")) { state = kind; } else if (TextUtils.isEmpty(this.kind) && !TextUtils.isEmpty(kind)) { this.kind = kind; } else if (TextUtils.isEmpty(state) && !TextUtils.isEmpty(kind)) { state = kind; } else if (wordsS != null && state != null && this.kind != null) { break; } } } public String getWordsS() { return wordsS; } public String getState() { return state; } public String getKind() { return kind; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookShelfBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; import android.text.TextUtils; import com.google.gson.Gson; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.constant.BookType; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.Transient; import java.util.HashMap; import java.util.Map; import java.util.Objects; import static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING; /** * 书架item Bean */ @Entity public class BookShelfBean implements Cloneable, BaseBookBean { @Transient public static final String LOCAL_TAG = "loc_book"; @Transient private String errorMsg; @Transient private boolean isLoading; @Id private String noteUrl; //对应BookInfoBean noteUrl; private Integer durChapter = 0; //当前章节 (包括番外) private Integer durChapterPage = 0; // 当前章节位置 用页码 private Long finalDate = System.currentTimeMillis(); //最后阅读时间 private Boolean hasUpdate = false; //是否有更新 private Integer newChapters = 0; //更新章节数 private String tag; private Integer serialNumber = 0; //手动排序 private Long finalRefreshData = System.currentTimeMillis(); //章节最后更新时间 private Integer group = 0; private String durChapterName; private String lastChapterName; private Integer chapterListSize = 0; private String customCoverPath; private Boolean allowUpdate = true; private Boolean useReplaceRule = true; private String variable; private Boolean replaceEnable = MApplication.getConfigPreferences().getBoolean("replaceEnableDefault", true); @Transient private Map variableMap; @Transient private BookInfoBean bookInfoBean; public BookShelfBean() { } @Generated(hash = 451550884) public BookShelfBean(String noteUrl, Integer durChapter, Integer durChapterPage, Long finalDate, Boolean hasUpdate, Integer newChapters, String tag, Integer serialNumber, Long finalRefreshData, Integer group, String durChapterName, String lastChapterName, Integer chapterListSize, String customCoverPath, Boolean allowUpdate, Boolean useReplaceRule, String variable, Boolean replaceEnable) { this.noteUrl = noteUrl; this.durChapter = durChapter; this.durChapterPage = durChapterPage; this.finalDate = finalDate; this.hasUpdate = hasUpdate; this.newChapters = newChapters; this.tag = tag; this.serialNumber = serialNumber; this.finalRefreshData = finalRefreshData; this.group = group; this.durChapterName = durChapterName; this.lastChapterName = lastChapterName; this.chapterListSize = chapterListSize; this.customCoverPath = customCoverPath; this.allowUpdate = allowUpdate; this.useReplaceRule = useReplaceRule; this.variable = variable; this.replaceEnable = replaceEnable; } @Override public Object clone() { try { Gson gson = new Gson(); String json = gson.toJson(this); return gson.fromJson(json, BookShelfBean.class); } catch (Exception ignored) { } return this; } @Override public String getVariable() { return this.variable; } @Override public void setVariable(String variable) { this.variable = variable; } @Override public void putVariable(String key, String value) { if (variableMap == null) { variableMap = new HashMap<>(); } variableMap.put(key, value); variable = new Gson().toJson(variableMap); } @Override public Map getVariableMap() { if (variableMap == null && !TextUtils.isEmpty(variable)) { variableMap = new Gson().fromJson(variable, MAP_STRING); } return variableMap; } @Override public String getNoteUrl() { return noteUrl; } @Override public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } public int getDurChapter() { return durChapter < 0 ? 0 : durChapter; } public int getDurChapter(int chapterListSize) { if (durChapter < 0 | chapterListSize == 0) { return 0; } else if (durChapter >= chapterListSize) { return chapterListSize - 1; } return durChapter; } public int getDurChapterPage() { return durChapterPage < 0 ? 0 : durChapterPage; } public long getFinalDate() { return finalDate; } @Override public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public BookInfoBean getBookInfoBean() { if (bookInfoBean == null) { bookInfoBean = new BookInfoBean(); bookInfoBean.setNoteUrl(noteUrl); bookInfoBean.setTag(tag); } return bookInfoBean; } public void setBookInfoBean(BookInfoBean bookInfoBean) { this.bookInfoBean = bookInfoBean; } public boolean getHasUpdate() { return hasUpdate && !isAudio(); } public int getNewChapters() { return newChapters; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } public int getSerialNumber() { return this.serialNumber; } public long getFinalRefreshData() { return this.finalRefreshData; } public boolean isLoading() { return isLoading; } public void setLoading(boolean loading) { isLoading = loading; } public int getGroup() { return this.group == null ? 0 : this.group; } public void setDurChapter(Integer durChapter) { this.durChapter = durChapter; } public void setDurChapterPage(Integer durChapterPage) { this.durChapterPage = durChapterPage; } public void setFinalDate(Long finalDate) { this.finalDate = finalDate; } public void setHasUpdate(Boolean hasUpdate) { this.hasUpdate = hasUpdate; } public void setNewChapters(Integer newChapters) { this.newChapters = newChapters; } public void setSerialNumber(Integer serialNumber) { this.serialNumber = serialNumber; } public void setFinalRefreshData(Long finalRefreshData) { this.finalRefreshData = finalRefreshData; } public void setGroup(Integer group) { this.group = group; } public String getDurChapterName() { return this.durChapterName; } public void setDurChapterName(String durChapterName) { this.durChapterName = durChapterName; } public String getLastChapterName() { return this.lastChapterName; } public void setLastChapterName(String lastChapterName) { this.lastChapterName = lastChapterName; } public int getUnreadChapterNum() { int num = getChapterListSize() - getDurChapter() - 1; return Math.max(num, 0); } public int getChapterListSize() { return this.chapterListSize == null ? 0 : this.chapterListSize; } public void setChapterListSize(Integer chapterListSize) { this.chapterListSize = chapterListSize; } public String getCustomCoverPath() { return this.customCoverPath; } public String getCoverPath() { if (TextUtils.isEmpty(customCoverPath)) { return bookInfoBean.getCoverUrl(); } else { return this.customCoverPath; } } public void setCustomCoverPath(String customCoverPath) { this.customCoverPath = customCoverPath; } public Boolean getAllowUpdate() { return allowUpdate == null ? true : allowUpdate; } public void setAllowUpdate(Boolean allowUpdate) { this.allowUpdate = allowUpdate; } public Boolean getUseReplaceRule() { return this.useReplaceRule; } public void setUseReplaceRule(Boolean useReplaceRule) { this.useReplaceRule = useReplaceRule; } public boolean isAudio() { return Objects.equals(bookInfoBean.getBookSourceType(), BookType.AUDIO); } public Boolean getReplaceEnable() { return replaceEnable == null ? MApplication.getConfigPreferences().getBoolean("replaceEnableDefault", true) : replaceEnable; } public String getName() { return bookInfoBean.getName(); } public String getAuthor() { return bookInfoBean.getAuthor(); } public void setReplaceEnable(Boolean replaceEnable) { this.replaceEnable = replaceEnable; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookSource3Bean.java ================================================ package com.kunfei.bookshelf.bean; import com.google.gson.Gson; import java.util.ArrayList; import java.util.List; import kotlin.jvm.Transient; // 解析阅读3.0的书源规则,toBookSourceBean()方法转换为阅读2.0书源规则 // https://raw.githubusercontent.com/gedoor/legado/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt public class BookSource3Bean { private String bookSourceName = ""; // 名称 private String bookSourceGroup; // 分组 private String bookSourceUrl = ""; // 地址,包括 http/https private int bookSourceType = 0; // 类型,0 文本,1 音频 private String bookUrlPattern; // 详情页url正则 private int customOrder = 0; // 手动排序编号 private Boolean enabled = true; // 是否启用 private Boolean enabledExplore = true; // 启用发现 private String header; // 请求头 private String loginUrl; // 登录地址 private String loginUi; private String loginCheckJs; private String bookSourceComment; // 注释 private Long lastUpdateTime = 0L; // 最后更新时间,用于排序 private int weight = 0; // 智能排序的权重 private String exploreUrl; // 发现url private ExploreRule ruleExplore; // 发现规则 private String searchUrl; // 搜索url private SearchRule ruleSearch; // 搜索规则 private BookInfoRule ruleBookInfo; // 书籍信息页规则 private TocRule ruleToc; // 目录页规则 private ContentRule ruleContent; // 正文页规则 @Transient private String userAgent; @Transient private String RuleSearchUrl; class ContentRule { String content; String nextContentUrl; String webJs; String sourceRegex; String replaceRegex; String imageStyle; //默认大小居中,FULL最大宽度 String actions; } class TocRule { String chapterList; String chapterName; String chapterUrl; String isVip; String isPay; String updateTime; String nextTocUrl; } class BookInfoRule { String init; String name; String author; String intro; String kind; String lastChapter; String updateTime; String coverUrl; String tocUrl; String wordCount; } class SearchRule { String bookList; String name; String author; String intro; String kind; String lastChapter; String updateTime; String bookUrl; String coverUrl; String wordCount; } class ExploreRule { String bookList; String name; String author; String intro; String kind; String lastChapter; String updateTime; String bookUrl; String coverUrl; String wordCount; } /* @Override public Object clone() { try { Gson gson = new Gson(); String json = gson.toJson(this); return gson.fromJson(json, BookSource3Bean.class); } catch (Exception ignored) { } return this; }*/ // 给书源增加一个标签 public BookSource3Bean addGroupTag(String tag) { if (this.bookSourceGroup != null) { //为了避免空格、首尾位置的差异造成影响,这里做循环处理 String[] tags = (this.bookSourceGroup + ";" + tag).split(";"); List list = new ArrayList<>(); list.add(tag); for (String s : tags) { if (!list.contains(s)) { list.add(s); } } bookSourceGroup = tag; for (int i = 1; i < list.size(); i++) { bookSourceGroup = bookSourceGroup + ";" + list.get(i); } } return this; } class httpRequest { String method; String body; String headers; String charset; } private String searchUrl2RuleSearchUrl(String searchUrl) { String RuleSearchUrl = searchUrl; if (searchUrl != null) { String q = ""; if (searchUrl.replaceAll("\\s", "").matches("^[^,]+,\\{.+\\}")) { // 正常网址不包含逗号 String[] strings = searchUrl.split(",", 2); try { Gson gson = new Gson(); httpRequest request = gson.fromJson(strings[1], httpRequest.class); if (gson.toJson(request).replaceAll("\\s", "").length() > 0) { // 阅读2.0没有单独的header,只有useragent if (request.headers != null) { if (this.header == null) this.header = request.headers; else if (request.headers.trim().length() < 1) this.header = request.headers; } if (request.charset != null) { if (request.charset.trim().length() > 0) q = q + "|char=" + request.charset; } if (request.body != null) { q = request.body .replace("{{key}}", "searchKey") .replaceFirst("\\{\\{([^{}]*)page([^{}]*)\\}\\}", "$1searchPage$2") + q; // post请求的关键词一定在第二部分 if (request.method != null) { if (request.method.toLowerCase().contains("post")) q = "@" + q; else q = "?" + q; } return strings[0] + q; } RuleSearchUrl = strings[0] + q; } else RuleSearchUrl = searchUrl; } catch (Exception e) { e.printStackTrace(); RuleSearchUrl = searchUrl; } } return RuleSearchUrl.replaceAll("\\s", "") .replace("{{key}}", "searchKey") .replaceFirst("\\{\\{([^{}]*)page([^{}]*)\\}\\}", "$1searchPage$2") ; } return null; } public BookSourceBean toBookSourceBean() { // 带注释的行,表示2.0/3.0书源json的数据命名不同。注释后方为2.0名称 String bookSourceType = ""; if (this.bookSourceType != 0) bookSourceType = "" + this.bookSourceType; RuleSearchUrl = searchUrl2RuleSearchUrl(searchUrl); if (header != null && userAgent == null) { if (header.matches("(?!).*(User-Agent).*")) userAgent = header.replaceFirst("(?!).*(User-Agent)[\\s:]+\"([^\"]+)\".*", "$2"); } String ruleFindUrl=null; if(exploreUrl!=null){ ruleFindUrl=exploreUrl.replaceAll("\\{\\{page\\}\\}","searchPage"); } // 暂时只给发现和搜索添加了header String header=""; if(this.header!=null){ if(this.header.trim().length()>0) header="@Header:"+this.header.replaceAll("\\n"," "); } return new BookSourceBean( bookSourceUrl, bookSourceName, bookSourceGroup, bookSourceType, userAgent, // httpUserAgent loginUrl, loginUi, loginCheckJs, lastUpdateTime, 0, //u serialNumber, weight, true, //u enable, ruleFindUrl + header,//发现规则 ruleFindUrl, ruleExplore.bookList, // 列表 ruleFindList, ruleExplore.name,// ruleFindName, ruleExplore.author,// ruleFindAuthor, ruleExplore.kind,// ruleFindKind, ruleExplore.intro,// ruleFindIntroduce, ruleExplore.lastChapter,// ruleFindLastChapter, ruleExplore.coverUrl,// ruleFindCoverUrl, ruleExplore.bookUrl,//??? ruleFindNoteUrl, RuleSearchUrl+header,// ruleSearchUrl, ruleSearch.bookList,// ruleSearchList, ruleSearch.name,// ruleSearchName, ruleSearch.author,// ruleSearchAuthor, ruleSearch.kind,// ruleSearchKind, ruleSearch.intro,// ruleSearchIntroduce, ruleSearch.lastChapter,// ruleSearchLastChapter, ruleSearch.coverUrl,//ruleSearchCoverUrl, ruleSearch.bookUrl,// ruleSearchNoteUrl, bookUrlPattern, //??? ruleBookUrlPattern, ruleBookInfo.init,// ruleBookInfoInit, ruleBookInfo.name,// ruleBookName, ruleBookInfo.author,// ruleBookAuthor, ruleBookInfo.coverUrl,// ruleCoverUrl, ruleBookInfo.intro,// ruleIntroduce, ruleBookInfo.kind,// ruleBookKind, ruleBookInfo.lastChapter,// ruleBookLastChapter, ruleBookInfo.tocUrl,// ruleChapterUrl, ruleToc.nextTocUrl,// ruleChapterUrlNext, ruleToc.chapterList,// ruleChapterList, ruleToc.chapterName,// ruleChapterName, ruleToc.chapterUrl, // ruleContentUrl, ruleToc.isVip, ruleToc.isPay, ruleContent.nextContentUrl, //ruleContentUrlNext, ruleContent.content, // ruleBookContent, ruleContent.replaceRegex,// ruleBookContentReplace, ruleContent.actions ); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookSourceBean.java ================================================ package com.kunfei.bookshelf.bean; import static android.text.TextUtils.isEmpty; import static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING; import static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE; import android.text.TextUtils; import android.util.Pair; import com.google.gson.Gson; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.help.JsExtensions; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders; import com.kunfei.bookshelf.utils.ACache; import com.kunfei.bookshelf.utils.StringUtils; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.NotNull; import org.greenrobot.greendao.annotation.OrderBy; import org.greenrobot.greendao.annotation.Transient; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.script.SimpleBindings; /** * Created by GKF on 2017/12/14. * 书源信息 */ @SuppressWarnings("unused") @Entity public class BookSourceBean implements Cloneable, JsExtensions { @Id private String bookSourceUrl; private String bookSourceName; private String bookSourceGroup; private String bookSourceType; private String httpUserAgent; private String loginUrl; private String loginUi; private String loginCheckJs; private Long lastUpdateTime; @OrderBy private int serialNumber; @OrderBy @NotNull private int weight = 0; private boolean enable; //发现规则 private String ruleFindUrl; private String ruleFindList; private String ruleFindName; private String ruleFindAuthor; private String ruleFindKind; private String ruleFindIntroduce; private String ruleFindLastChapter; private String ruleFindCoverUrl; private String ruleFindNoteUrl; //搜索规则 private String ruleSearchUrl; private String ruleSearchList; private String ruleSearchName; private String ruleSearchAuthor; private String ruleSearchKind; private String ruleSearchIntroduce; private String ruleSearchLastChapter; private String ruleSearchCoverUrl; private String ruleSearchNoteUrl; //详情页规则 private String ruleBookUrlPattern; private String ruleBookInfoInit; private String ruleBookName; private String ruleBookAuthor; private String ruleCoverUrl; private String ruleIntroduce; private String ruleBookKind; private String ruleBookLastChapter; private String ruleChapterUrl; //目录页规则 private String ruleChapterUrlNext; private String ruleChapterList; private String ruleChapterName; private String ruleContentUrl; private String ruleChapterVip; private String ruleChapterPay; //正文页规则 private String ruleContentUrlNext; private String ruleBookContent; private String ruleBookContentReplace; private String payAction; @Transient private transient ArrayList groupList; @Generated(hash = 1152082575) public BookSourceBean(String bookSourceUrl, String bookSourceName, String bookSourceGroup, String bookSourceType, String httpUserAgent, String loginUrl, String loginUi, String loginCheckJs, Long lastUpdateTime, int serialNumber, int weight, boolean enable, String ruleFindUrl, String ruleFindList, String ruleFindName, String ruleFindAuthor, String ruleFindKind, String ruleFindIntroduce, String ruleFindLastChapter, String ruleFindCoverUrl, String ruleFindNoteUrl, String ruleSearchUrl, String ruleSearchList, String ruleSearchName, String ruleSearchAuthor, String ruleSearchKind, String ruleSearchIntroduce, String ruleSearchLastChapter, String ruleSearchCoverUrl, String ruleSearchNoteUrl, String ruleBookUrlPattern, String ruleBookInfoInit, String ruleBookName, String ruleBookAuthor, String ruleCoverUrl, String ruleIntroduce, String ruleBookKind, String ruleBookLastChapter, String ruleChapterUrl, String ruleChapterUrlNext, String ruleChapterList, String ruleChapterName, String ruleContentUrl, String ruleChapterVip, String ruleChapterPay, String ruleContentUrlNext, String ruleBookContent, String ruleBookContentReplace, String payAction) { this.bookSourceUrl = bookSourceUrl; this.bookSourceName = bookSourceName; this.bookSourceGroup = bookSourceGroup; this.bookSourceType = bookSourceType; this.httpUserAgent = httpUserAgent; this.loginUrl = loginUrl; this.loginUi = loginUi; this.loginCheckJs = loginCheckJs; this.lastUpdateTime = lastUpdateTime; this.serialNumber = serialNumber; this.weight = weight; this.enable = enable; this.ruleFindUrl = ruleFindUrl; this.ruleFindList = ruleFindList; this.ruleFindName = ruleFindName; this.ruleFindAuthor = ruleFindAuthor; this.ruleFindKind = ruleFindKind; this.ruleFindIntroduce = ruleFindIntroduce; this.ruleFindLastChapter = ruleFindLastChapter; this.ruleFindCoverUrl = ruleFindCoverUrl; this.ruleFindNoteUrl = ruleFindNoteUrl; this.ruleSearchUrl = ruleSearchUrl; this.ruleSearchList = ruleSearchList; this.ruleSearchName = ruleSearchName; this.ruleSearchAuthor = ruleSearchAuthor; this.ruleSearchKind = ruleSearchKind; this.ruleSearchIntroduce = ruleSearchIntroduce; this.ruleSearchLastChapter = ruleSearchLastChapter; this.ruleSearchCoverUrl = ruleSearchCoverUrl; this.ruleSearchNoteUrl = ruleSearchNoteUrl; this.ruleBookUrlPattern = ruleBookUrlPattern; this.ruleBookInfoInit = ruleBookInfoInit; this.ruleBookName = ruleBookName; this.ruleBookAuthor = ruleBookAuthor; this.ruleCoverUrl = ruleCoverUrl; this.ruleIntroduce = ruleIntroduce; this.ruleBookKind = ruleBookKind; this.ruleBookLastChapter = ruleBookLastChapter; this.ruleChapterUrl = ruleChapterUrl; this.ruleChapterUrlNext = ruleChapterUrlNext; this.ruleChapterList = ruleChapterList; this.ruleChapterName = ruleChapterName; this.ruleContentUrl = ruleContentUrl; this.ruleChapterVip = ruleChapterVip; this.ruleChapterPay = ruleChapterPay; this.ruleContentUrlNext = ruleContentUrlNext; this.ruleBookContent = ruleBookContent; this.ruleBookContentReplace = ruleBookContentReplace; this.payAction = payAction; } public BookSourceBean(BookSourceBean sourceBean) { this.bookSourceUrl = sourceBean.bookSourceUrl; this.bookSourceName = sourceBean.bookSourceName; this.bookSourceGroup = sourceBean.bookSourceGroup; this.bookSourceType = sourceBean.bookSourceType; this.loginUrl = sourceBean.loginUrl; this.lastUpdateTime = sourceBean.lastUpdateTime; this.serialNumber = sourceBean.serialNumber; this.weight = sourceBean.weight; this.enable = sourceBean.enable; this.ruleFindUrl = sourceBean.ruleFindUrl; this.ruleFindList = sourceBean.ruleFindList; this.ruleFindName = sourceBean.ruleFindName; this.ruleFindAuthor = sourceBean.ruleFindAuthor; this.ruleFindKind = sourceBean.ruleFindKind; this.ruleFindIntroduce = sourceBean.ruleFindIntroduce; this.ruleFindLastChapter = sourceBean.ruleFindLastChapter; this.ruleFindCoverUrl = sourceBean.ruleFindCoverUrl; this.ruleFindNoteUrl = sourceBean.ruleFindNoteUrl; this.ruleSearchUrl = sourceBean.ruleSearchUrl; this.ruleSearchList = sourceBean.ruleSearchList; this.ruleSearchName = sourceBean.ruleSearchName; this.ruleSearchAuthor = sourceBean.ruleSearchAuthor; this.ruleSearchKind = sourceBean.ruleSearchKind; this.ruleSearchIntroduce = sourceBean.ruleSearchIntroduce; this.ruleSearchLastChapter = sourceBean.ruleSearchLastChapter; this.ruleSearchCoverUrl = sourceBean.ruleSearchCoverUrl; this.ruleSearchNoteUrl = sourceBean.ruleSearchNoteUrl; this.ruleBookUrlPattern = sourceBean.ruleBookUrlPattern; this.ruleBookInfoInit = sourceBean.ruleBookInfoInit; this.ruleBookName = sourceBean.ruleBookName; this.ruleBookAuthor = sourceBean.ruleBookAuthor; this.ruleCoverUrl = sourceBean.ruleCoverUrl; this.ruleIntroduce = sourceBean.ruleIntroduce; this.ruleBookKind = sourceBean.ruleBookKind; this.ruleBookLastChapter = sourceBean.ruleBookLastChapter; this.ruleChapterUrl = sourceBean.ruleChapterUrl; this.ruleChapterUrlNext = sourceBean.ruleChapterUrlNext; this.ruleChapterList = sourceBean.ruleChapterList; this.ruleChapterName = sourceBean.ruleChapterName; this.ruleContentUrl = sourceBean.ruleContentUrl; this.ruleContentUrlNext = sourceBean.ruleContentUrlNext; this.ruleBookContent = sourceBean.ruleBookContent; this.ruleBookContentReplace = sourceBean.ruleBookContentReplace; this.httpUserAgent = sourceBean.httpUserAgent; } public BookSourceBean() { } @Override public boolean equals(Object obj) { if (obj instanceof BookSourceBean) { BookSourceBean bs = (BookSourceBean) obj; return stringEquals(bookSourceUrl, bs.bookSourceUrl) && stringEquals(bookSourceName, bs.bookSourceName) && stringEquals(bookSourceType, bs.bookSourceType) && stringEquals(loginUrl, bs.loginUrl) && stringEquals(bookSourceGroup, bs.bookSourceGroup) && stringEquals(ruleBookName, bs.ruleBookName) && stringEquals(ruleBookAuthor, bs.ruleBookAuthor) && stringEquals(ruleChapterUrl, bs.ruleChapterUrl) && stringEquals(ruleChapterUrlNext, ruleChapterUrlNext) && stringEquals(ruleCoverUrl, bs.ruleCoverUrl) && stringEquals(ruleIntroduce, bs.ruleIntroduce) && stringEquals(ruleChapterList, bs.ruleChapterList) && stringEquals(ruleChapterName, bs.ruleChapterName) && stringEquals(ruleContentUrl, bs.ruleContentUrl) && stringEquals(ruleContentUrlNext, bs.ruleContentUrlNext) && stringEquals(ruleBookContent, bs.ruleBookContent) && stringEquals(ruleSearchUrl, bs.ruleSearchUrl) && stringEquals(ruleSearchList, bs.ruleSearchList) && stringEquals(ruleSearchName, bs.ruleSearchName) && stringEquals(ruleSearchAuthor, bs.ruleSearchAuthor) && stringEquals(ruleSearchKind, bs.ruleSearchKind) && stringEquals(ruleSearchLastChapter, bs.ruleSearchLastChapter) && stringEquals(ruleSearchCoverUrl, bs.ruleSearchCoverUrl) && stringEquals(ruleSearchNoteUrl, bs.ruleSearchNoteUrl) && stringEquals(httpUserAgent, bs.httpUserAgent) && stringEquals(ruleBookKind, bs.ruleBookKind) && stringEquals(ruleBookLastChapter, bs.ruleBookLastChapter) && stringEquals(ruleBookUrlPattern, bs.ruleBookUrlPattern) && stringEquals(ruleBookContentReplace, bs.ruleBookContentReplace); } return false; } private Boolean stringEquals(String str1, String str2) { return Objects.equals(str1, str2) || (isEmpty(str1) && isEmpty(str2)); } @Override public Object clone() { try { Gson gson = new Gson(); String json = gson.toJson(this); return gson.fromJson(json, BookSourceBean.class); } catch (Exception ignored) { } return this; } public String getBookSourceName() { return bookSourceName; } public void setBookSourceName(String bookSourceName) { this.bookSourceName = bookSourceName; } public String getBookSourceUrl() { return bookSourceUrl; } public void setBookSourceUrl(String bookSourceUrl) { this.bookSourceUrl = bookSourceUrl; } public int getSerialNumber() { return this.serialNumber; } public void setSerialNumber(int serialNumber) { this.serialNumber = serialNumber; } public int getWeight() { return this.weight; } public void setWeight(int weight) { this.weight = weight; } // 换源时选择的源权重+500 public void increaseWeightBySelection() { this.weight += 500; } public void increaseWeight(int increase) { this.weight += increase; } public boolean getEnable() { return this.enable; } public void setEnable(boolean enable) { this.enable = enable; } public String getRuleBookName() { return this.ruleBookName; } public void setRuleBookName(String ruleBookName) { this.ruleBookName = ruleBookName; } public String getRuleBookAuthor() { return this.ruleBookAuthor; } public void setRuleBookAuthor(String ruleBookAuthor) { this.ruleBookAuthor = ruleBookAuthor; } public String getRuleChapterUrl() { return this.ruleChapterUrl; } public void setRuleChapterUrl(String ruleChapterUrl) { this.ruleChapterUrl = ruleChapterUrl; } public String getRuleCoverUrl() { return this.ruleCoverUrl; } public void setRuleCoverUrl(String ruleCoverUrl) { this.ruleCoverUrl = ruleCoverUrl; } public String getRuleIntroduce() { return this.ruleIntroduce; } public void setRuleIntroduce(String ruleIntroduce) { this.ruleIntroduce = ruleIntroduce; } public String getRuleBookContent() { return this.ruleBookContent; } public void setRuleBookContent(String ruleBookContent) { this.ruleBookContent = ruleBookContent; } public String getRuleSearchUrl() { return this.ruleSearchUrl; } public void setRuleSearchUrl(String ruleSearchUrl) { this.ruleSearchUrl = ruleSearchUrl; } public String getRuleContentUrl() { return this.ruleContentUrl; } public void setRuleContentUrl(String ruleContentUrl) { this.ruleContentUrl = ruleContentUrl; } public String getRuleSearchName() { return this.ruleSearchName; } public void setRuleSearchName(String ruleSearchName) { this.ruleSearchName = ruleSearchName; } public String getRuleSearchAuthor() { return this.ruleSearchAuthor; } public void setRuleSearchAuthor(String ruleSearchAuthor) { this.ruleSearchAuthor = ruleSearchAuthor; } public String getRuleSearchKind() { return this.ruleSearchKind; } public void setRuleSearchKind(String ruleSearchKind) { this.ruleSearchKind = ruleSearchKind; } public String getRuleSearchLastChapter() { return this.ruleSearchLastChapter; } public void setRuleSearchLastChapter(String ruleSearchLastChapter) { this.ruleSearchLastChapter = ruleSearchLastChapter; } public String getRuleSearchCoverUrl() { return this.ruleSearchCoverUrl; } public void setRuleSearchCoverUrl(String ruleSearchCoverUrl) { this.ruleSearchCoverUrl = ruleSearchCoverUrl; } public String getRuleSearchNoteUrl() { return this.ruleSearchNoteUrl; } public void setRuleSearchNoteUrl(String ruleSearchNoteUrl) { this.ruleSearchNoteUrl = ruleSearchNoteUrl; } public String getRuleSearchList() { return this.ruleSearchList; } public void setRuleSearchList(String ruleSearchList) { this.ruleSearchList = ruleSearchList; } public String getRuleChapterList() { return this.ruleChapterList; } public void setRuleChapterList(String ruleChapterList) { this.ruleChapterList = ruleChapterList; } public String getRuleChapterName() { return this.ruleChapterName; } public void setRuleChapterName(String ruleChapterName) { this.ruleChapterName = ruleChapterName; } public String getHttpUserAgent() { return this.httpUserAgent; } public void setHttpUserAgent(String httpHeaders) { this.httpUserAgent = httpHeaders; } public String getRuleFindUrl() { return this.ruleFindUrl; } public void setRuleFindUrl(String ruleFindUrl) { this.ruleFindUrl = ruleFindUrl; } public String getBookSourceGroup() { return this.bookSourceGroup; } public void setBookSourceGroup(String bookSourceGroup) { this.bookSourceGroup = bookSourceGroup; upGroupList(); this.bookSourceGroup = TextUtils.join("; ", groupList); } public String getRuleChapterUrlNext() { return this.ruleChapterUrlNext; } public void setRuleChapterUrlNext(String ruleChapterUrlNext) { this.ruleChapterUrlNext = ruleChapterUrlNext; } public String getRuleContentUrlNext() { return this.ruleContentUrlNext; } public void setRuleContentUrlNext(String ruleContentUrlNext) { this.ruleContentUrlNext = ruleContentUrlNext; } public String getRuleBookUrlPattern() { return ruleBookUrlPattern; } public void setRuleBookUrlPattern(String ruleBookUrlPattern) { this.ruleBookUrlPattern = ruleBookUrlPattern; } public String getRuleBookKind() { return ruleBookKind; } public void setRuleBookKind(String ruleBookKind) { this.ruleBookKind = ruleBookKind; } public String getRuleBookLastChapter() { return ruleBookLastChapter; } public void setRuleBookLastChapter(String ruleBookLastChapter) { this.ruleBookLastChapter = ruleBookLastChapter; } private void upGroupList() { if (groupList == null) groupList = new ArrayList<>(); else groupList.clear(); if (!TextUtils.isEmpty(bookSourceGroup)) { for (String group : bookSourceGroup.split("\\s*[,;,;]\\s*")) { group = group.trim(); if (TextUtils.isEmpty(group) || groupList.contains(group)) continue; groupList.add(group); } } } public void addGroup(String group) { if (groupList == null) upGroupList(); if (!groupList.contains(group)) { groupList.add(group); updateModTime(); bookSourceGroup = TextUtils.join("; ", groupList); } } public void removeGroup(String group) { if (groupList == null) upGroupList(); if (groupList.contains(group)) { groupList.remove(group); updateModTime(); bookSourceGroup = TextUtils.join("; ", groupList); } } public boolean containsGroup(String group) { if (groupList == null) { upGroupList(); } return groupList.contains(group); } public Long getLastUpdateTime() { return lastUpdateTime; } public void setLastUpdateTime(Long lastUpdateTime) { this.lastUpdateTime = lastUpdateTime; } public void updateModTime() { this.lastUpdateTime = System.currentTimeMillis(); } public String getLoginUrl() { return this.loginUrl; } public void setLoginUrl(String loginUrl) { this.loginUrl = loginUrl; } public String getBookSourceType() { return bookSourceType == null ? "" : bookSourceType; } public void setBookSourceType(String bookSourceType) { this.bookSourceType = bookSourceType; } public String getRuleSearchIntroduce() { return this.ruleSearchIntroduce; } public void setRuleSearchIntroduce(String ruleSearchIntroduce) { this.ruleSearchIntroduce = ruleSearchIntroduce; } public String getRuleFindList() { return this.ruleFindList; } public void setRuleFindList(String ruleFindList) { this.ruleFindList = ruleFindList; } public String getRuleFindName() { return this.ruleFindName; } public void setRuleFindName(String ruleFindName) { this.ruleFindName = ruleFindName; } public String getRuleFindAuthor() { return this.ruleFindAuthor; } public void setRuleFindAuthor(String ruleFindAuthor) { this.ruleFindAuthor = ruleFindAuthor; } public String getRuleFindKind() { return this.ruleFindKind; } public void setRuleFindKind(String ruleFindKind) { this.ruleFindKind = ruleFindKind; } public String getRuleFindIntroduce() { return this.ruleFindIntroduce; } public void setRuleFindIntroduce(String ruleFindIntroduce) { this.ruleFindIntroduce = ruleFindIntroduce; } public String getRuleFindLastChapter() { return this.ruleFindLastChapter; } public void setRuleFindLastChapter(String ruleFindLastChapter) { this.ruleFindLastChapter = ruleFindLastChapter; } public String getRuleFindCoverUrl() { return this.ruleFindCoverUrl; } public void setRuleFindCoverUrl(String ruleFindCoverUrl) { this.ruleFindCoverUrl = ruleFindCoverUrl; } public String getRuleFindNoteUrl() { return this.ruleFindNoteUrl; } public void setRuleFindNoteUrl(String ruleFindNoteUrl) { this.ruleFindNoteUrl = ruleFindNoteUrl; } public String getRuleBookInfoInit() { return this.ruleBookInfoInit; } public void setRuleBookInfoInit(String ruleBookInfoInit) { this.ruleBookInfoInit = ruleBookInfoInit; } public String getRuleBookContentReplace() { return ruleBookContentReplace; } public void setRuleBookContentReplace(String ruleBookContentReplace) { this.ruleBookContentReplace = ruleBookContentReplace; } public String getJson(int maxLength) { try { String source = getMinJson(false); // 优先直接输出 if (source.getBytes("utf-8").length <= maxLength) return source; source = StringUtils.zipString(source .replaceFirst("^\\{", "") // 保留末位的括号,用于解压缩后的验证 .replaceFirst("(\\s|\n)*\\}$", "}") .trim()); // 优先输出带发现的书源 if (source.getBytes("utf-8").length < maxLength) return "{" + source; // 去除发现 return "{" + StringUtils.zipString(getMinJson(true) .replaceFirst("^\\{", "") // 保留末位的括号,用于解压缩后的验证 .replaceFirst("(\\s|\n)*\\}$", "}") .trim()); } catch (Exception e) { e.printStackTrace(); } return ""; } /** * 把书源bean转为json,并去除不必要的信息。 * * @param removeFind false,去除空信息和排序、可用性信息。true,额外去除发现规则 * @return */ public String getMinJson(Boolean removeFind) { BookSourceBean bean = new BookSourceBean(this); String json; if (removeFind) { bean.setRuleFindUrl(null); bean.setRuleFindList(null); bean.setRuleFindName(null); bean.setRuleFindAuthor(null); bean.setRuleFindKind(null); bean.setRuleFindIntroduce(null); bean.setRuleFindLastChapter(null); bean.setRuleFindCoverUrl(null); bean.setRuleFindNoteUrl(null); } try { Gson gson = new Gson(); json = gson.toJson(bean); return json.replaceFirst("\n\\s*\"enable\":\\s*\\S+(,)?\\s*", "\n") .replaceFirst("\n\\s*\"serialNumber\":\\s*\\d+(,)?\\s*", "\n") .replaceFirst("\n\\s*\"\"weight\":\\s*\\d+(,)?\\s*", "\n") .replaceAll("\n\\s*\"[a-zA-Z]+\"(:\"\"|: \"\"| :\"\"| : \"\")\\s*,\\s*\n", "\n") .replaceAll("\\s*\n\\s*", "") ; } catch (Exception ignored) { } return ""; } public String getLoginUi() { return loginUi; } public void setLoginUi(String loginUi) { this.loginUi = loginUi; } public String getLoginCheckJs() { return loginCheckJs; } public void setLoginCheckJs(String loginCheckJs) { this.loginCheckJs = loginCheckJs; } /** * 执行JS */ public Object evalJS(String jsStr) throws Exception { try { SimpleBindings bindings = new SimpleBindings(); bindings.put("java", this); bindings.put("source", this); bindings.put("baseUrl", bookSourceUrl); return SCRIPT_ENGINE.eval(jsStr, bindings); } catch (Exception e) { e.printStackTrace(); return e.getLocalizedMessage(); } } public Map getHeaderMap(Boolean hasLoginHeader) { Map headerMap = new HashMap<>(); String headers = getHttpUserAgent(); if (!isEmpty(headers)) { if (StringUtils.isJsonObject(headers)) { Map map = new Gson().fromJson(headers, MAP_STRING); headerMap.putAll(map); } else { headerMap.put("User-Agent", headers); } } else { headerMap.put("User-Agent", AnalyzeHeaders.getDefaultUserAgent()); } CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load(bookSourceUrl); if (cookie != null) { headerMap.put("Cookie", cookie.getCookie()); } if (hasLoginHeader) { headerMap.putAll(getLoginHeaderMap()); } return headerMap; } /** * @return 登录头, Map格式 */ public Map getLoginHeaderMap() { Map headerMap = new HashMap<>(); String header = getLoginHeader(); if (header != null) { Map map = new Gson().fromJson(header, MAP_STRING); if (map != null) { headerMap.putAll(map); } } return headerMap; } public String getLoginHeader() { CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load("loginHeader_" + bookSourceUrl); if (cookie == null) { return null; } return cookie.getCookie(); } public void putLoginHeader(String value) { CookieBean cookie = new CookieBean("loginHeader_" + bookSourceUrl, value); DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(cookie); } public String getLoginInfo() { CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load("loginInfo_" + bookSourceUrl); if (cookie == null) { return null; } return cookie.getCookie(); } /** * @return 用户登录信息 */ public Map getLoginInfoMap() { String info = getLoginInfo(); if (info != null) { return new Gson().fromJson(info, MAP_STRING); } return null; } public void putLoginInfo(Map info) { String json = new Gson().toJson(info); CookieBean cookieBean = new CookieBean("loginInfo_" + bookSourceUrl, json); DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(cookieBean); } public Pair> getFindList() { String findError = "发现规则语法错误"; ACache aCache = ACache.get(MApplication.getInstance(), "findCache"); try { String[] kindA; String findRule; if (!TextUtils.isEmpty(getRuleFindUrl()) && !containsGroup(findError)) { boolean isJsAndCache = getRuleFindUrl().startsWith("") || getRuleFindUrl().startsWith("@js:"); if (isJsAndCache) { findRule = aCache.getAsString(getBookSourceUrl()); if (TextUtils.isEmpty(findRule)) { String jsStr; if (getRuleFindUrl().startsWith("")) { jsStr = getRuleFindUrl().substring(4, getRuleFindUrl().lastIndexOf("<")); } else { jsStr = getRuleFindUrl().substring(4); } findRule = evalJS(jsStr).toString(); } else { isJsAndCache = false; } } else { findRule = getRuleFindUrl(); } kindA = findRule.split("(&&|\n)+"); List children = new ArrayList<>(); for (String kindB : kindA) { if (kindB.trim().isEmpty()) continue; String[] kind = kindB.split("::"); FindKindBean findKindBean = new FindKindBean(); findKindBean.setGroup(getBookSourceName()); findKindBean.setTag(getBookSourceUrl()); findKindBean.setKindName(kind[0]); findKindBean.setKindUrl(kind[1]); children.add(findKindBean); } if (isJsAndCache) { aCache.put(getBookSourceUrl(), findRule); } FindKindGroupBean groupBean = new FindKindGroupBean(); groupBean.setGroupName(getBookSourceName()); groupBean.setGroupTag(getBookSourceUrl()); return new Pair<>(groupBean, children); } } catch (Exception exception) { exception.printStackTrace(); addGroup(findError); BookSourceManager.addBookSource(this); } return null; } public String getPayAction() { return this.payAction; } public void setPayAction(String payAction) { this.payAction = payAction; } public String getRuleChapterVip() { return this.ruleChapterVip; } public void setRuleChapterVip(String ruleChapterVip) { this.ruleChapterVip = ruleChapterVip; } public String getRuleChapterPay() { return this.ruleChapterPay; } public void setRuleChapterPay(String ruleChapterPay) { this.ruleChapterPay = ruleChapterPay; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/BookmarkBean.java ================================================ package com.kunfei.bookshelf.bean; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; @Entity public class BookmarkBean implements Cloneable { @Id private Long id = System.currentTimeMillis(); private String noteUrl; private String bookName; private String chapterName; private Integer chapterIndex; private Integer pageIndex; private String content; @Generated(hash = 1176037419) public BookmarkBean(Long id, String noteUrl, String bookName, String chapterName, Integer chapterIndex, Integer pageIndex, String content) { this.id = id; this.noteUrl = noteUrl; this.bookName = bookName; this.chapterName = chapterName; this.chapterIndex = chapterIndex; this.pageIndex = pageIndex; this.content = content; } @Generated(hash = 1612540172) public BookmarkBean() { } @Override protected Object clone() throws CloneNotSupportedException { BookmarkBean bookmarkBean = (BookmarkBean) super.clone(); bookmarkBean.id = id; bookmarkBean.noteUrl = noteUrl; bookmarkBean.bookName = bookName; bookmarkBean.chapterIndex = chapterIndex; bookmarkBean.chapterName = chapterName; bookmarkBean.pageIndex = pageIndex; bookmarkBean.content = content; return bookmarkBean; } public String getNoteUrl() { return this.noteUrl; } public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } public String getChapterName() { return this.chapterName; } public void setChapterName(String chapterName) { this.chapterName = chapterName; } public Integer getChapterIndex() { return this.chapterIndex; } public void setChapterIndex(Integer chapterIndex) { this.chapterIndex = chapterIndex; } public String getContent() { return this.content; } public void setContent(String content) { this.content = content; } public String getBookName() { return this.bookName; } public void setBookName(String bookName) { this.bookName = bookName; } public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public Integer getPageIndex() { return this.pageIndex; } public void setPageIndex(Integer pageIndex) { this.pageIndex = pageIndex; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/CookieBean.java ================================================ package com.kunfei.bookshelf.bean; import android.os.Parcel; import android.os.Parcelable; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; @Entity public class CookieBean implements Parcelable { @Id private String url; private String cookie; private CookieBean(Parcel in) { url = in.readString(); cookie = in.readString(); } @Generated(hash = 517179762) public CookieBean(String url, String cookie) { this.url = url; this.cookie = cookie; } @Generated(hash = 769081142) public CookieBean() { } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(url); dest.writeString(cookie); } @Override public int describeContents() { return 0; } public String getUrl() { return this.url; } public void setUrl(String url) { this.url = url; } public String getCookie() { return cookie == null ? "" : cookie; } public void setCookie(String cookie) { this.cookie = cookie; } public static final Creator CREATOR = new Creator() { @Override public CookieBean createFromParcel(Parcel in) { return new CookieBean(in); } @Override public CookieBean[] newArray(int size) { return new CookieBean[size]; } }; } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/DownloadBookBean.java ================================================ package com.kunfei.bookshelf.bean; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import io.reactivex.annotations.Nullable; public class DownloadBookBean implements Parcelable, Comparable { private String name; //小说名 private String noteUrl; private String coverUrl; private int downloadCount; private int start; private int end; private int successCount; private boolean isValid; private long finalDate; public DownloadBookBean() { } protected DownloadBookBean(Parcel in) { name = in.readString(); noteUrl = in.readString(); coverUrl = in.readString(); downloadCount = in.readInt(); start = in.readInt(); end = in.readInt(); successCount = in.readInt(); isValid = in.readByte() != 0; finalDate = in.readLong(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeString(noteUrl); dest.writeString(coverUrl); dest.writeInt(downloadCount); dest.writeInt(start); dest.writeInt(end); dest.writeInt(successCount); dest.writeByte((byte) (isValid ? 1 : 0)); dest.writeLong(finalDate); } @Override public int describeContents() { return 0; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public DownloadBookBean createFromParcel(Parcel in) { return new DownloadBookBean(in); } @Override public DownloadBookBean[] newArray(int size) { return new DownloadBookBean[size]; } }; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNoteUrl() { return noteUrl; } public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } public String getCoverUrl() { return coverUrl; } public void setCoverUrl(String coverUrl) { this.coverUrl = coverUrl; } public int getDownloadCount() { return downloadCount; } public void setDownloadCount(int downloadCount) { this.downloadCount = downloadCount; setValid(downloadCount > 0); } public int getStart() { return start; } public void setStart(int start) { this.start = start; } public int getEnd() { return end; } public void setEnd(int end) { this.end = end; } public int getSuccessCount() { return successCount; } public int getWaitingCount() { return this.downloadCount - this.successCount; } public synchronized void successCountAdd() { if (this.successCount < this.downloadCount) { this.successCount += 1; } } public boolean isValid() { return isValid; } public void setValid(boolean valid) { isValid = valid; } public long getFinalDate() { return finalDate; } public void setFinalDate(long finalDate) { this.finalDate = finalDate; } @Override public boolean equals(@Nullable Object obj) { if (obj instanceof DownloadBookBean) { return TextUtils.equals(((DownloadBookBean) obj).getNoteUrl(), this.noteUrl); } return super.equals(obj); } @Override public int compareTo(DownloadBookBean o) { return (int) (this.finalDate - o.finalDate); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/DownloadChapterBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; public class DownloadChapterBean implements BaseChapterBean { private String noteUrl; private int durChapterIndex; //当前章节数 private String durChapterUrl; //当前章节对应的文章地址 private String durChapterName; //当前章节名称 private String tag; private String bookName; public DownloadChapterBean() { } @Override public String getNoteUrl() { return noteUrl; } public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } @Override public int getDurChapterIndex() { return durChapterIndex; } public void setDurChapterIndex(int durChapterIndex) { this.durChapterIndex = durChapterIndex; } @Override public String getDurChapterUrl() { return durChapterUrl; } public void setDurChapterUrl(String durChapterUrl) { this.durChapterUrl = durChapterUrl; } @Override public String getDurChapterName() { return durChapterName; } public void setDurChapterName(String durChapterName) { this.durChapterName = durChapterName; } @Override public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public String getBookName() { return bookName; } public void setBookName(String bookName) { this.bookName = bookName; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/FindKindBean.java ================================================ package com.kunfei.bookshelf.bean; public class FindKindBean { private String group; private String tag; private String kindName; private String kindUrl; public FindKindBean() { } public String getKindName() { return kindName; } public void setKindName(String kindName) { this.kindName = kindName; } public String getKindUrl() { return kindUrl; } public void setKindUrl(String kindUrl) { this.kindUrl = kindUrl; } public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public String getGroup() { return group; } public void setGroup(String group) { this.group = group; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/FindKindGroupBean.java ================================================ package com.kunfei.bookshelf.bean; public class FindKindGroupBean { private String groupName; private String groupTag; public String getGroupName() { return groupName; } public void setGroupName(String groupName) { this.groupName = groupName; } public String getGroupTag() { return groupTag; } public void setGroupTag(String groupTag) { this.groupTag = groupTag; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/LocBookShelfBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; public class LocBookShelfBean { private Boolean isNew; private BookShelfBean bookShelfBean; public LocBookShelfBean(Boolean isNew, BookShelfBean bookShelfBean) { this.isNew = isNew; this.bookShelfBean = bookShelfBean; } public Boolean getNew() { return isNew; } public void setNew(Boolean aNew) { isNew = aNew; } public BookShelfBean getBookShelfBean() { return bookShelfBean; } public void setBookShelfBean(BookShelfBean bookShelfBean) { this.bookShelfBean = bookShelfBean; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/OpenChapterBean.java ================================================ package com.kunfei.bookshelf.bean; public class OpenChapterBean { private int chapterIndex; private int pageIndex; public OpenChapterBean(int chapterIndex, int pageIndex) { this.chapterIndex = chapterIndex; this.pageIndex = pageIndex; } public int getChapterIndex() { return chapterIndex; } public int getPageIndex() { return pageIndex; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/ReplaceRuleBean.java ================================================ package com.kunfei.bookshelf.bean; import android.os.Parcel; import android.os.Parcelable; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.OrderBy; import org.greenrobot.greendao.annotation.Transient; import java.util.regex.Pattern; /** * Created by GKF on 2018/2/7. * 阅读内容替换规则 */ @Entity public class ReplaceRuleBean implements Parcelable { @Id(autoincrement = true) private Long id; //描述 private String replaceSummary; //替换规则 private String regex; //替换为 private String replacement; //作用于 private String useTo; private Boolean enable; private Boolean isRegex; @OrderBy private int serialNumber; private ReplaceRuleBean(Parcel in) { id = in.readLong(); regex = in.readString(); replacement = in.readString(); replaceSummary = in.readString(); useTo = in.readString(); enable = in.readByte() != 0; serialNumber = in.readInt(); isRegex = in.readByte() != 0; } @Generated(hash = 1896663649) public ReplaceRuleBean(Long id, String replaceSummary, String regex, String replacement, String useTo, Boolean enable, Boolean isRegex, int serialNumber) { this.id = id; this.replaceSummary = replaceSummary; this.regex = regex; this.replacement = replacement; this.useTo = useTo; this.enable = enable; this.isRegex = isRegex; this.serialNumber = serialNumber; } @Generated(hash = 582692869) public ReplaceRuleBean() { } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeLong(id); parcel.writeString(regex); parcel.writeString(replacement); parcel.writeString(replaceSummary); parcel.writeString(useTo); parcel.writeByte((byte) (enable ? 1 : 0)); parcel.writeInt(serialNumber); parcel.writeByte((byte) (isRegex ? 1 : 0)); } @Transient public static final Creator CREATOR = new Creator() { @Override public ReplaceRuleBean createFromParcel(Parcel in) { return new ReplaceRuleBean(in); } @Override public ReplaceRuleBean[] newArray(int size) { return new ReplaceRuleBean[size]; } }; @Override public int describeContents() { return 0; } public String getReplaceSummary() { return this.replaceSummary; } public void setReplaceSummary(String replaceSummary) { this.replaceSummary = replaceSummary; } public String getRegex() { return this.regex; } public String getFixedRegex() { if (getIsRegex()) return this.regex; else return Pattern.quote(regex); } public void setRegex(String regex) { this.regex = regex; } public String getReplacement() { return this.replacement; } public void setReplacement(String replacement) { this.replacement = replacement; } public Boolean getEnable() { if (enable == null) { return false; } return this.enable; } public void setEnable(Boolean enable) { this.enable = enable; } public int getSerialNumber() { return this.serialNumber; } public void setSerialNumber(int serialNumber) { this.serialNumber = serialNumber; } public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getUseTo() { return this.useTo; } public void setUseTo(String useTo) { this.useTo = useTo; } public Boolean getIsRegex() { return isRegex == null ? true : isRegex; } public void setIsRegex(Boolean isRegex) { this.isRegex = isRegex; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/SearchBookBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; import com.google.gson.Gson; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.BookSourceManager; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.Transient; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING; @Entity public class SearchBookBean implements BaseBookBean { @Id private String noteUrl; private String coverUrl;//封面URL private String name; private String author; private String tag; private String kind;//分类 private String origin;//来源 private String lastChapter; private String introduce; //简介 private String chapterUrl;//目录URL private Long addTime = 0L; private Long upTime = 0L; private String variable; @Transient private Boolean isCurrentSource = false; @Transient private int originNum = 1; @Transient private int lastChapterNum = -2; @Transient private int searchTime = Integer.MAX_VALUE; @Transient private LinkedHashSet originUrls; @Transient private Map variableMap; @Transient private String bookInfoHtml; public SearchBookBean() { } public SearchBookBean(String tag, String origin) { this.tag = tag; this.origin = origin; } @Generated(hash = 337890066) public SearchBookBean(String noteUrl, String coverUrl, String name, String author, String tag, String kind, String origin, String lastChapter, String introduce, String chapterUrl, Long addTime, Long upTime, String variable) { this.noteUrl = noteUrl; this.coverUrl = coverUrl; this.name = name; this.author = author; this.tag = tag; this.kind = kind; this.origin = origin; this.lastChapter = lastChapter; this.introduce = introduce; this.chapterUrl = chapterUrl; this.addTime = addTime; this.upTime = upTime; this.variable = variable; } @Override public String getVariable() { return this.variable; } @Override public void setVariable(String variable) { this.variable = variable; } @Override public void putVariable(String key, String value) { if (variableMap == null) { variableMap = new HashMap<>(); } variableMap.put(key, value); variable = new Gson().toJson(variableMap); } @Override public Map getVariableMap() { if (variableMap == null) { return new Gson().fromJson(variable, MAP_STRING); } return variableMap; } @Override public String getNoteUrl() { return noteUrl; } @Override public void setNoteUrl(String noteUrl) { this.noteUrl = noteUrl; } public String getCoverUrl() { return coverUrl; } public void setCoverUrl(String coverUrl) { this.coverUrl = coverUrl; } public String getName() { return name; } public void setName(String name) { this.name = name != null ? name.trim().replaceAll(" ", "") : null; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = BookshelfHelp.formatAuthor(author); } public String getLastChapter() { return lastChapter == null ? "" : lastChapter; } public void setLastChapter(String lastChapter) { this.lastChapter = lastChapter; } public int getLastChapterNum() { if (lastChapterNum == -2) this.lastChapterNum = BookshelfHelp.guessChapterNum(lastChapter); return lastChapterNum; } public String getKind() { return kind; } public void setKind(String kind) { this.kind = kind; } @Override public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public String getOrigin() { return origin; } public void setOrigin(String origin) { this.origin = origin; } public Boolean getIsCurrentSource() { return this.isCurrentSource; } public void setIsCurrentSource(Boolean isCurrentSource) { this.isCurrentSource = isCurrentSource; if (isCurrentSource) this.addTime = System.currentTimeMillis(); } public int getOriginNum() { return originNum; } public void addOriginUrl(String origin) { if (this.originUrls == null) { this.originUrls = new LinkedHashSet<>(); } this.originUrls.add(origin); originNum = this.originUrls.size(); } public String getIntroduce() { return introduce; } public void setIntroduce(String introduce) { this.introduce = introduce; } public String getChapterUrl() { return this.chapterUrl; } public void setChapterUrl(String chapterUrl) { this.chapterUrl = chapterUrl; } public long getAddTime() { return this.addTime; } public void setAddTime(Long addTime) { this.addTime = addTime; } public int getWeight() { BookSourceBean source = BookSourceManager.getBookSourceByUrl(this.tag); if (source != null) return source.getWeight(); else return 0; } public int getSearchTime() { return searchTime; } public void setSearchTime(int searchTime) { this.searchTime = searchTime; } public Long getUpTime() { return this.upTime; } public void setUpTime(Long upTime) { this.upTime = upTime; } public String getBookInfoHtml() { return bookInfoHtml; } public void setBookInfoHtml(String bookInfoHtml) { this.bookInfoHtml = bookInfoHtml; } // 一次性存入搜索书籍节点信息 public void setSearchInfo(String name, String author, String kind, String lastChapter, String introduce, String coverUrl, String noteUrl) { this.name = name; this.author = author; this.kind = kind; this.lastChapter = lastChapter; this.introduce = introduce; this.coverUrl = coverUrl; this.noteUrl = noteUrl; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/SearchHistoryBean.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.bean; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; @Entity public class SearchHistoryBean { @Id(autoincrement = true) private Long id = null; private int type; private String content; private long date; public long getDate() { return this.date; } public void setDate(long date) { this.date = date; } public String getContent() { return this.content; } public void setContent(String content) { this.content = content; } public int getType() { return this.type; } public void setType(int type) { this.type = type; } public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public SearchHistoryBean(int type, String content, long date) { this.type = type; this.content = content; this.date = date; } @Generated(hash = 488115752) public SearchHistoryBean(Long id, int type, String content, long date) { this.id = id; this.type = type; this.content = content; this.date = date; } @Generated(hash = 1570282321) public SearchHistoryBean() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/TwoDataBean.java ================================================ package com.kunfei.bookshelf.bean; public class TwoDataBean { private T data1; private S data2; public TwoDataBean(T data1, S data2) { this.data1 = data1; this.data2 = data2; } public T getData1() { return data1; } public S getData2() { return data2; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/TxtChapterRuleBean.java ================================================ package com.kunfei.bookshelf.bean; import androidx.annotation.Nullable; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Generated; import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.OrderBy; import java.util.Objects; @Entity public class TxtChapterRuleBean { @Id private String name; private String rule; @OrderBy private Integer serialNumber; private Boolean enable; @Generated(hash = 2018686288) public TxtChapterRuleBean(String name, String rule, Integer serialNumber, Boolean enable) { this.name = name; this.rule = rule; this.serialNumber = serialNumber; this.enable = enable; } @Generated(hash = 382733400) public TxtChapterRuleBean() { } @Override public boolean equals(@Nullable Object obj) { if (obj instanceof TxtChapterRuleBean) { return Objects.equals(this.name, ((TxtChapterRuleBean) obj).name); } return false; } public TxtChapterRuleBean copy() { TxtChapterRuleBean ruleBean = new TxtChapterRuleBean(); ruleBean.setName(name); ruleBean.setRule(rule); ruleBean.setEnable(enable); ruleBean.setSerialNumber(serialNumber); return ruleBean; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getRule() { return rule; } public void setRule(String rule) { this.rule = rule; } public Integer getSerialNumber() { return serialNumber; } public void setSerialNumber(Integer serialNumber) { this.serialNumber = serialNumber; } public Boolean getEnable() { return enable == null ? true : enable; } public void setEnable(Boolean enable) { this.enable = enable; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/UpdateInfoBean.java ================================================ package com.kunfei.bookshelf.bean; import android.os.Parcel; import android.os.Parcelable; public class UpdateInfoBean implements Parcelable { private String lastVersion; private String url; private String detail; private Boolean upDate; public UpdateInfoBean() { } protected UpdateInfoBean(Parcel in) { lastVersion = in.readString(); url = in.readString(); detail = in.readString(); upDate = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public UpdateInfoBean createFromParcel(Parcel in) { return new UpdateInfoBean(in); } @Override public UpdateInfoBean[] newArray(int size) { return new UpdateInfoBean[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(lastVersion); parcel.writeString(url); parcel.writeString(detail); parcel.writeByte((byte) (upDate ? 1 : 0)); } public String getLastVersion() { return lastVersion; } public void setLastVersion(String lastVersion) { this.lastVersion = lastVersion; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getDetail() { return detail; } public void setDetail(String detail) { this.detail = detail; } public Boolean getUpDate() { return upDate; } public void setUpDate(Boolean upDate) { this.upDate = upDate; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/bean/WebChapterBean.java ================================================ package com.kunfei.bookshelf.bean; import java.util.LinkedHashSet; import java.util.List; public class WebChapterBean { private String url; private List data; private LinkedHashSet nextUrlList; public WebChapterBean(String url) { this.url = url; } public WebChapterBean(List data, LinkedHashSet nextUrlList) { this.data = data; this.nextUrlList = nextUrlList; } public List getData() { return data; } public void setData(List data) { this.data = data; } public LinkedHashSet getNextUrlList() { return nextUrlList; } public String getUrl() { return url; } public boolean noData() { return data == null; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/constant/AppConst.kt ================================================ package com.kunfei.bookshelf.constant import android.annotation.SuppressLint import android.provider.Settings import splitties.init.appCtx import java.text.SimpleDateFormat import javax.script.ScriptEngine import javax.script.ScriptEngineManager @SuppressLint("SimpleDateFormat") object AppConst { const val APP_TAG = "Legado" val androidId: String by lazy { Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID) } const val channelIdDownload = "channel_download" const val channelIdReadAloud = "channel_read_aloud" const val channelIdWeb = "channel_web" const val UA_NAME = "User-Agent" val SCRIPT_ENGINE: ScriptEngine by lazy { ScriptEngineManager().getEngineByName("rhino") } val timeFormat: SimpleDateFormat by lazy { SimpleDateFormat("HH:mm") } val dateFormat: SimpleDateFormat by lazy { SimpleDateFormat("yyyy/MM/dd HH:mm") } val fileNameFormat: SimpleDateFormat by lazy { SimpleDateFormat("yy-MM-dd-HH-mm-ss") } val keyboardToolChars: List by lazy { arrayListOf( "❓", "@css:", "", "{{}}", "##", "&&", "%%", "||", "//", "\\", "$.", "@", ":", "class", "text", "href", "textNodes", "ownText", "all", "html", "[", "]", "<", ">", "#", "!", ".", "+", "-", "*", "=", "{'webView': true}" ) } const val bookGroupAllId = -1L const val bookGroupLocalId = -2L const val bookGroupAudioId = -3L const val bookGroupNoneId = -4L const val notificationIdRead = 1144771 const val notificationIdAudio = 1144772 const val notificationIdWeb = 1144773 const val notificationIdDownload = 1144774 val urlOption: String by lazy { """ ,{ 'charset': '', 'method': 'POST', 'body': '', 'headers': { 'User-Agent': '' } } """.trimIndent() } val menuViewNames = arrayOf( "com.android.internal.view.menu.ListMenuItemView", "androidx.appcompat.view.menu.ListMenuItemView" ) val darkWebViewJs by lazy { """ document.body.style.backgroundColor = "#222222"; document.getElementsByTagName('body')[0].style.webkitTextFillColor = '#8a8a8a'; """.trimIndent() } val charsets = arrayListOf("UTF-8", "GB2312", "GB18030", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII") data class AppInfo( var versionCode: Long = 0L, var versionName: String = "" ) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/constant/AppConstant.java ================================================ package com.kunfei.bookshelf.constant; import android.content.Context; import android.provider.Settings; import com.google.gson.reflect.TypeToken; import com.kunfei.bookshelf.BuildConfig; import com.kunfei.bookshelf.MApplication; import java.io.File; import java.lang.reflect.Type; import java.util.Map; import java.util.regex.Pattern; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import okhttp3.MediaType; public class AppConstant { public static final String ActionStartService = "startService"; public static final String ActionDoneService = "doneService"; public static final long TIME_OUT = BuildConfig.DEBUG ? 600 : 180; //Book Date Convert Format public static final String FORMAT_TIME = "HH:mm"; public static final String FORMAT_FILE_DATE = "yyyy-MM-dd"; //BookCachePath (因为getCachePath引用了Context,所以必须是静态变量,不能够是静态常量) public static String BOOK_CACHE_PATH = MApplication.downloadPath + File.separator + "book_cache" + File.separator; public static Type MAP_STRING = new TypeToken>() { }.getType(); public static final String DEFAULT_WEB_DAV_URL = "https://dav.jianguoyun.com/dav/"; public static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"; public static final Pattern JS_PATTERN = Pattern.compile("([\\w\\W]*?|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE); public static final Pattern EXP_PATTERN = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}"); public static final ScriptEngine SCRIPT_ENGINE = new ScriptEngineManager().getEngineByName("rhino"); public static final MediaType jsonMediaType = MediaType.parse("Content-Type, application/json"); static public String androidId(Context context) { return Settings.System.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/constant/BookType.java ================================================ package com.kunfei.bookshelf.constant; public class BookType { public final static String AUDIO = "AUDIO"; } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/constant/RxBusTag.java ================================================ package com.kunfei.bookshelf.constant; public class RxBusTag { public final static String HAD_ADD_BOOK = "add_book"; public final static String HAD_REMOVE_BOOK = "remove_book"; public final static String REFRESH_BOOK_LIST = "reFresh_book"; public final static String UPDATE_GROUP = "UPDATE_GROUP"; public final static String UPDATE_BOOK_PROGRESS = "update_book_progress"; public final static String UPDATE_READ = "update_read"; public final static String CHAPTER_CHANGE = "chapter_change"; public final static String MEDIA_BUTTON = "media_button"; public final static String ALOUD_STATE = "aloud_state"; public final static String ALOUD_TIMER = "aloud_timer"; public final static String RECREATE = "recreate"; public final static String CHECK_SOURCE_STATE = "checkSourceState"; public final static String CHECK_SOURCE_FINISH = "checkSourceFinish"; public final static String IMMERSION_CHANGE = "Immersion_Change"; public final static String SEARCH_BOOK = "search_book"; public final static String UPDATE_APK_STATE = "updateApkState"; public final static String DOWNLOAD_ALL = "downloadAll"; public final static String UP_SEARCH_BOOK = "upSearchBook"; public final static String SKIP_TO_CHAPTER = "skipToChapter"; public final static String OPEN_BOOK_MARK = "openBookMark"; public final static String READ_ALOUD_NUMBER = "readAloudNumber"; public final static String READ_ALOUD_START = "readAloudStart"; public final static String AUTO_BACKUP = "autoBackup"; public final static String PRINT_DEBUG_LOG = "printDebugLog"; public final static String AUDIO_SIZE = "audioSize"; public final static String AUDIO_DUR = "audioDur"; } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/constant/TimeConstants.java ================================================ package com.kunfei.bookshelf.constant; import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class TimeConstants { public static final int MSEC = 1; public static final int SEC = 1000; public static final int MIN = 60000; public static final int HOUR = 3600000; public static final int DAY = 86400000; @IntDef({MSEC, SEC, MIN, HOUR, DAY}) @Retention(RetentionPolicy.SOURCE) public @interface Unit { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/AppFrontBackHelper.java ================================================ package com.kunfei.bookshelf.help; import android.app.Activity; import android.app.Application; import android.os.Bundle; /** * 应用前后台状态监听帮助类,仅在Application中使用 */ public class AppFrontBackHelper { private OnAppStatusListener mOnAppStatusListener; public static AppFrontBackHelper getInstance() { return new AppFrontBackHelper(); } /** * 注册状态监听,仅在Application中使用 */ public void register(Application application, OnAppStatusListener listener) { mOnAppStatusListener = listener; application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks); } private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { //打开的Activity数量统计 private int activityStartCount = 0; @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { activityStartCount++; //数值从0变到1说明是从后台切到前台 if (activityStartCount == 1) { //从后台切到前台 if (mOnAppStatusListener != null) { mOnAppStatusListener.onFront(); } } } @Override public void onActivityResumed(Activity activity) { } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { activityStartCount--; //数值从1到0说明是从前台切到后台 if (activityStartCount == 0) { //从前台切到后台 if (mOnAppStatusListener != null) { mOnAppStatusListener.onBack(); } } } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } }; public interface OnAppStatusListener { void onFront(); void onBack(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/BlurTransformation.java ================================================ package com.kunfei.bookshelf.help; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.os.Build; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RenderScript; import android.renderscript.ScriptIntrinsicBlur; import androidx.annotation.NonNull; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; import java.security.MessageDigest; public class BlurTransformation extends BitmapTransformation { private RenderScript rs; private int radius; public BlurTransformation(Context context, int radius) { super(); rs = RenderScript.create(context); this.radius = radius; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { Bitmap blurredBitmap = toTransform.copy(Bitmap.Config.ARGB_8888, true); // Allocate memory for Renderscript to work with //分配用于渲染脚本的内存 Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED); Allocation output = Allocation.createTyped(rs, input.getType()); // Load up an instance of the specific script that we want to use. //加载我们想要使用的特定脚本的实例。 ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); script.setInput(input); // Set the blur radius //设置模糊半径 script.setRadius(radius); // Start the ScriptIntrinsicBlur //启动 ScriptIntrinsicBlur, script.forEach(output); // Copy the output to the blurred bitmap //将输出复制到模糊的位图 output.copyTo(blurredBitmap); return blurredBitmap; } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { messageDigest.update("blur transformation".getBytes()); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/BookshelfHelp.java ================================================ package com.kunfei.bookshelf.help; import android.annotation.SuppressLint; import android.text.TextUtils; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.bean.BaseChapterBean; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookmarkBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.constant.AppConstant; import com.kunfei.bookshelf.dao.BookChapterBeanDao; import com.kunfei.bookshelf.dao.BookInfoBeanDao; import com.kunfei.bookshelf.dao.BookShelfBeanDao; import com.kunfei.bookshelf.dao.BookmarkBeanDao; import com.kunfei.bookshelf.utils.StringUtils; import net.ricecode.similarity.JaroWinklerStrategy; import net.ricecode.similarity.StringSimilarityService; import net.ricecode.similarity.StringSimilarityServiceImpl; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Created by GKF on 2018/1/18. * 添加删除Book */ public class BookshelfHelp { private static final Pattern chapterNamePattern = Pattern.compile("^(.*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟0-9\\s]+)[章节篇回集])[、,。 ::.\\s]*"); public static String getCachePathName(String bookName, String tag) { return formatFolderName(bookName + "-" + tag); } @SuppressLint("DefaultLocale") public static String getCacheFileName(int chapterIndex, String chapterName) { return String.format("%05d-%s", chapterIndex, formatFolderName(chapterName)); } public static boolean isChapterCached(String bookName, String tag, BaseChapterBean chapter, boolean isAudio) { if (isAudio) { BookContentBean contentBean = DbHelper.getDaoSession().getBookContentBeanDao().load(chapter.getDurChapterUrl()); if (contentBean == null) return false; if (contentBean.outTime()) { DbHelper.getDaoSession().getBookContentBeanDao().delete(contentBean); return false; } return !TextUtils.isEmpty(contentBean.getDurChapterContent()); } File file = new File(AppConstant.BOOK_CACHE_PATH + getCachePathName(bookName, tag) + File.separator + getCacheFileName(chapter.getDurChapterIndex(), chapter.getDurChapterName()) + FileHelp.SUFFIX_NB); return file.exists(); } public static String getChapterCache(BookShelfBean bookShelfBean, BookChapterBean chapter) { if (bookShelfBean.isAudio()) { BookContentBean contentBean = DbHelper.getDaoSession().getBookContentBeanDao().load(chapter.getDurChapterUrl()); if (contentBean == null) return null; if (contentBean.outTime()) { DbHelper.getDaoSession().getBookContentBeanDao().delete(contentBean); return null; } return contentBean.getDurChapterContent(); } File file = new File(AppConstant.BOOK_CACHE_PATH + formatFolderName(BookshelfHelp.getCachePathName(bookShelfBean.getBookInfoBean().getName(), bookShelfBean.getTag())) + File.separator + getCacheFileName(chapter.getDurChapterIndex(), chapter.getDurChapterName()) + FileHelp.SUFFIX_NB); if (!file.exists()) return null; byte[] contentByte = DocumentHelper.getBytes(file); return new String(contentByte, StandardCharsets.UTF_8); } public static void clearCaches(boolean clearChapterList) { FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH); FileHelp.getFolder(AppConstant.BOOK_CACHE_PATH); if (clearChapterList) DbHelper.getDaoSession().getBookChapterBeanDao().deleteAll(); } /** * 删除章节文件 */ public static void delChapter(String folderName, int index, String fileName) { FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH + folderName + File.separator + getCacheFileName(index, fileName) + FileHelp.SUFFIX_NB); } /** * 存储章节 */ public static synchronized boolean saveChapterInfo(String folderName, int index, String fileName, String content) { if (content == null) { return false; } File file = getBookFile(folderName, index, fileName); //获取流并存储 try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { writer.write(fileName + "\n\n"); writer.write(content); writer.write("\n\n"); writer.flush(); return true; } catch (IOException e) { e.printStackTrace(); return false; } } /** * 创建或获取存储文件 */ public static File getBookFile(String folderName, int index, String fileName) { return FileHelp.createFileIfNotExist(AppConstant.BOOK_CACHE_PATH + formatFolderName(folderName) + File.separator + getCacheFileName(index, fileName) + FileHelp.SUFFIX_NB); } private static String formatFolderName(String folderName) { return folderName.replaceAll("[\\\\/:*?\"<>|.]", ""); } /** * 根据目录名获取当前章节 */ public static int getDurChapter(int oldDurChapterIndex, int oldChapterListSize, String oldDurChapterName, List newChapterList) { if (oldChapterListSize == 0) return 0; int oldChapterNum = getChapterNum(oldDurChapterName); String oldName = getPureChapterName(oldDurChapterName); int newChapterSize = newChapterList.size(); int min = Math.max(0, Math.min(oldDurChapterIndex, oldDurChapterIndex - oldChapterListSize + newChapterSize) - 10); int max = Math.min(newChapterSize - 1, Math.max(oldDurChapterIndex, oldDurChapterIndex - oldChapterListSize + newChapterSize) + 10); double nameSim = 0; int newIndex = 0; int newNum = 0; if (!oldName.isEmpty()) { StringSimilarityService service = new StringSimilarityServiceImpl(new JaroWinklerStrategy()); for (int i = min; i <= max; i++) { String newName = getPureChapterName(newChapterList.get(i).getDurChapterName()); double temp = service.score(oldName, newName); if (temp > nameSim) { nameSim = temp; newIndex = i; } } } if (nameSim < 0.96 && oldChapterNum > 0) { for (int i = min; i <= max; i++) { int temp = getChapterNum(newChapterList.get(i).getDurChapterName()); if (temp == oldChapterNum) { newNum = temp; newIndex = i; break; } else if (Math.abs(temp - oldChapterNum) < Math.abs(newNum - oldChapterNum)) { newNum = temp; newIndex = i; } } } if (nameSim > 0.96 || Math.abs(newNum - oldChapterNum) < 1) { return newIndex; } else { return Math.min(Math.max(0, newChapterList.size() - 1), oldDurChapterIndex); } } private static int getChapterNum(String chapterName) { if (chapterName != null) { Matcher matcher = chapterNamePattern.matcher(chapterName); if (matcher.find()) { return StringUtils.stringToInt(matcher.group(2)); } } return -1; } private static String getPureChapterName(String chapterName) { return chapterName == null ? "" : StringUtils.fullToHalf(chapterName).replaceAll("\\s", "") .replaceAll("^第.*?章|[(\\[][^()\\[\\]]{2,}[)\\]]$", "") .replaceAll("[^\\w\\u4E00-\\u9FEF〇\\u3400-\\u4DBF\\u20000-\\u2A6DF\\u2A700-\\u2EBEF]", ""); // 所有非字母数字中日韩文字 CJK区+扩展A-F区 } /** * 获取所有书籍 */ public static List getAllBook() { List bookShelfList = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder() .orderDesc(BookShelfBeanDao.Properties.FinalDate).list(); for (int i = 0; i < bookShelfList.size(); i++) { BookInfoBean bookInfoBean = DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder() .where(BookInfoBeanDao.Properties.NoteUrl.eq(bookShelfList.get(i).getNoteUrl())).limit(1).build().unique(); if (bookInfoBean != null) { bookShelfList.get(i).setBookInfoBean(bookInfoBean); } else { bookShelfList.remove(i); i--; } } return bookShelfList; } /** * 获取书籍按分组 */ public static List getBooksByGroup(int group) { List bookShelfList = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder() .where(BookShelfBeanDao.Properties.Group.eq(group)) .orderDesc(BookShelfBeanDao.Properties.FinalDate).list(); for (int i = 0; i < bookShelfList.size(); i++) { BookInfoBean bookInfoBean = DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder() .where(BookInfoBeanDao.Properties.NoteUrl.eq(bookShelfList.get(i).getNoteUrl())).limit(1).build().unique(); if (bookInfoBean != null) { bookShelfList.get(i).setBookInfoBean(bookInfoBean); } else { DbHelper.getDaoSession().getBookShelfBeanDao().delete(bookShelfList.get(i)); bookShelfList.remove(i); i--; } } return bookShelfList; } /** * 获取书籍按bookUrl */ public static BookShelfBean getBook(String bookUrl) { BookShelfBean bookShelfBean = DbHelper.getDaoSession().getBookShelfBeanDao().load(bookUrl); if (bookShelfBean != null) { BookInfoBean bookInfoBean = DbHelper.getDaoSession().getBookInfoBeanDao().load(bookUrl); if (bookInfoBean != null) { bookShelfBean.setBookInfoBean(bookInfoBean); return bookShelfBean; } } return null; } public static List searchBookInfo(String key) { return DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder() .where(BookInfoBeanDao.Properties.Name.like("%" + key + "%")) .orderAsc(BookInfoBeanDao.Properties.Name) .list(); } /** * 移除书籍 */ public static void removeFromBookShelf(BookShelfBean bookShelfBean, boolean keepCaches) { DbHelper.getDaoSession().getBookShelfBeanDao().deleteByKey(bookShelfBean.getNoteUrl()); DbHelper.getDaoSession().getBookInfoBeanDao().deleteByKey(bookShelfBean.getBookInfoBean().getNoteUrl()); delChapterList(bookShelfBean.getNoteUrl()); if (!keepCaches) { String bookName = bookShelfBean.getBookInfoBean().getName(); // 如果书架上有其他同名书籍,只删除本书源的缓存 long bookNum = DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder() .where(BookInfoBeanDao.Properties.Name.eq(bookName)).count(); if (bookNum > 0) { FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH + getCachePathName(bookShelfBean.getBookInfoBean().getName(), bookShelfBean.getTag())); return; } // 没有同名书籍,删除本书所有的缓存 try { File file = FileHelp.getFolder(AppConstant.BOOK_CACHE_PATH); String[] bookCaches = file.list((dir, name) -> new File(dir, name).isDirectory() && name.startsWith(bookName + "-")); for (String bookPath : bookCaches) { FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH + bookPath); } } catch (Exception ignored) { } } } /** * 是否在书架 */ public static boolean isInBookShelf(String bookUrl) { if (bookUrl == null) { return false; } long count = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder() .where(BookShelfBeanDao.Properties.NoteUrl.eq(bookUrl)) .count(); return count > 0; } /** * 移除书籍 */ public static void removeFromBookShelf(BookShelfBean bookShelfBean) { removeFromBookShelf(bookShelfBean, false); } /** * 保存书籍 */ public static void saveBookToShelf(BookShelfBean bookShelfBean) { if (bookShelfBean.getErrorMsg() == null) { DbHelper.getDaoSession().getBookInfoBeanDao().insertOrReplace(bookShelfBean.getBookInfoBean()); DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean); } } /** * 搜索转书籍 */ public static BookShelfBean getBookFromSearchBook(SearchBookBean searchBookBean) { BookShelfBean bookShelfBean = new BookShelfBean(); bookShelfBean.setTag(searchBookBean.getTag()); bookShelfBean.setNoteUrl(searchBookBean.getNoteUrl()); bookShelfBean.setFinalDate(System.currentTimeMillis()); bookShelfBean.setDurChapter(0); bookShelfBean.setDurChapterPage(0); bookShelfBean.setVariable(searchBookBean.getVariable()); BookInfoBean bookInfo = bookShelfBean.getBookInfoBean(); bookInfo.setNoteUrl(searchBookBean.getNoteUrl()); bookInfo.setAuthor(searchBookBean.getAuthor()); bookInfo.setCoverUrl(searchBookBean.getCoverUrl()); bookInfo.setName(searchBookBean.getName()); bookInfo.setTag(searchBookBean.getTag()); bookInfo.setOrigin(searchBookBean.getOrigin()); bookInfo.setIntroduce(searchBookBean.getIntroduce()); bookInfo.setChapterUrl(searchBookBean.getChapterUrl()); bookInfo.setBookInfoHtml(searchBookBean.getBookInfoHtml()); bookShelfBean.setVariable(searchBookBean.getVariable()); return bookShelfBean; } public static List getChapterList(String noteUrl) { return DbHelper.getDaoSession().getBookChapterBeanDao().queryBuilder() .where(BookChapterBeanDao.Properties.NoteUrl.eq(noteUrl)) .orderAsc(BookChapterBeanDao.Properties.DurChapterIndex) .build() .list(); } public static void delChapterList(String noteUrl) { DbHelper.getDaoSession().getBookChapterBeanDao().queryBuilder() .where(BookChapterBeanDao.Properties.NoteUrl.eq(noteUrl)) .buildDelete().executeDeleteWithoutDetachingEntities(); } public static void saveBookmark(BookmarkBean bookmarkBean) { DbHelper.getDaoSession().getBookmarkBeanDao().insertOrReplace(bookmarkBean); } public static void delBookmark(BookmarkBean bookmarkBean) { DbHelper.getDaoSession().getBookmarkBeanDao().delete(bookmarkBean); } public static List getBookmarkList(String bookName) { return DbHelper.getDaoSession().getBookmarkBeanDao().queryBuilder() .where(BookmarkBeanDao.Properties.BookName.eq(bookName)) .orderAsc(BookmarkBeanDao.Properties.ChapterIndex) .build() .list(); } public static String getReadProgress(BookShelfBean bookShelfBean) { return getReadProgress(bookShelfBean.getDurChapter(), bookShelfBean.getChapterListSize(), 0, 0); } public static String getReadProgress(int durChapterIndex, int chapterAll, int durPageIndex, int durPageAll) { DecimalFormat df = new DecimalFormat("0.0%"); if (chapterAll == 0 || (durPageAll == 0 && durChapterIndex == 0)) { return "0.0%"; } else if (durPageAll == 0) { return df.format((durChapterIndex + 1.0f) / chapterAll); } String percent = df.format(durChapterIndex * 1.0f / chapterAll + 1.0f / chapterAll * (durPageIndex + 1) / durPageAll); if (percent.equals("100.0%") && (durChapterIndex + 1 != chapterAll || durPageIndex + 1 != durPageAll)) { percent = "99.9%"; } return percent; } public static String formatAuthor(String author) { if (author == null) { return ""; } return author.replaceAll("作\\s*者[\\s::]*", "").replaceAll("\\s+", " ").trim(); } public static int guessChapterNum(String name) { if (TextUtils.isEmpty(name) || name.matches("第.*?卷.*?第.*[章节回]")) return -1; Matcher matcher = chapterNamePattern.matcher(name); if (matcher.find()) { return StringUtils.stringToInt(matcher.group(2)); } return -1; } /** * 排序 */ public static void order(List books, String bookshelfOrder) { if (books == null || books.size() == 0) { return; } switch (bookshelfOrder) { case "0": Collections.sort(books, (o1, o2) -> Long.compare(o2.getFinalDate(), o1.getFinalDate())); break; case "1": Collections.sort(books, (o1, o2) -> Long.compare(o2.getFinalRefreshData(), o1.getFinalRefreshData())); break; case "2": Collections.sort(books, (o1, o2) -> Integer.compare(o1.getSerialNumber(), o2.getSerialNumber())); break; } } /** * 清除书架 */ public static void clearBookshelf() { DbHelper.getDaoSession().getBookShelfBeanDao().deleteAll(); DbHelper.getDaoSession().getBookInfoBeanDao().deleteAll(); DbHelper.getDaoSession().getBookChapterBeanDao().deleteAll(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/ChangeSourceHelp.java ================================================ package com.kunfei.bookshelf.help; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.bean.TwoDataBean; import com.kunfei.bookshelf.model.SearchBookModel; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.utils.RxUtils; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.reactivex.Observable; public class ChangeSourceHelp { private SearchBookModel searchBookModel; private BookShelfBean bookShelfBean; private ChangeSourceListener changeSourceListener; private boolean finish; public ChangeSourceHelp() { SearchBookModel.OnSearchListener searchListener = new SearchBookModel.OnSearchListener() { @Override public void refreshSearchBook() { } @Override public void refreshFinish(Boolean value) { } @Override public void loadMoreFinish(Boolean value) { } @Override public void loadMoreSearchBook(List value) { selectBook(value); } @Override public void searchBookError(Throwable throwable) { if (!finish && changeSourceListener != null) { changeSourceListener.error(throwable); searchBookModel.onDestroy(); } } @Override public int getItemCount() { return 0; } }; searchBookModel = new SearchBookModel(searchListener); } public void autoChange(BookShelfBean bookShelfBean, ChangeSourceListener changeSourceListener) { this.bookShelfBean = bookShelfBean; this.changeSourceListener = changeSourceListener; long searchTime = System.currentTimeMillis(); finish = false; searchBookModel.setSearchTime(searchTime); searchBookModel.search(bookShelfBean.getBookInfoBean().getName(), searchTime, new ArrayList<>(), false); } private synchronized void selectBook(List value) { if (finish) return; for (SearchBookBean searchBookBean : value) { if (Objects.equals(searchBookBean.getName(), bookShelfBean.getBookInfoBean().getName())) { if (Objects.equals(searchBookBean.getAuthor(), bookShelfBean.getBookInfoBean().getAuthor())) { if (changeSourceListener != null) { finish = true; changeBookSource(searchBookBean, bookShelfBean) .subscribe(new MyObserver>>() { @Override public void onNext(TwoDataBean> twoData) { changeSourceListener.finish(twoData.getData1(), twoData.getData2()); } @Override public void onError(Throwable e) { changeSourceListener.error(e); } }); } searchBookModel.onDestroy(); break; } } else { break; } } } public void stopSearch() { if (searchBookModel != null) { searchBookModel.onDestroy(); } } public static Observable>> changeBookSource(SearchBookBean searchBook, BookShelfBean oldBook) { BookShelfBean bookShelfBean = BookshelfHelp.getBookFromSearchBook(searchBook); bookShelfBean.setSerialNumber(oldBook.getSerialNumber()); bookShelfBean.setLastChapterName(oldBook.getLastChapterName()); bookShelfBean.setDurChapterName(oldBook.getDurChapterName()); bookShelfBean.setDurChapter(oldBook.getDurChapter()); bookShelfBean.setDurChapterPage(oldBook.getDurChapterPage()); bookShelfBean.setReplaceEnable(oldBook.getReplaceEnable()); bookShelfBean.setAllowUpdate(oldBook.getAllowUpdate()); return WebBookModel.getInstance().getBookInfo(bookShelfBean) .flatMap(book -> WebBookModel.getInstance().getChapterList(book)) .flatMap(chapterBeanList -> saveChangedBook(bookShelfBean, oldBook, chapterBeanList)) .compose(RxUtils::toSimpleSingle); } private static Observable>> saveChangedBook(BookShelfBean newBook, BookShelfBean oldBook, List chapterBeanList) { return Observable.create(e -> { if (newBook.getChapterListSize() <= oldBook.getChapterListSize()) { newBook.setHasUpdate(false); } newBook.setCustomCoverPath(oldBook.getCustomCoverPath()); newBook.setDurChapter(BookshelfHelp.getDurChapter(oldBook.getDurChapter(), oldBook.getChapterListSize(), oldBook.getDurChapterName(), chapterBeanList)); newBook.setDurChapterName(chapterBeanList.get(newBook.getDurChapter()).getDurChapterName()); newBook.setGroup(oldBook.getGroup()); newBook.getBookInfoBean().setName(oldBook.getBookInfoBean().getName()); newBook.getBookInfoBean().setAuthor(oldBook.getBookInfoBean().getAuthor()); BookshelfHelp.removeFromBookShelf(oldBook); BookshelfHelp.saveBookToShelf(newBook); DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeanList); e.onNext(new TwoDataBean<>(newBook, chapterBeanList)); e.onComplete(); }); } public interface ChangeSourceListener { void finish(BookShelfBean bookShelfBean, List chapterBeanList); void error(Throwable throwable); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/ChapterContentHelp.java ================================================ package com.kunfei.bookshelf.help; import android.text.TextUtils; import android.util.Log; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import com.kunfei.bookshelf.model.ReplaceRuleManager; import com.luhuiguo.chinese.ChineseUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ChapterContentHelp { private static ChapterContentHelp instance; public static synchronized ChapterContentHelp getInstance() { if (instance == null) instance = new ChapterContentHelp(); return instance; } /** * 转繁体 */ private String toTraditional(String content) { int convertCTS = ReadBookControl.getInstance().getTextConvert(); switch (convertCTS) { case 0: break; case 1: content = ChineseUtils.toSimplified(content); break; case 2: content = ChineseUtils.toTraditional(content); break; } return content; } /** * 替换净化 */ public String replaceContent(String bookName, String bookTag, String content, Boolean replaceEnable) { if (!replaceEnable) return toTraditional(content); if (ReplaceRuleManager.getEnabled().size() == 0) return toTraditional(content); //替换 for (ReplaceRuleBean replaceRule : ReplaceRuleManager.getEnabled()) { if (isUseTo(replaceRule.getUseTo(), bookTag, bookName)) { { try { // 因为这里获取不到context,就不使用getString(R.string.replace_ad)了 if (replaceRule.getReplaceSummary().matches("^广告话术(-.*|$)")) { // 跳过太短的文本 if (content.length() > 100) content = replaceAd2(content, replaceRule.getRegex()); } else content = content.replaceAll(replaceRule.getFixedRegex(), replaceRule.getReplacement()); } catch (Exception e) { e.printStackTrace(); } } } } return toTraditional(content); } // 緩存生成的廣告規則正則表達式 // private Map adMap = new HashMap<>(); private Map adMap = new HashMap<>(); // 缓存长表达式,使用普通方式替换 private Map adMapL = new HashMap<>(); // 使用广告话术规则对正文进行替换,此方法为正则算法,效率较高,但是有漏失,故暂时放弃使用 private String replaceAd(String content, String replaceRule) { // replaceRule只对选择的内容进行了切片,不包含正则 if (replaceRule == null) return content; replaceRule = replaceRule.substring(2, replaceRule.length() - 2).trim(); // Pattern rule = adMap.get(replaceRule); String rule = adMap.get(replaceRule); StringBuffer buffer = new StringBuffer(replaceRule.length() * 2); if (rule == null) { String rules[] = replaceRule.split("\n"); for (String s : rules) { s = s.trim(); if (s.length() < 1) continue; // 如果规则只包含特殊字符,且长度大于2,直接替换。如果长度不大于2,会在自动扩大范围的过程中包含字符 if (s.matches("\\p{P}*")) { if (s.length() > 2) { if (buffer.length() > 0) buffer.append('|'); buffer.append(Pattern.quote(s)); } } else { // 如果规则不止包含特殊字符,需要移除首尾的特殊字符,把中间的空字符转换为\s+,把其他特殊字符转换为转义符 if (buffer.length() > 0) buffer.append('|'); buffer.append(s .replaceFirst("^\\p{P}+", "") .replaceFirst("\\p{P}$", "") .replaceAll("\\s+", "xxsp") .replaceAll("(\\p{P})", "(\\\\p{P}?)") .replaceAll("xxsp", "\\s+") ); } } // 广告话术至少出现两次 // rule = Pattern.compile("((" + buffer + ")(\\p{P}{0,2})){1,10}(" + buffer + ")"); rule = ("((" + buffer.toString() + ")(\\p{P}{0,2})){1,20}(" + buffer.toString() + ")((\\p{P}{0,12})(?=\\p{P}{2}))?"); adMap.put(replaceRule, rule); } content = content.replaceAll(rule, ""); rule = adMapL.get(replaceRule); if (rule == null) { String rules[] = replaceRule.split("\n"); buffer = new StringBuffer(replaceRule.length() * 2); for (String s : rules) { s = s.trim(); if (s.length() < 1) continue; if (s.length() > 6) { if (buffer.length() > 0) buffer.append('|'); buffer.append(Pattern.quote(s)); } } rule = "(" + buffer.toString() + ")"; adMapL.put(replaceRule, rule); } // Pattern p=Pattern.compile(rule); content = content.replaceAll(rule, ""); return content; } // 緩存生成的廣告 原文規則+正则扩展 // 原文与正则的最大区别,在于正则匹配规则对特殊符号的处理是保守的 private Map AdPatternMap = new HashMap<>(); // 不包含符号的文本形式的规则缓存。用于广告规则的第二次替换,以解决如下问题: 规则有 abc def,而实际出现了adefbc private Map AdStringDict = new HashMap<>(); // 使用广告话术规则对正文进行替换,此方法 使用Matcher匹配,合并相邻区域,再StringBlock.getResult()的算法取回没有被替换的部分 // 广告话术规则的相关代码可能存在以下问题: 零宽断言书写错误, \p{P}的使用(比如我最开始不知道\p{P}是不包含\\s的), getResult.remove()的算法(为了方便调试专门写了verify方法) private String replaceAd2(String content, String replaceRule) { if (replaceRule == null) return content; StringBlock block = new StringBlock(content); Pattern rule = AdPatternMap.get(replaceRule); String stringDict = AdStringDict.get(replaceRule); if (rule == null) { StringBuffer bufferRegex = new StringBuffer(replaceRule.length() * 3); StringBuffer bufferDict = new StringBuffer(); String rules[] = replaceRule.split("\n"); for (String s : rules) { s = s.trim(); if (s.length() < 1) continue; s = Pattern.quote(s); if (bufferRegex.length() > 0) bufferRegex.append('|'); else bufferRegex.append("(?=("); bufferRegex.append(s); } for (String s : rules) { s = s.trim(); if (s.length() < 1) continue; // 如果规则不止包含特殊字符,需要移除首尾的特殊字符,把中间的空字符转换为\s+,把其他特殊字符转换为转义符 if (!s.matches("[\\p{P}\\s]*")) { if (bufferRegex.length() > 0) bufferRegex.append('|'); else bufferRegex.append("(?=("); bufferRegex.append(s .replaceFirst("^\\p{P}+", "") .replaceFirst("\\p{P}$", "") .replaceAll("\\s+", "xxsp") .replaceAll("(\\p{P})", "(\\\\p{P}?)") .replaceAll("xxsp", "\\s+") ); } if (s.matches("[\\p{P}\\s]*[^\\p{P}]{4,}[\\p{P}\\s]*")) { bufferDict.append('\n'); bufferDict.append(s); } } bufferRegex.append("))((\\p{P}{0,12})(?=\\p{P}{2}))?"); rule = Pattern.compile(bufferRegex.toString()); AdPatternMap.put(replaceRule, rule); stringDict = bufferDict.toString(); AdStringDict.put(replaceRule, bufferDict.toString()); } Matcher matcher0 = rule.matcher(content); if (matcher0.groupCount() < 2) { // 构造的正则表达式分2个部分,第一部分匹配文字,第二部分匹配符号。完成匹配后实际已经不需要拆墙了 Log.w("replaceAd2", "2 > matcher0.group()==" + matcher0.groupCount()); return content; } while (matcher0.find()) { if (matcher0.group(2) != null) block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length() + matcher0.group(2).length()); else block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length()); Log.d("replaceAd2()", "Remove=" + block.verify()); } block.remove("(\\p{P}|\\s){1,6}([^\\p{P}]?(\\p{P}|\\s){1,6})?"); block.removeDict(stringDict); block.increase(5); return block.getResult(); } class StringBlock { // 保存字符串本体 private String string = ""; // 保存可以复制的区域,奇数为start,偶数为end。 private ArrayList list; // 保存删除的区域,用于校验 private ArrayList removed; public StringBlock(String string) { this.string = string; list = new ArrayList<>(); list.add(0); list.add(string.length()); removed = new ArrayList<>(); } // 验证删除操作是否有bug 验证OK输出正数,异常输出负数 public int verify() { // 验证list数列是否有异常 if (list.size() % 2 != 0) return -1; int p = list.get(0); if (p < 0) return -2; for (int i = 1; i < list.size(); i++) { int q = list.get(i); if (q <= p) return -3; p = q; } // 验证删除的区域是否还在list构成的区域内 for (int j = 0; j < removed.size() / 2; j++) { int j2 = removed.get(j * 2); int j2_1 = removed.get(j * 2 + 1); for (int i = 0; i < list.size() / 2; i++) { int i2_1 = list.get(i * 2 + 1); int i2 = list.get(i * 2); if (i2 > j2) { break; } if (i2_1 < j2) { continue; } if (i2_1 == j2) { if (i * 2 + 2 < list.size()) { if (list.get(i * 2 + 2) < j2_1) return -4; } } else { return -5; } } } return 0; } // 增加字符串的文本,避免被误删除 public void increase(int size) { ArrayList cache = new ArrayList<>(); if (list.get(0) > size) cache.add(list.get(0)); else cache.add(0); for (int i = 1; i < list.size() - 1; i = i + 2) { if (list.get(i + 1) - list.get(i) > size) { cache.add(list.get(i)); cache.add(list.get(i + 1)); } } if (string.length() - list.get(list.size() - 1) > size) cache.add(list.get(list.size() - 1)); else cache.add(string.length()); list = cache; } // 去除长度小于等于墙厚的区域 public void remove(int wallThick) { int j = list.size() / 2; ArrayList cache = new ArrayList<>(); for (int i = 0; i < j; i++) { int i2_1 = list.get(i * 2 + 1); int i2 = list.get(i * 2); if ((i2_1 - i2) > wallThick) { cache.add(i2); cache.add(i2_1); } } list = cache; } // 去除完全与正则匹配的区域 public void remove(String wall) { int j = list.size() / 2; ArrayList cache = new ArrayList<>(); for (int i = 0; i < j; i++) { int i2_1 = list.get(i * 2 + 1); int i2 = list.get(i * 2); if (!string.substring(i2, i2_1).matches(wall)) { cache.add(i2); cache.add(i2_1); } } list = cache; } public void removeDict(String dict) { // 如果孔穴的两端刚好匹配到同一词条,说明这是嵌套的广告话术 int j = list.size() / 2; // 缓存需要操作的参数 ArrayList cache = new ArrayList<>(); for (int i = 1; i < j; i++) { String str_s0 = getSubString(2 * i - 2).replaceFirst("[\\p{P}\\s]+$", ""); String str_s1 = str_s0.replaceFirst("^.*[\\p{P}\\s][^$]", ""); if (str_s1.length() < 1) continue; String str_e0 = getSubString(2 * i).replaceFirst("^[\\p{P}\\s]+", ""); String str_e1 = str_e0.replaceFirst("[\\p{P}\\s].*$", ""); if (str_e1.length() < 1) continue; // m 第一部分开始的位置 int m = list.get(i * 2 - 2) + str_s0.length() - str_s1.length(); // 第二部分结尾 int n = list.get(i * 2 + 1) - str_e0.length() + str_e1.length(); if (dict.matches("[\\s\\S]*(" + str_s1 + ")([^\\p{P}]*)(" + str_e1 + ")[\\s\\S]*")) { cache.add(m); cache.add(n); } else if (dict.matches("[\\s\\S]*(\n|^).*" + str_s1 + ".*(\n|\\s*$)[\\s\\S]*")) { cache.add(m); cache.add(list.get(i * 2)); } else if (dict.matches("[\\s\\S]*(\n|^).*" + str_e1 + ".*(\n|\\s*$)[\\s\\S]*")) { // 因为java.*不匹配\n cache.add(list.get(i * 2)); cache.add(n); } } for (int i = 0; i < cache.size() / 2; i++) { Log.d("removeDict", string.substring(cache.get(i * 2), cache.get((i * 2 + 1)))); remove(cache.get(i * 2), cache.get((i * 2 + 1))); } } public boolean remove(int start, int end) { if (start < 0 || end < 0 || start > string.length() || end > string.length() || start >= end) return false; removed.add(start); removed.add(end); int j = list.size() / 2; for (int i = 0; i < j; i++) { // start在有效区间中间和在区间的两个边缘,是不同的算法。 int i2_1 = list.get(i * 2 + 1); int i2 = list.get(i * 2); if (start < i2) return true; if (start == i2) { if (i2_1 > end) { list.set(i * 2, end); return true; } else { for (int k = 0; 2 * i + k < list.size(); k++) { if (list.get(k + 2 * i) > end) { if (k % 2 == 1) { list.set(2 * i + k - 1, end); } else { list.remove(i * 2); } for (int m = 0; m < k - 1; m++) list.remove(i * 2); return true; } } } } else if (i2 < start && i2_1 > start) { if (i2_1 > end) { list.add(i * 2 + 1, end); list.add(i * 2 + 1, start); return true; } else { list.set(i * 2 + 1, start); // i*2+2开始的元素可能需要被删除 for (int k = 2; 2 * i + k < list.size(); k++) { if (list.get(k + 2 * i) < end) continue; if (k % 2 == 1) { if (list.get(k + 2 * i) > end) { list.set(2 * i + k - 1, end); } } else { list.remove(i * 2 + 2); } for (int m = 0; m < k - 1; m++) list.remove(i * 2 + 2); return true; } } } } return false; } public String getResult() { StringBuffer buffer = new StringBuffer(string.length()); int j = list.size() / 2; if (j * 2 > list.size()) Log.e("StringBlock", "list.size=" + list.size()); for (int i = 0; i < j; i++) { buffer.append(string, list.get(i * 2), list.get(i * 2 + 1)); } return buffer.toString(); } public String getSubString(int start) { if (start >= 0 && start < list.size() - 1) return string.substring(list.get(start), list.get(start + 1)); return null; } } /** * 段落重排算法入口。把整篇内容输入,连接错误的分段,再把每个段落调用其他方法重新切分 * * @param content 正文 * @param chapterName 标题 * @return */ public static String LightNovelParagraph2(String content, String chapterName) { if (ReadBookControl.getInstance().getLightNovelParagraph()) { String _content; int chapterNameLength = chapterName.trim().length(); if (chapterNameLength > 1) { String regexp = chapterName.trim().replaceAll("\\s+", "(\\\\s*)"); // 质量较低的页面,章节内可能重复出现章节标题 if (chapterNameLength > 5) _content = content.replaceAll(regexp, "").trim(); else _content = content.replaceFirst("^\\s*" + regexp, "").trim(); } else { _content = content; } List dict = makeDict(_content); String[] p = _content .replaceAll(""", "“") .replaceAll("[::]['\"‘”“]+", ":“") .replaceAll("[\"”“]+[\\s]*[\"”“][\\s\"”“]*", "”\n“") .split("\n(\\s*)"); // 初始化StringBuffer的长度,在原content的长度基础上做冗余 StringBuffer buffer = new StringBuffer((int) (content.length() * 1.15)); // 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。 buffer.append(" "); if (!chapterName.trim().equals(p[0].trim())) { // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 buffer.append(p[0].replaceAll("[\u3000\\s]+", "")); } // 如果原文存在分段错误,需要把段落重新黏合 for (int i = 1; i < p.length; i++) { if (match(MARK_SENTENCES_END, buffer.charAt(buffer.length() - 1))) buffer.append("\n"); // 段落开头以外的地方不应该有空格 // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 buffer.append(p[i].replaceAll("[\u3000\\s]", "")); } // 预分段预处理 // ”“处理为”\n“。 // ”。“处理为”。\n“。不考虑“?” “!”的情况。 // ”。xxx处理为 ”。\n xxx p = buffer.toString() .replaceAll("[\"”“]+[\\s]*[\"”“]+", "”\n“") .replaceAll("[\"”“]+(?。!?!~)[\"”“]+", "”$1\n“") .replaceAll("[\"”“]+(?。!?!~)([^\"”“])", "”$1\n$2") .replaceAll("([问说喊唱叫骂道着答])[\\.。]", "$1。\n") // .replaceAll("([\\.。\\!!??])([^\"”“]+)[::][\"”“]", "$1\n$2:“") .split("\n"); buffer = new StringBuffer((int) (content.length() * 1.15)); for (String s : p) { buffer.append("\n"); buffer.append(FindNewLines(s, dict) ); } buffer = reduceLength(buffer); content = chapterName + "\n\n" + buffer.toString() //处理章节头部空格和换行 .replaceFirst("^\\s+", "") // 此规则会造成不规范引号被误换行,暂时无法解决,我认为利大于弊 // 例句:“你”“我”“他”都是一样的 // 误处理为 “你”\n“我”\n“他”都是一样的 // 而规范使用的标点不会被误处理: “你”、“我”、“他”,都是一样的。 .replaceAll("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*", "”\n“") // 规范 A:“B... .replaceAll("[::][”“\"\\s]+", ":“") // 处理奇怪的多余引号 \n”A:“B... 为 \nA:“B... .replaceAll("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)", "\n$1:“$3") .replaceAll("\n(\\s*)", "\n") // 处理“……” // .replaceAll("\n[\"”“][.,。,…]+\\s*[.,。,…]+[\"”“]","\n“……”") // 处理被错误断行的省略号。存在较高的误判,但是我认为利大于弊 .replaceAll("[.,。,…]+\\s*[.,。,…]+", "……") .replaceAll("\n([\\s::,,]+)", "\n") ; } return content; } /** * 从字符串提取引号包围,且不止出现一次的内容为字典 * * @param str * @return 词条列表 */ private static List makeDict(String str) { // 引号中间不包含任何标点,但是没有排除空格 Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\\p{P}]{1," + WORD_MAX_LENGTH + "})(?=[\"'”“])"); Matcher matcher = patten.matcher(str); List cache = new ArrayList<>(); List dict = new ArrayList<>(); List groups = new ArrayList<>(); while (matcher.find()) { String word = matcher.group(); String w = word.replaceAll("\\s+", ""); if (!groups.contains(word)) groups.add(word); if (!groups.contains(w)) groups.add(w); } for (String word : groups) { String w = word.replaceAll("\\s+", ""); if (cache.contains(w)) { if (!dict.contains(w)) { dict.add(w); if (!dict.contains(word)) dict.add(word); } } else { cache.add(w); cache.add(word); } } /* System.out.print("makeDict:"); for (String s : dict) System.out.print("\t" + s); System.out.print("\n"); */ return dict; } /** * 强制切分,减少段落内的句子 * 如果连续2对引号的段落没有提示语,进入对话模式。最后一对引号后强制切分段落 * 如果引号内的内容长于5句,可能引号状态有误,随机分段 * 如果引号外的内容长于3句,随机分段 * * @param str * @return */ private static StringBuffer reduceLength(StringBuffer str) { String[] p = str.toString().split("\n"); int l = p.length; boolean[] b = new boolean[l]; for (int i = 0; i < l; i++) { if (p[i].matches(PARAGRAPH_DIAGLOG)) b[i] = true; else b[i] = false; } int dialogue = 0; for (int i = 0; i < l; i++) { if (b[i]) { if (dialogue < 0) dialogue = 1; else if (dialogue < 2) dialogue++; } else { if (dialogue > 1) { p[i] = splitQuote(p[i]); dialogue--; } else if (dialogue > 0 && i < l - 2) { if (b[i + 1]) p[i] = splitQuote(p[i]); } } } StringBuffer string = new StringBuffer(); for (int i = 0; i < l; i++) { string.append('\n'); string.append(p[i]); // System.out.print(" "+b[i]); } // System.out.println(" " + str); return string; } // 强制切分进入对话模式后,未构成 “xxx” 形式的段落 private static String splitQuote(String str) { // System.out.println("splitQuote() " + str); int length = str.length(); if (length < 3) return str; if (match(MARK_QUOTATION, str.charAt(0))) { int i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1; if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1))) return str.substring(0, i) + "\n" + str.substring(i); } else if (match(MARK_QUOTATION, str.charAt(length - 1))) { int i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false); if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1))) return str.substring(0, i) + "\n" + str.substring(i); } return str; } /** * 计算随机插入换行符的位置。 * * @param str 字符串 * @param offset 传回的结果需要叠加的偏移量 * @param min 最低几个句子,随机插入换行 * @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行 * @return */ private static ArrayList forceSplit(String str, int offset, int min, int gain, int tigger) { ArrayList result = new ArrayList<>(); ArrayList array_end = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length() - 2, true); ArrayList array_mid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length() - 2, true); if (array_end.size() < tigger && array_mid.size() < tigger * 3) return result; int j = 0; for (int i = min; i < array_end.size(); i++) { int k = 0; for (; j < array_mid.size(); j++) { if (array_mid.get(j) < array_end.get(i)) k++; } if (Math.random() * gain < (0.8 + k / 2.5)) { result.add(array_end.get(i) + offset); i = Math.max(i + min, i); } } return result; } // 对内容重新划分段落.输入参数str已经使用换行符预分割 private static String FindNewLines(String str, List dict) { StringBuffer string = new StringBuffer(str); // 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。 List array_quote = new ArrayList<>(); // 标记忽略的引号 List array_ignore_quote = new ArrayList<>(); // 标记插入换行符的位置,int为插入位置(str的char下标) ArrayList ins_n = new ArrayList<>(); // 标记不需要插入换行符的位置。功能暂未实现。 ArrayList remove_n = new ArrayList<>(); // mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。 // 长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间 // 0未知,正数引号内,负数引号外。 // 如果相邻的两个标记都为+1,那么需要增加1个引号。 // 引号内不进行断句 int[] mod = new int[str.length()]; boolean wait_close = false; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (match(MARK_QUOTATION, c)) { int size = array_quote.size(); // 把“xxx”、“yy”和“z”合并为“xxx_yy_z”进行处理 if (size > 0) { int quote_pre = array_quote.get(size - 1); if (i - quote_pre == 2) { boolean remove = false; if (wait_close) { if (match(",,、/", str.charAt(i - 1))) { // 考虑出现“和”这种特殊情况 remove = true; } } else if (match(",,、/和与或", str.charAt(i - 1))) { remove = true; } if (remove) { string.setCharAt(i, '“'); string.setCharAt(i - 2, '”'); array_quote.remove(size - 1); mod[size - 1] = 1; mod[size] = -1; continue; } } } array_quote.add(i); // 为xxx:“xxx”做标记 if (i > 1) { // 当前发言的正引号的前一个字符 char char_b1 = str.charAt(i - 1); // 上次发言的正引号的前一个字符 char char_b2 = 0; if (match(MARK_QUOTATION_BEFORE, char_b1)) { // 如果不是第一处引号,寻找上一处断句,进行分段 if (array_quote.size() > 1) { int last_quote = array_quote.get(array_quote.size() - 2); int p = 0; if (char_b1 == ',' || char_b1 == ',') { if (array_quote.size() > 2) { p = array_quote.get(array_quote.size() - 3); if (p > 0) { char_b2 = str.charAt(p - 1); } } } // if(char_b2=='.' || char_b2=='。') if (match(MARK_SENTENCES_END_P, char_b2)) ins_n.add(p - 1); else if (match("的", char_b2)) { //剔除引号标记aaa的"xxs",bbb的“yyy” } else { int last_end = seekLast(str, MARK_SENTENCES_END, i, last_quote); if (last_end > 0) ins_n.add(last_end); else ins_n.add(last_quote); } } wait_close = true; mod[size] = 1; if (size > 0) { mod[size - 1] = -1; if (size > 1) { mod[size - 2] = 1; } /* int quote_pre = array_quote.get(array_quote.size() - 2); boolean flag_ins_n = false; for (int j = i; j > quote_pre; j--) { if (match(MARK_SENTENCES_END, string.charAt(j))) { ins_n.add(j); flag_ins_n = true; } } if (!flag_ins_n) ins_n.add(quote_pre); */ } } else if (wait_close) { { wait_close = false; ins_n.add(i); } } } } } int size = array_quote.size(); // 标记循环状态,此位置前的引号是否已经配对 boolean opend = false; if (size > 0) { // 第1次遍历array_quote,令其元素的值不为0 for (int i = 0; i < size; i++) { if (mod[i] > 0) { opend = true; } else if (mod[i] < 0) { // 连续2个反引号表明存在冲突,强制把前一个设为正引号 if (!opend) { if (i > 0) mod[i] = 3; } opend = false; } else { opend = !opend; if (opend) mod[i] = 2; else mod[i] = -2; } } // 修正,断尾必须封闭引号 if (opend) { if (array_quote.get(size - 1) - string.length() > -3) { // if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){ if (size > 1) mod[size - 2] = 4; // 0<=i=1 mod[size - 1] = -4; } else if (!match(MARK_SENTENCES_SAY, string.charAt(string.length() - 2))) string.append("”"); } // 第2次循环,mod[i]由负变正时,前1字符如果是句末,需要插入换行 int loop2_mod_1 = -1; //上一个引号跟随内容的状态 int loop2_mod_2; //当前引号跟随内容的状态 int i = 0; int j = array_quote.get(0) - 1; //当前引号前一字符的序号 if (j < 0) { i = 1; loop2_mod_1 = 0; } for (; i < size; i++) { j = array_quote.get(i) - 1; loop2_mod_2 = mod[i]; if (loop2_mod_1 < 0 && loop2_mod_2 > 0) { if (match(MARK_SENTENCES_END, string.charAt(j))) ins_n.add(j); } /* else if (mod[i - 1] > 0 && mod[i] < 0) { if (j > 0) { if (match(MARK_SENTENCES_END, string.charAt(j))) ins_n.add(j); } } */ loop2_mod_1 = loop2_mod_2; } } // 第3次循环,匹配并插入换行。 // "xxxx" xxxx。\n xxx“xxxx” // 未实现 // 使用字典验证ins_n , 避免插入不必要的换行。 // 由于目前没有插入、的列表,无法解决 “xx”、“xx”“xx” 被插入换行的问题 ArrayList _ins_n = new ArrayList<>(); for (int i : ins_n) { if (match("\"'”“", string.charAt(i))) { int start = seekLast(str, "\"'”“", i - 1, i - WORD_MAX_LENGTH); if (start > 0) { String word = str.substring(start + 1, i); if (dict.contains(word)) { // System.out.println("使用字典验证 跳过\tins_n=" + i + " word=" + word); // 引号内如果是字典词条,后方不插入换行符(前方不需要优化) remove_n.add(i); continue; } else { System.out.println("使用字典验证 插入\tins_n=" + i + " word=" + word); if (match("的地得和或", str.charAt(start))) { // xx的“xx”,后方不插入换行符(前方不需要优化) continue; } } } } else { // System.out.println("使用字典验证 else\tins_n=" + i + " substring=" + string.substring(i-5,i+5)); } _ins_n.add(i); } ins_n = _ins_n; // 随机在句末插入换行符 ins_n = new ArrayList(new HashSet(ins_n)); Collections.sort(ins_n); { String subs = ""; int j = 0; int progress = 0; int next_line = -1; if (ins_n.size() > 0) next_line = ins_n.get(j); int gain = 3; int min = 0; int trigger = 2; for (int i = 0; i < array_quote.size(); i++) { int qutoe = array_quote.get(i); if (qutoe > 0) { gain = 4; min = 2; trigger = 4; } else { gain = 3; min = 0; trigger = 2; } // 把引号前的换行符与内容相间插入 for (; j < ins_n.size(); j++) { // 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 if (next_line >= qutoe) break; next_line = ins_n.get(j); if (progress < next_line) { subs = string.substring(progress, next_line); ins_n.addAll(forceSplit(subs, progress, min, gain, trigger)); progress = next_line + 1; } } if (progress < qutoe) { subs = string.substring(progress, qutoe + 1); ins_n.addAll(forceSplit(subs, progress, min, gain, trigger)); progress = qutoe + 1; } } for (; j < ins_n.size(); j++) { next_line = ins_n.get(j); if (progress < next_line) { subs = string.substring(progress, next_line); ins_n.addAll(forceSplit(subs, progress, min, gain, trigger)); progress = next_line + 1; } } if (progress < string.length()) { subs = string.substring(progress, string.length()); ins_n.addAll(forceSplit(subs, progress, min, gain, trigger)); } } // 根据段落状态修正引号方向、计算需要插入引号的位置 // ins_quote跟随array_quote ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' boolean[] ins_quote = new boolean[size]; opend = false; for (int i = 0; i < size; i++) { int p = array_quote.get(i); if (mod[i] > 0) { string.setCharAt(p, '“'); if (opend) ins_quote[i] = true; opend = true; } else if (mod[i] < 0) { string.setCharAt(p, '”'); opend = false; } else { opend = !opend; if (opend) string.setCharAt(p, '“'); else string.setCharAt(p, '”'); } } ins_n = new ArrayList(new HashSet(ins_n)); Collections.sort(ins_n); // 输出log进行检验 /* System.out.println("quote[i]:position/mod\t" + string); for (int i = 0; i < array_quote.size(); i++) { System.out.print(" [" + i + "]" + array_quote.get(i) + "/" + mod[i]); } System.out.print("\n"); System.out.print("ins_q:"); for (int i = 0; i < ins_quote.length; i++) { System.out.print(" " + ins_quote[i]); } System.out.print("\n"); System.out.print("ins_n:"); for (int i : ins_n) { System.out.print(" " + i); } System.out.print("\n"); */ // 完成字符串拼接(从string复制、插入引号和换行 // ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' // ins_n 插入换行。数组的值表示插入换行符的位置 StringBuffer buffer = new StringBuffer((int) (str.length() * 1.15)); int j = 0; int progress = 0; int next_line = -1; if (ins_n.size() > 0) next_line = ins_n.get(j); for (int i = 0; i < array_quote.size(); i++) { int qutoe = array_quote.get(i); // 把引号前的换行符与内容相间插入 for (; j < ins_n.size(); j++) { // 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 if (next_line >= qutoe) break; next_line = ins_n.get(j); buffer.append(string, progress, next_line + 1); buffer.append('\n'); progress = next_line + 1; } if (progress < qutoe) { buffer.append(string, progress, qutoe + 1); progress = qutoe + 1; } if (ins_quote[i] && buffer.length() > 2) { if (buffer.charAt(buffer.length() - 1) == '\n') buffer.append('“'); else buffer.insert(buffer.length() - 1, "”\n"); } } for (; j < ins_n.size(); j++) { next_line = ins_n.get(j); if (progress <= next_line) { buffer.append(string, progress, next_line + 1); buffer.append('\n'); progress = next_line + 1; } } if (progress < string.length()) { buffer.append(string, progress, string.length()); } return buffer.toString(); } /** * 计算匹配到字典的每个字符的位置 * * @param str 待匹配的字符串 * @param key 字典 * @param from 从字符串的第几个字符开始匹配 * @param to 匹配到第几个字符结束 * @param inOrder 是否按照从前向后的顺序匹配 * @return 返回距离构成的ArrayList */ private static ArrayList seekIndexs(String str, String key, int from, int to, boolean inOrder) { ArrayList list = new ArrayList<>(); if (str.length() - from < 1) return list; int i = 0; if (from > i) i = from; int t = str.length(); if (to > 0) t = Math.min(t, to); char c; for (; i < t; i++) { if (inOrder) c = str.charAt(i); else c = str.charAt(str.length() - i - 1); if (key.indexOf(c) != -1) { list.add(i); } } return list; } /** * 计算字符串最后出现与字典中字符匹配的位置 * * @param str 数据字符串 * @param key 字典字符串 * @param from 从哪个字符开始匹配,默认最末位 * @param to 匹配到哪个字符(不包含此字符)默认0 * @return 位置(正向计算) */ private static int seekLast(String str, String key, int from, int to) { if (str.length() - from < 1) return -1; int i = str.length() - 1; if (from < i && i > 0) i = from; int t = 0; if (to > 0) t = to; char c; for (; i > t; i--) { c = str.charAt(i); if (key.indexOf(c) != -1) { return i; } } return -1; } /** * 计算字符串与字典中字符的最短距离 * * @param str 数据字符串 * @param key 字典字符串 * @param from 从哪个字符开始匹配,默认0 * @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位 * @param inOrder 是否从正向开始匹配 * @return 返回最短距离, 注意不是str的char的下标 */ private static int seekIndex(String str, String key, int from, int to, boolean inOrder) { if (str.length() - from < 1) return -1; int i = 0; if (from > i) i = from; int t = str.length(); if (to > 0) t = Math.min(t, to); char c; for (; i < t; i++) { if (inOrder) c = str.charAt(i); else c = str.charAt(str.length() - i - 1); if (key.indexOf(c) != -1) { return i; } } return -1; } /** * 计算字符串与字典的距离。 * * @param str 数据字符串 * @param form 从第几个字符开始匹配 * @param to 匹配到第几个字符串结束 * @param inOrder 是否从前向后匹配。 * @param words 可变长参数构成的字典。每个字符串代表一个字符 * @return 匹配结果。注意这个距离是使用第一个字符进行计算的 */ private static int seekWordsIndex(String str, int form, int to, boolean inOrder, String... words) { if (words.length < 1) return -2; int i = seekIndex(str, words[0], form, to, inOrder); if (i < 0) return i; for (int j = 1; j < words.length; j++) { int k = seekIndex(str, words[j], form, to, inOrder); if (inOrder) { if (i + j != k) return -3; } else { if (i - j != k) return -3; } } return i; } /* 搜寻引号并进行分段。处理了一、二、五三类常见情况 参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。 一、完整引用说话内容,在反引号内侧有断句标点。例如: 1) 丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!” 2)“哎呀,真是美极了!”皇帝说,“我十分满意!” 3)“怕什么!海的美就在这里!”我说道。 二、部分引用,在反引号外侧有断句标点: 4)适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。 5)现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。 6)唐朝的张嘉贞说它“制造奇特,人不知其所为”。 三、一段接着一段地直接引用时,中间段落只在段首用起引号,该段段尾却不用引回号。但是正统文学不在考虑范围内。 四、引号里面又要用引号时,外面一层用双引号,里面一层用单引号。暂时不需要考虑 五、反语和强调,周围没有断句符号。 */ // 段落换行符 private static String SPACE_BEFORE_PARAGRAPH = "\n "; // 段落末位的标点 private static String MARK_SENTENCES = "?。!?!~”\""; // 句子结尾的标点。因为引号可能存在误判,不包含引号。 private static String MARK_SENTENCES_END = "?。!?!~"; private static String MARK_SENTENCES_END_P = ".?。!?!~"; // 句中标点,由于某些网站常把“,”写为".",故英文句点按照句中标点判断 private static String MARK_SENTENCES_MID = ".,、,—…"; private static String MARK_SENTENCES_F = "啊嘛吧吗噢哦了呢呐"; private static String MARK_SENTENCES_SAY = "问说喊唱叫骂道着答"; // XXX说:“”的冒号 private static String MARK_QUOTATION_BEFORE = ",:,:"; // 引号 private static String MARK_QUOTATION = "\"“”"; private static String PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$"; // 限制字典的长度 private static int WORD_MAX_LENGTH = 16; private static boolean isFullSentences(String s) { if (s.length() < 2) return false; char c = s.charAt(s.length() - 1); return MARK_SENTENCES.indexOf(c) != -1; } private static boolean match(String rule, char chr) { return rule.indexOf(chr) != -1; } private boolean isUseTo(String useTo, String bookTag, String bookName) { return TextUtils.isEmpty(useTo) || useTo.contains(bookTag) || useTo.contains(bookName); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/CrashHandler.java ================================================ package com.kunfei.bookshelf.help; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.widget.Toast; import java.io.File; import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Field; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 异常管理类 */ public class CrashHandler implements Thread.UncaughtExceptionHandler { /** * 系统默认UncaughtExceptionHandler */ private Thread.UncaughtExceptionHandler mDefaultHandler; /** * context */ private Context mContext; /** * 存储异常和参数信息 */ private Map paramsMap = new HashMap<>(); /** * 格式化时间 */ @SuppressLint("SimpleDateFormat") private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); private String TAG = this.getClass().getSimpleName(); @SuppressLint("StaticFieldLeak") private static CrashHandler mInstance; private CrashHandler() { } /** * 获取CrashHandler实例 */ public static synchronized CrashHandler getInstance() { if (null == mInstance) { mInstance = new CrashHandler(); } return mInstance; } public void init(Context context) { mContext = context; mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); //设置该CrashHandler为系统默认的 Thread.setDefaultUncaughtExceptionHandler(this); } /** * uncaughtException 回调函数 */ @Override public void uncaughtException(Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null) { //如果自己没处理交给系统处理 mDefaultHandler.uncaughtException(thread, ex); } else { //自己处理 try {//延迟3秒杀进程 Thread.sleep(3000); } catch (InterruptedException e) { Log.e(TAG, "error : ", e); } } } /** * 收集错误信息.发送到服务器 * * @return 处理了该异常返回true, 否则false */ private boolean handleException(Throwable ex) { if (ex == null) { return false; } //收集设备参数信息 collectDeviceInfo(mContext); //添加自定义信息 addCustomInfo(); try { //使用Toast来显示异常信息 new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(mContext, ex.getMessage(), Toast.LENGTH_LONG).show()); } catch (Exception ignored) { } //保存日志文件 saveCrashInfo2File(ex); return false; } /** * 收集设备参数信息 */ private void collectDeviceInfo(Context ctx) { //获取versionName,versionCode try { PackageManager pm = ctx.getPackageManager(); PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES); if (pi != null) { String versionName = pi.versionName == null ? "null" : pi.versionName; String versionCode = pi.versionCode + ""; paramsMap.put("versionName", versionName); paramsMap.put("versionCode", versionCode); } } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "an error occured when collect package info", e); } //获取所有系统信息 Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); paramsMap.put(field.getName(), field.get(null).toString()); } catch (Exception e) { Log.e(TAG, "an error occured when collect crash info", e); } } } /** * 添加自定义参数 */ private void addCustomInfo() { Log.i(TAG, "addCustomInfo: 程序出错了..."); } /** * 保存错误信息到文件中 */ private void saveCrashInfo2File(Throwable ex) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : paramsMap.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); sb.append(key).append("=").append(value).append("\n"); } Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); ex.printStackTrace(printWriter); Throwable cause = ex.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); String result = writer.toString(); sb.append(result); try { long timestamp = System.currentTimeMillis(); String time = format.format(new Date()); String fileName = "crash-" + time + "-" + timestamp + ".log"; String path = FileHelp.getCachePath() + "/crash/"; File dir = new File(path); if (!dir.exists()) { //noinspection ResultOfMethodCallIgnored dir.mkdirs(); } FileOutputStream fos = new FileOutputStream(path + fileName); fos.write(sb.toString().getBytes()); Log.i(TAG, "saveCrashInfo2File: " + sb.toString()); fos.close(); } catch (Exception e) { Log.e(TAG, "an error occured while writing file...", e); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/DefaultValueHelper.kt ================================================ package com.kunfei.bookshelf.help import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.bean.BookSourceBean import com.kunfei.bookshelf.utils.GSON import com.kunfei.bookshelf.utils.fromJsonObject import java.io.File object DefaultValueHelper { val xxlSource: BookSourceBean by lazy { val json = String( MApplication.getInstance().assets.open("data${File.separator}BookSourceXxl.json") .readBytes() ) GSON.fromJsonObject(json)!! } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/DocumentHelper.java ================================================ package com.kunfei.bookshelf.help; import android.graphics.Bitmap; import android.net.Uri; import androidx.documentfile.provider.DocumentFile; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.utils.DocumentUtil; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Created by PureDark on 2016/9/24. */ public class DocumentHelper { public static boolean isFileExist(String fileName, String rootPath, String... subDirs) { return DocumentUtil.isFileExist(MApplication.getInstance(), fileName, rootPath, subDirs); } public static DocumentFile getDirDocument(String rootPath, String... subDirs) { return DocumentUtil.getDirDocument(MApplication.getInstance(), rootPath, subDirs); } public static DocumentFile createFileIfNotExist(String fileName, String path, String... subDirs) { if (!path.startsWith("content://")) path = "file://" + Uri.decode(path); return DocumentUtil.createFileIfNotExist(MApplication.getInstance(), fileName, path, subDirs); } public static DocumentFile createDirIfNotExist(String path, String... subDirs) { if (!path.startsWith("content://")) path = "file://" + Uri.decode(path); return DocumentUtil.createDirIfNotExist(MApplication.getInstance(), path, subDirs); } public static boolean deleteFile(String fileName, String rootPath, String... subDirs) { if (!rootPath.startsWith("content://")) rootPath = "file://" + Uri.decode(rootPath); return DocumentUtil.deleteFile(MApplication.getInstance(), fileName, rootPath, subDirs); } public static boolean writeString(String string, DocumentFile file) { return DocumentUtil.writeBytes(MApplication.getInstance(), string.getBytes(), file); } public static boolean writeString(String string, String fileName, String rootPath, String... subDirs) { if (!rootPath.startsWith("content://")) rootPath = "file://" + Uri.decode(rootPath); return DocumentUtil.writeBytes(MApplication.getInstance(), string.getBytes(), fileName, rootPath, subDirs); } public static String readString(String fileName, String rootPath, String... subDirs) { byte[] data = DocumentUtil.readBytes(MApplication.getInstance(), fileName, rootPath, subDirs); String string = null; try { string = new String(data, "utf-8"); } catch (Exception e) { e.printStackTrace(); } return string; } public static String readString(Uri uri) { byte[] data = DocumentUtil.readBytes(MApplication.getInstance(), uri); String string = null; try { string = new String(data, "utf-8"); } catch (Exception e) { e.printStackTrace(); } return string; } public static String readString(DocumentFile file) { byte[] data = DocumentUtil.readBytes(MApplication.getInstance(), file); String string = null; try { string = new String(data, "utf-8"); } catch (Exception e) { e.printStackTrace(); } return string; } public static boolean writeBytes(byte[] data, String fileName, String rootPath, String... subDirs) { if (!rootPath.startsWith("content://")) rootPath = "file://" + Uri.decode(rootPath); return DocumentUtil.writeBytes(MApplication.getInstance(), data, fileName, rootPath, subDirs); } public static boolean writeBytes(byte[] data, DocumentFile file) { if (file == null) return false; return DocumentUtil.writeBytes(MApplication.getInstance(), data, file); } public static boolean writeFromFile(File fromFile, DocumentFile file) { if (file == null) return false; try { return DocumentUtil.writeFromInputStream(MApplication.getInstance(), new FileInputStream(fromFile), file); } catch (FileNotFoundException e) { e.printStackTrace(); return false; } } public static boolean writeFromInputStream(InputStream inStream, DocumentFile file) { if (file == null) return false; return DocumentUtil.writeFromInputStream(MApplication.getInstance(), inStream, file); } public static void saveBitmapToFile(Bitmap bitmap, DocumentFile file) throws IOException { saveBitmapToFile(bitmap, file.getUri()); } public static void saveBitmapToFile(Bitmap bitmap, Uri fileUri) throws IOException { OutputStream out = MApplication.getInstance().getContentResolver().openOutputStream(fileUri); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); assert out != null; out.flush(); out.close(); } public static OutputStream getFileOutputSteam(String fileName, String rootPath, String... subDirs) { if (!rootPath.startsWith("content://")) rootPath = "file://" + Uri.decode(rootPath); return DocumentUtil.getFileOutputSteam(MApplication.getInstance(), fileName, rootPath, subDirs); } public static InputStream getFileInputSteam(String fileName, String rootPath, String... subDirs) { if (!rootPath.startsWith("content://")) rootPath = "file://" + Uri.decode(rootPath); return DocumentUtil.getFileInputSteam(MApplication.getInstance(), fileName, rootPath, subDirs); } public static String filenameFilter(String str) { return DocumentUtil.filenameFilter(str); } public static byte[] getBytes(File file) { byte[] buffer = null; try { FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(1000); byte[] b = new byte[1000]; 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; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/Donate.java ================================================ package com.kunfei.bookshelf.help; import android.content.Context; import android.content.Intent; import android.net.Uri; import java.net.URLEncoder; /** * Created by GKF on 2017/12/18. * 捐赠 */ public class Donate { public static void aliDonate(Context context) { try { String qrCode = URLEncoder.encode("https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=web-other", "utf-8"); String aliPayQr = "alipayqr://platformapi/startapp?saId=10000007&qrcode=" + qrCode + "&_t=" + System.currentTimeMillis(); openUri(context, aliPayQr); } catch (Exception e) { e.printStackTrace(); } } /** * 发送一个intent */ private static void openUri(Context context, String s) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s)); context.startActivity(intent); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/EncodeConverter.java ================================================ package com.kunfei.bookshelf.help; import android.text.TextUtils; import com.kunfei.bookshelf.utils.EncodingDetect; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.charset.Charset; import okhttp3.MediaType; import okhttp3.ResponseBody; import retrofit2.Converter; import retrofit2.Retrofit; public class EncodeConverter extends Converter.Factory { private String encode; private EncodeConverter() { } private EncodeConverter(String encode) { this.encode = encode; } public static EncodeConverter create() { return new EncodeConverter(); } public static EncodeConverter create(String en) { return new EncodeConverter(en); } @Override public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { return value -> { byte[] responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes()); if (!TextUtils.isEmpty(encode)) { try { return new String((responseBytes), Charset.forName(encode)); } catch (Exception ignored) { } } String charsetStr; MediaType mediaType = value.contentType(); //根据http头判断 if (mediaType != null) { Charset charset = mediaType.charset(); if (charset != null) { return new String((responseBytes), charset); } } //根据内容判断 charsetStr = EncodingDetect.getEncodeInHtml(responseBytes); return new String(responseBytes, Charset.forName(charsetStr)); }; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/ExoPlayerHelper.kt ================================================ package com.kunfei.bookshelf.help import android.net.Uri import com.google.android.exoplayer2.C import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.dash.DashMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource import com.google.android.exoplayer2.util.Util.inferContentType import com.kunfei.bookshelf.base.BaseModelImpl object ExoPlayerHelper { fun createMediaSource(uri: Uri, overrideExtension: String?): MediaSource { val mediaItem = MediaItem.fromUri(uri) val dataSourceFactory = OkHttpDataSource.Factory(BaseModelImpl.getClient()) val mediaSourceFactory = when (inferContentType(uri, overrideExtension)) { C.TYPE_SS -> SsMediaSource.Factory(dataSourceFactory) C.TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory) C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) else -> ProgressiveMediaSource.Factory(dataSourceFactory) } return mediaSourceFactory.createMediaSource(mediaItem) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/FileHelp.java ================================================ package com.kunfei.bookshelf.help; import android.os.Environment; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.utils.IOUtils; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; import io.reactivex.Single; /** * Created by newbiechen on 17-5-11. */ @SuppressWarnings("ALL") public class FileHelp { public static final byte BLANK = 0x0a; //采用自己的格式去设置文件,防止文件被系统文件查询到 public static final String SUFFIX_NB = ".nb"; public static final String SUFFIX_TXT = ".txt"; public static final String SUFFIX_EPUB = ".epub"; public static final String SUFFIX_PDF = ".pdf"; //获取文件夹 public static File getFolder(String filePath) { File file = new File(filePath); //如果文件夹不存在,就创建它 if (!file.exists()) { file.mkdirs(); } return file; } //获取文件 public static synchronized File createFileIfNotExist(String filePath) { File file = new File(filePath); try { if (!file.exists()) { //创建父类文件夹 getFolder(file.getParent()); //创建文件 file.createNewFile(); } } catch (IOException e) { } return file; } //获取Cache文件夹 public static String getFilesPath() { if (isSdCardExist()) { try { return MApplication.getInstance() .getExternalFilesDir(null) .getAbsolutePath(); } catch (Exception ignored) { } } return MApplication.getInstance() .getFilesDir() .getAbsolutePath(); } //获取Cache文件夹 public static String getCachePath() { if (isSdCardExist()) { try { return MApplication.getInstance() .getExternalCacheDir() .getAbsolutePath(); } catch (Exception ignored) { } } return MApplication.getInstance() .getCacheDir() .getAbsolutePath(); } public static long getDirSize(File file) { //判断文件是否存在 if (file.exists()) { //如果是目录则递归计算其内容的总大小 if (file.isDirectory()) { File[] children = file.listFiles(); long size = 0; for (File f : children) size += getDirSize(f); return size; } else { return file.length(); } } else { return 0; } } public static String getFileSize(long size) { if (size <= 0) return "0"; final String[] units = new String[]{"b", "kb", "M", "G", "T"}; //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 return new DecimalFormat("#,##0.##").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; } /** * 本来是获取File的内容的。但是为了解决文本缩进、换行的问题 * 这个方法就是专门用来获取书籍的... *

* 应该放在BookRepository中。。。 * * @param file * @return */ public static String getFileContent(File file) { Reader reader = null; String str = null; StringBuilder sb = new StringBuilder(); try { reader = new FileReader(file); BufferedReader br = new BufferedReader(reader); while ((str = br.readLine()) != null) { //过滤空语句 if (!str.equals("")) { //由于sb会自动过滤\n,所以需要加上去 sb.append(" " + str + "\n"); } } } catch (FileNotFoundException e) { } catch (IOException e) { } finally { IOUtils.close(reader); } return sb.toString(); } //判断是否挂载了SD卡 public static boolean isSdCardExist() { if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { return true; } return false; } //递归删除文件夹下的数据 public static synchronized void deleteFile(String filePath) { File file = new File(filePath); if (!file.exists()) return; if (file.isDirectory()) { File[] files = file.listFiles(); for (File subFile : files) { String path = subFile.getPath(); deleteFile(path); } } //删除文件 file.delete(); } //由于递归的耗时问题,取巧只遍历内部三层 //获取txt文件 public static List getTxtFiles(String filePath, int layer) { List txtFiles = new ArrayList(); File file = new File(filePath); //如果层级为 3,则直接返回 if (layer == 3) { return txtFiles; } //获取文件夹 File[] dirs = file.listFiles( pathname -> { if (pathname.isDirectory() && !pathname.getName().startsWith(".")) { return true; } //获取txt文件 else if (pathname.getName().endsWith(".txt")) { txtFiles.add(pathname); return false; } else { return false; } } ); //遍历文件夹 for (File dir : dirs) { //递归遍历txt文件 txtFiles.addAll(getTxtFiles(dir.getPath(), layer + 1)); } return txtFiles; } //由于遍历比较耗时 public static Single> getSDTxtFile() { //外部存储卡路径 String rootPath = Environment.getExternalStorageDirectory().getPath(); return Single.create(e -> { List files = getTxtFiles(rootPath, 0); e.onSuccess(files); }); } public static String getFileSuffix(String filePath) { File file = new File(filePath); return getFileSuffix(file); } public static String getFileSuffix(File file) { if (file == null || !file.exists() || file.isDirectory()) { return ""; } String fileName = file.getName(); int dotIndex = fileName.lastIndexOf("."); return dotIndex > 0 ? fileName.substring(dotIndex) : ""; } public static void createFolderIfNotExists(String path) { File folder = new File(path); if (!folder.exists()) { folder.mkdirs(); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/IntentData.kt ================================================ package com.kunfei.bookshelf.help object IntentData { private val bigData: MutableMap = mutableMapOf() @Synchronized fun put(key: String, data: Any?) { data?.let { bigData[key] = data } } @Synchronized fun put(data: Any?): String { val key = System.currentTimeMillis().toString() data?.let { bigData[key] = data } return key } @Suppress("UNCHECKED_CAST") @Synchronized fun get(key: String?): T? { if (key == null) return null val data = bigData[key] bigData.remove(key) return data as? T } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/ItemTouchCallback.java ================================================ package com.kunfei.bookshelf.help; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.viewpager.widget.ViewPager; /** * Created by GKF on 2018/3/16. */ public class ItemTouchCallback extends ItemTouchHelper.Callback { private SwipeRefreshLayout swipeRefreshLayout; private ViewPager viewPager; public void setSwipeRefreshLayout(SwipeRefreshLayout swipeRefreshLayout) { this.swipeRefreshLayout = swipeRefreshLayout; } public void setViewPager(ViewPager viewPager) { this.viewPager = viewPager; } /** * Item操作的回调 */ private OnItemTouchCallbackListener onItemTouchCallbackListener; /** * 是否可以拖拽 */ private boolean isCanDrag = false; /** * 是否可以被滑动 */ private boolean isCanSwipe = false; /** * 设置Item操作的回调,去更新UI和数据源 */ public void setOnItemTouchCallbackListener(OnItemTouchCallbackListener onItemTouchCallbackListener) { this.onItemTouchCallbackListener = onItemTouchCallbackListener; } /** * 设置是否可以被拖拽 * * @param canDrag 是true,否false */ public void setDragEnable(boolean canDrag) { isCanDrag = canDrag; } /** * 设置是否可以被滑动 * * @param canSwipe 是true,否false */ public void setSwipeEnable(boolean canSwipe) { isCanSwipe = canSwipe; } /** * 当Item被长按的时候是否可以被拖拽 */ @Override public boolean isLongPressDragEnabled() { return isCanDrag; } /** * Item是否可以被滑动(H:左右滑动,V:上下滑动) */ @Override public boolean isItemViewSwipeEnabled() { return isCanSwipe; } /** * 当用户拖拽或者滑动Item的时候需要我们告诉系统滑动或者拖拽的方向 */ @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) {// GridLayoutManager // flag如果值是0,相当于这个功能被关闭 int dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT | ItemTouchHelper.UP | ItemTouchHelper.DOWN; int swipeFlag = 0; // create make return makeMovementFlags(dragFlag, swipeFlag); } else if (layoutManager instanceof LinearLayoutManager) {// linearLayoutManager LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; int orientation = linearLayoutManager.getOrientation(); int dragFlag = 0; int swipeFlag = 0; // 为了方便理解,相当于分为横着的ListView和竖着的ListView if (orientation == LinearLayoutManager.HORIZONTAL) {// 如果是横向的布局 swipeFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; } else if (orientation == LinearLayoutManager.VERTICAL) {// 如果是竖向的布局,相当于ListView dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; swipeFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; } return makeMovementFlags(dragFlag, swipeFlag); } return 0; } /** * 当Item被拖拽的时候被回调 * * @param recyclerView recyclerView * @param srcViewHolder 拖拽的ViewHolder * @param targetViewHolder 目的地的viewHolder */ @Override public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder srcViewHolder, @NonNull RecyclerView.ViewHolder targetViewHolder) { if (onItemTouchCallbackListener != null) { return onItemTouchCallbackListener.onMove(srcViewHolder.getAdapterPosition(), targetViewHolder.getAdapterPosition()); } return false; } @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { if (onItemTouchCallbackListener != null) { onItemTouchCallbackListener.onSwiped(viewHolder.getAdapterPosition()); } } @Override public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) { super.onSelectedChanged(viewHolder, actionState); final boolean swiping = actionState == ItemTouchHelper.ACTION_STATE_DRAG; if (swipeRefreshLayout != null) { swipeRefreshLayout.setEnabled(!swiping); } if (viewPager != null) { viewPager.requestDisallowInterceptTouchEvent(swiping); } } public interface OnItemTouchCallbackListener { /** * 当某个Item被滑动删除的时候 * * @param adapterPosition item的position */ void onSwiped(int adapterPosition); /** * 当两个Item位置互换的时候被回调 * * @param srcPosition 拖拽的item的position * @param targetPosition 目的地的Item的position * @return 开发者处理了操作应该返回true,开发者没有处理就返回false */ boolean onMove(int srcPosition, int targetPosition); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/JsExtensions.java ================================================ package com.kunfei.bookshelf.help; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.bean.CookieBean; import com.kunfei.bookshelf.constant.AppConst; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl; import com.kunfei.bookshelf.utils.MD5Utils; import com.kunfei.bookshelf.utils.StringUtils; import org.jsoup.Connection; import org.jsoup.Jsoup; import java.io.IOException; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import retrofit2.Response; @SuppressWarnings({"unused", "WeakerAccess"}) public interface JsExtensions { /** * js实现跨域访问,不能删 */ default String ajax(String urlStr) { try { AnalyzeUrl analyzeUrl = new AnalyzeUrl(urlStr, AnalyzeHeaders.getDefaultHeader()); Response response = BaseModelImpl.getInstance().getResponseO(analyzeUrl) .blockingFirst(); return response.body(); } catch (Exception e) { return e.getLocalizedMessage(); } } /** * js实现跨域访问,不能删 */ default Response getResponse(String urlStr) { try { AnalyzeUrl analyzeUrl = new AnalyzeUrl(urlStr, AnalyzeHeaders.getDefaultHeader()); return BaseModelImpl.getInstance().getResponseO(analyzeUrl) .blockingFirst(); } catch (Exception e) { return Response.success(e.getLocalizedMessage()); } } /** * js实现解码,不能删 */ default String base64Decoder(String base64) { return StringUtils.base64Decode(base64); } /** * 章节数转数字 */ default String toNumChapter(String s) { if (s == null) { return null; } Pattern pattern = Pattern.compile("(第)(.+?)(章)"); Matcher matcher = pattern.matcher(s); if (matcher.find()) { return matcher.group(1) + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3); } return s; } /** * js实现重定向拦截,不能删 */ default Connection.Response get(String urlStr, Map headers) throws IOException { return Jsoup.connect(urlStr) .sslSocketFactory(SSLSocketClient.getSSLSocketFactory()) .ignoreContentType(true) .followRedirects(false) .headers(headers) .method(Connection.Method.GET) .execute(); } /** * js实现重定向拦截,不能删 */ default Connection.Response post(String urlStr, String body, Map headers) throws IOException { return Jsoup.connect(urlStr) .sslSocketFactory(SSLSocketClient.getSSLSocketFactory()) .ignoreContentType(true) .followRedirects(false) .requestBody(body) .headers(headers) .method(Connection.Method.POST) .execute(); } default void putCache(String key, String value) { CookieBean cookie = new CookieBean(key, value); DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(cookie); } default String getCache(String key) { CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load(key); if (cookie == null) { return null; } return cookie.getCookie(); } default String md5Encode(String text) { return MD5Utils.strToMd5By32(text); } default String androidId() { return AppConst.INSTANCE.getAndroidId(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/LauncherIcon.java ================================================ package com.kunfei.bookshelf.help; import android.content.ComponentName; import android.content.pm.PackageManager; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; /** * Created by GKF on 2018/2/27. * 更换图标 */ public class LauncherIcon { private static PackageManager packageManager = MApplication.getInstance().getPackageManager(); private static ComponentName componentNameMain = new ComponentName(MApplication.getInstance(), "com.kunfei.bookshelf.view.activity.WelcomeActivity"); private static ComponentName componentNameBookMain = new ComponentName(MApplication.getInstance(), "com.kunfei.bookshelf.view.activity.WelcomeBookActivity"); public static void ChangeIcon(String icon) { if (icon.equals(MApplication.getInstance().getString(R.string.icon_book))) { if (packageManager.getComponentEnabledSetting(componentNameBookMain) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { //启用 packageManager.setComponentEnabledSetting(componentNameBookMain, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); //禁用 packageManager.setComponentEnabledSetting(componentNameMain, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } } else { if (packageManager.getComponentEnabledSetting(componentNameMain) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { //启用 packageManager.setComponentEnabledSetting(componentNameMain, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); //禁用 packageManager.setComponentEnabledSetting(componentNameBookMain, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } } } public static String getInUseIcon() { if (packageManager.getComponentEnabledSetting(componentNameBookMain) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { return MApplication.getInstance().getString(R.string.icon_book); } return MApplication.getInstance().getString(R.string.icon_main); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/MediaManager.java ================================================ package com.kunfei.bookshelf.help; import android.content.Context; import android.media.AudioManager; import android.media.MediaPlayer; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; /** * Created by GKF on 2018/1/9. * 播放音频 */ public class MediaManager { private static int VOLUME; private AudioManager audioManager; private int stream; private final int FADE_DURATION = 1000; private final int FADE_INTERVAL = 100; private boolean isFading = false; private boolean cancelFading = false; public static MediaManager instance; private MediaManager() { audioManager = (AudioManager) MApplication.getInstance().getSystemService(Context.AUDIO_SERVICE); } public void setStream(int stream) { this.stream = stream; getSysVolume(); } public static synchronized MediaManager getInstance() { if (instance == null) instance = new MediaManager(); return instance; } public void fadeInVolume() { if (!isFading) { getSysVolume(); } else { cancelFading = true; } while (isFading) try { Thread.sleep(10); } catch (Exception ignored) { } cancelFading = false; startAudioFade(1, VOLUME); setSysVolume(VOLUME); } public void fadeOutVolume() { if (!isFading) { getSysVolume(); } else { cancelFading = true; } while (isFading) try { Thread.sleep(FADE_INTERVAL); } catch (Exception ignored) { } cancelFading = false; startAudioFade(VOLUME, 1); setSysVolume(VOLUME); } private void getSysVolume() { VOLUME = audioManager.getStreamVolume(stream); } private void setSysVolume(float vol) { audioManager.setStreamVolume(stream, (int) vol, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); } private void startAudioFade(float from, float to) { isFading = true; cancelFading = false; int numberOfSteps = FADE_DURATION / FADE_INTERVAL; float deltaVolume = (to - from) / numberOfSteps; for (float vol = from; (vol - to) * (vol - from) <= 0 && !cancelFading; vol += deltaVolume) { setSysVolume(vol); try { Thread.sleep(FADE_INTERVAL); } catch (Exception ignored) { } } isFading = false; cancelFading = false; } public static void playSilentSound(Context mContext) { try { // Stupid Android 8 "Oreo" hack to make media buttons work MediaPlayer mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent_sound); mMediaPlayer.setOnCompletionListener(MediaPlayer::release); mMediaPlayer.start(); } catch (Exception ignored) { } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/ProcessTextHelp.java ================================================ package com.kunfei.bookshelf.help; import android.content.ComponentName; import android.content.pm.PackageManager; import com.kunfei.bookshelf.MApplication; public class ProcessTextHelp { private static PackageManager packageManager = MApplication.getInstance().getPackageManager(); private static ComponentName componentName = new ComponentName(MApplication.getInstance(), "com.kunfei.bookshelf.view.activity.ReceivingSharedActivity"); public static boolean isProcessTextEnabled() { return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; } public static void setProcessTextEnable(boolean enable) { if (enable) { packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } else { packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/ReadBookControl.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.help; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.provider.Settings; import android.util.DisplayMetrics; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.utils.BitmapUtil; import com.kunfei.bookshelf.utils.MeUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.kunfei.bookshelf.widget.page.PageLoader.DEFAULT_MARGIN_WIDTH; public class ReadBookControl { private static final int DEFAULT_BG = 1; private int textDrawableIndex = DEFAULT_BG; private List> textDrawable; private Bitmap bgBitmap; private int screenDirection; private int speechRate; private boolean speechRateFollowSys; private int textSize; private int textColor; private boolean bgIsColor; private int bgColor; private float lineMultiplier; private float paragraphSize; private int pageMode; private Boolean lightNovelParagraph; private Boolean hideStatusBar; private Boolean hideNavigationBar; private String fontPath; private int textConvert; private int navBarColor; private Boolean textBold; private Boolean canClickTurn; private Boolean canKeyTurn; private Boolean readAloudCanKeyTurn; private int CPM; private Boolean clickAllNext; private Boolean showTitle; private Boolean showTimeBattery; private Boolean showLine; private Boolean darkStatusIcon; private int indent; private int screenTimeOut; private int paddingLeft; private int paddingTop; private int paddingRight; private int paddingBottom; private int tipPaddingLeft; private int tipPaddingTop; private int tipPaddingRight; private int tipPaddingBottom; private float textLetterSpacing; private boolean canSelectText; public int minCPM = 200; public int maxCPM = 2000; private int defaultCPM = 500; private SharedPreferences preferences; private static ReadBookControl readBookControl; public static ReadBookControl getInstance() { if (readBookControl == null) { synchronized (ReadBookControl.class) { if (readBookControl == null) { readBookControl = new ReadBookControl(); } } } return readBookControl; } private ReadBookControl() { preferences = MApplication.getConfigPreferences(); initTextDrawable(); updateReaderSettings(); } public void updateReaderSettings() { this.lightNovelParagraph = preferences.getBoolean("light_novel_paragraph", false); this.hideStatusBar = preferences.getBoolean("hide_status_bar", false); this.hideNavigationBar = preferences.getBoolean("hide_navigation_bar", false); this.indent = preferences.getInt("indent", 2); this.textSize = preferences.getInt("textSize", 20); this.canClickTurn = preferences.getBoolean("canClickTurn", true); this.canKeyTurn = preferences.getBoolean("canKeyTurn", true); this.readAloudCanKeyTurn = preferences.getBoolean("readAloudCanKeyTurn", false); this.lineMultiplier = preferences.getFloat("lineMultiplier", 1); this.paragraphSize = preferences.getFloat("paragraphSize", 1); this.CPM = preferences.getInt("CPM", defaultCPM) > maxCPM ? minCPM : preferences.getInt("CPM", defaultCPM); this.clickAllNext = preferences.getBoolean("clickAllNext", false); this.fontPath = preferences.getString("fontPath", null); this.textConvert = preferences.getInt("textConvertInt", 0); this.textBold = preferences.getBoolean("textBold", false); this.speechRate = preferences.getInt("speechRate", 10); this.speechRateFollowSys = preferences.getBoolean("speechRateFollowSys", true); this.showTitle = preferences.getBoolean("showTitle", true); this.showTimeBattery = preferences.getBoolean("showTimeBattery", true); this.showLine = preferences.getBoolean("showLine", true); this.screenTimeOut = preferences.getInt("screenTimeOut", 0); this.paddingLeft = preferences.getInt("paddingLeft", DEFAULT_MARGIN_WIDTH); this.paddingTop = preferences.getInt("paddingTop", 0); this.paddingRight = preferences.getInt("paddingRight", DEFAULT_MARGIN_WIDTH); this.paddingBottom = preferences.getInt("paddingBottom", 0); this.tipPaddingLeft = preferences.getInt("tipPaddingLeft", DEFAULT_MARGIN_WIDTH); this.tipPaddingTop = preferences.getInt("tipPaddingTop", 0); this.tipPaddingRight = preferences.getInt("tipPaddingRight", DEFAULT_MARGIN_WIDTH); this.tipPaddingBottom = preferences.getInt("tipPaddingBottom", 0); this.pageMode = preferences.getInt("pageMode", 0); this.screenDirection = preferences.getInt("screenDirection", 0); this.navBarColor = preferences.getInt("navBarColorInt", 0); this.textLetterSpacing = preferences.getFloat("textLetterSpacing", 0); this.canSelectText = preferences.getBoolean("canSelectText", false); initTextDrawableIndex(); } //阅读背景 private void initTextDrawable() { if (null == textDrawable) { textDrawable = new ArrayList<>(); Map temp1 = new HashMap<>(); temp1.put("textColor", Color.parseColor("#3E3D3B")); temp1.put("bgIsColor", 1); temp1.put("textBackground", Color.parseColor("#F3F3F3")); temp1.put("darkStatusIcon", 1); textDrawable.add(temp1); Map temp2 = new HashMap<>(); temp2.put("textColor", Color.parseColor("#5E432E")); temp2.put("bgIsColor", 1); temp2.put("textBackground", Color.parseColor("#C6BAA1")); temp2.put("darkStatusIcon", 1); textDrawable.add(temp2); Map temp3 = new HashMap<>(); temp3.put("textColor", Color.parseColor("#22482C")); temp3.put("bgIsColor", 1); temp3.put("textBackground", Color.parseColor("#E1F1DA")); temp3.put("darkStatusIcon", 1); textDrawable.add(temp3); Map temp4 = new HashMap<>(); temp4.put("textColor", Color.parseColor("#FFFFFF")); temp4.put("bgIsColor", 1); temp4.put("textBackground", Color.parseColor("#015A86")); temp4.put("darkStatusIcon", 0); textDrawable.add(temp4); Map temp5 = new HashMap<>(); temp5.put("textColor", Color.parseColor("#808080")); temp5.put("bgIsColor", 1); temp5.put("textBackground", Color.parseColor("#000000")); temp5.put("darkStatusIcon", 0); textDrawable.add(temp5); } } public void initTextDrawableIndex() { if (getIsNightTheme()) { textDrawableIndex = preferences.getInt("textDrawableIndexNight", 4); } else { textDrawableIndex = preferences.getInt("textDrawableIndex", DEFAULT_BG); } if (textDrawableIndex == -1) { textDrawableIndex = DEFAULT_BG; } initPageStyle(); setTextDrawable(); } @SuppressWarnings("ConstantConditions") private void initPageStyle() { int bgCustom = getBgCustom(textDrawableIndex); if ((bgCustom == 2 || bgCustom == 3) && getBgPath(textDrawableIndex) != null) { bgIsColor = false; String bgPath = getBgPath(textDrawableIndex); Resources resources = MApplication.getInstance().getResources(); DisplayMetrics dm = resources.getDisplayMetrics(); int width = dm.widthPixels; int height = dm.heightPixels; if (bgCustom == 2) { bgBitmap = BitmapUtil.getFitSampleBitmap(bgPath, width, height); } else { bgBitmap = MeUtils.getFitAssetsSampleBitmap(MApplication.getInstance().getAssets(), bgPath, width, height); } if (bgBitmap != null) { return; } } else if (getBgCustom(textDrawableIndex) == 1) { bgIsColor = true; bgColor = getBgColor(textDrawableIndex); return; } bgIsColor = true; bgColor = textDrawable.get(textDrawableIndex).get("textBackground"); } private void setTextDrawable() { darkStatusIcon = getDarkStatusIcon(textDrawableIndex); textColor = getTextColor(textDrawableIndex); } public int getTextColor(int textDrawableIndex) { if (preferences.getInt("textColor" + textDrawableIndex, 0) != 0) { return preferences.getInt("textColor" + textDrawableIndex, 0); } else { return getDefaultTextColor(textDrawableIndex); } } public void setTextColor(int textDrawableIndex, int textColor) { preferences.edit() .putInt("textColor" + textDrawableIndex, textColor) .apply(); } @SuppressWarnings("ConstantConditions") public Drawable getBgDrawable(int textDrawableIndex, Context context, int width, int height) { int color; try { Bitmap bitmap = null; switch (getBgCustom(textDrawableIndex)) { case 3: bitmap = MeUtils.getFitAssetsSampleBitmap(context.getAssets(), getBgPath(textDrawableIndex), width, height); if (bitmap != null) { return new BitmapDrawable(context.getResources(), bitmap); } case 2: bitmap = BitmapUtil.getFitSampleBitmap(getBgPath(textDrawableIndex), width, height); if (bitmap != null) { return new BitmapDrawable(context.getResources(), bitmap); } break; case 1: color = getBgColor(textDrawableIndex); return new ColorDrawable(color); } if (textDrawable.get(textDrawableIndex).get("bgIsColor") != 0) { color = textDrawable.get(textDrawableIndex).get("textBackground"); return new ColorDrawable(color); } else { return getDefaultBgDrawable(textDrawableIndex, context); } } catch (Exception e) { if (textDrawable.get(textDrawableIndex).get("bgIsColor") != 0) { color = textDrawable.get(textDrawableIndex).get("textBackground"); return new ColorDrawable(color); } else { return getDefaultBgDrawable(textDrawableIndex, context); } } } @SuppressWarnings("ConstantConditions") public Drawable getDefaultBgDrawable(int textDrawableIndex, Context context) { if (textDrawable.get(textDrawableIndex).get("bgIsColor") != 0) { return new ColorDrawable(textDrawable.get(textDrawableIndex).get("textBackground")); } else { return context.getResources().getDrawable(getDefaultBg(textDrawableIndex)); } } public int getBgCustom(int textDrawableIndex) { return preferences.getInt("bgCustom" + textDrawableIndex, 0); } public void setBgCustom(int textDrawableIndex, int bgCustom) { preferences.edit() .putInt("bgCustom" + textDrawableIndex, bgCustom) .apply(); } public String getBgPath(int textDrawableIndex) { return preferences.getString("bgPath" + textDrawableIndex, null); } public void setBgPath(int textDrawableIndex, String bgUri) { preferences.edit() .putString("bgPath" + textDrawableIndex, bgUri) .apply(); } @SuppressWarnings("ConstantConditions") public int getDefaultTextColor(int textDrawableIndex) { return textDrawable.get(textDrawableIndex).get("textColor"); } @SuppressWarnings("ConstantConditions") private int getDefaultBg(int textDrawableIndex) { return textDrawable.get(textDrawableIndex).get("textBackground"); } public int getBgColor(int index) { return preferences.getInt("bgColor" + index, Color.parseColor("#1e1e1e")); } public void setBgColor(int index, int bgColor) { preferences.edit() .putInt("bgColor" + index, bgColor) .apply(); } private boolean getIsNightTheme() { return MApplication.getInstance().isNightTheme(); } public boolean getImmersionStatusBar() { return preferences.getBoolean("immersionStatusBar", false); } public void setImmersionStatusBar(boolean immersionStatusBar) { preferences.edit() .putBoolean("immersionStatusBar", immersionStatusBar) .apply(); } public int getTextSize() { return textSize; } public void setTextSize(int textSize) { this.textSize = textSize; preferences.edit() .putInt("textSize", textSize) .apply(); } public int getTextColor() { return textColor; } public boolean bgIsColor() { return bgIsColor; } public Drawable getTextBackground(Context context) { if (bgIsColor) { return new ColorDrawable(bgColor); } return new BitmapDrawable(context.getResources(), bgBitmap); } public int getBgColor() { return bgColor; } public boolean bgBitmapIsNull() { return bgBitmap == null || bgBitmap.isRecycled(); } public Bitmap getBgBitmap() { return bgBitmap.copy(Bitmap.Config.ARGB_8888, true); } public int getTextDrawableIndex() { return textDrawableIndex; } public void setTextDrawableIndex(int textDrawableIndex) { this.textDrawableIndex = textDrawableIndex; if (getIsNightTheme()) { preferences.edit() .putInt("textDrawableIndexNight", textDrawableIndex) .apply(); } else { preferences.edit() .putInt("textDrawableIndex", textDrawableIndex) .apply(); } setTextDrawable(); } public void setTextConvert(int textConvert) { this.textConvert = textConvert; preferences.edit() .putInt("textConvertInt", textConvert) .apply(); } public void setNavBarColor(int navBarColor) { this.navBarColor = navBarColor; preferences.edit() .putInt("navBarColorInt", navBarColor) .apply(); } public int getNavBarColor() { return navBarColor; } public void setTextBold(boolean textBold) { this.textBold = textBold; preferences.edit() .putBoolean("textBold", textBold) .apply(); } public void setReadBookFont(String fontPath) { this.fontPath = fontPath; preferences.edit() .putString("fontPath", fontPath) .apply(); } public String getFontPath() { return fontPath; } public int getTextConvert() { return textConvert == -1 ? 2 : textConvert; } public Boolean getTextBold() { return textBold; } public Boolean getCanKeyTurn(Boolean isPlay) { if (!canKeyTurn) { return false; } else if (readAloudCanKeyTurn) { return true; } else { return !isPlay; } } public Boolean getCanKeyTurn() { return canKeyTurn; } public void setCanKeyTurn(Boolean canKeyTurn) { this.canKeyTurn = canKeyTurn; preferences.edit() .putBoolean("canKeyTurn", canKeyTurn) .apply(); } public Boolean getAloudCanKeyTurn() { return readAloudCanKeyTurn; } public void setAloudCanKeyTurn(Boolean canAloudKeyTurn) { this.readAloudCanKeyTurn = canAloudKeyTurn; preferences.edit() .putBoolean("readAloudCanKeyTurn", canAloudKeyTurn) .apply(); } public Boolean getCanClickTurn() { return canClickTurn; } public void setCanClickTurn(Boolean canClickTurn) { this.canClickTurn = canClickTurn; preferences.edit() .putBoolean("canClickTurn", canClickTurn) .apply(); } public float getTextLetterSpacing() { return textLetterSpacing; } public void setTextLetterSpacing(float textLetterSpacing) { this.textLetterSpacing = textLetterSpacing; preferences.edit() .putFloat("textLetterSpacing", textLetterSpacing) .apply(); } public float getLineMultiplier() { return lineMultiplier; } public void setLineMultiplier(float lineMultiplier) { this.lineMultiplier = lineMultiplier; preferences.edit() .putFloat("lineMultiplier", lineMultiplier) .apply(); } public float getParagraphSize() { return paragraphSize; } public void setParagraphSize(float paragraphSize) { this.paragraphSize = paragraphSize; preferences.edit() .putFloat("paragraphSize", paragraphSize) .apply(); } public int getCPM() { return CPM; } public void setCPM(int cpm) { if (cpm < minCPM || cpm > maxCPM) cpm = defaultCPM; this.CPM = cpm; preferences.edit() .putInt("CPM", cpm) .apply(); } public Boolean getClickAllNext() { return clickAllNext; } public void setClickAllNext(Boolean clickAllNext) { this.clickAllNext = clickAllNext; preferences.edit() .putBoolean("clickAllNext", clickAllNext) .apply(); } public int getSpeechRate() { return speechRate; } public void setSpeechRate(int speechRate) { this.speechRate = speechRate; preferences.edit() .putInt("speechRate", speechRate) .apply(); } public boolean isSpeechRateFollowSys() { return speechRateFollowSys; } public void setSpeechRateFollowSys(boolean speechRateFollowSys) { this.speechRateFollowSys = speechRateFollowSys; preferences.edit() .putBoolean("speechRateFollowSys", speechRateFollowSys) .apply(); } public Boolean getShowTitle() { return showTitle; } public void setShowTitle(Boolean showTitle) { this.showTitle = showTitle; preferences.edit() .putBoolean("showTitle", showTitle) .apply(); } public Boolean getShowTimeBattery() { return showTimeBattery; } public void setShowTimeBattery(Boolean showTimeBattery) { this.showTimeBattery = showTimeBattery; preferences.edit() .putBoolean("showTimeBattery", showTimeBattery) .apply(); } public Boolean getLightNovelParagraph(){return lightNovelParagraph;} public void setLightNovelParagraph(Boolean lightNovelParagraph) { this.lightNovelParagraph = lightNovelParagraph; preferences.edit() .putBoolean("light_novel_paragraph", lightNovelParagraph) .apply(); } public Boolean getHideStatusBar() { return hideStatusBar; } public void setHideStatusBar(Boolean hideStatusBar) { this.hideStatusBar = hideStatusBar; preferences.edit() .putBoolean("hide_status_bar", hideStatusBar) .apply(); } public Boolean getToLh() { return preferences.getBoolean("toLh", false); } public void setToLh(Boolean toLh) { preferences.edit() .putBoolean("toLh", toLh) .apply(); } public Boolean getHideNavigationBar() { return hideNavigationBar; } public void setHideNavigationBar(Boolean hideNavigationBar) { this.hideNavigationBar = hideNavigationBar; preferences.edit() .putBoolean("hide_navigation_bar", hideNavigationBar) .apply(); } public Boolean getShowLine() { return showLine; } public void setShowLine(Boolean showLine) { this.showLine = showLine; preferences.edit() .putBoolean("showLine", showLine) .apply(); } public boolean getDarkStatusIcon() { return darkStatusIcon; } @SuppressWarnings("ConstantConditions") public boolean getDarkStatusIcon(int textDrawableIndex) { return preferences.getBoolean("darkStatusIcon" + textDrawableIndex, textDrawable.get(textDrawableIndex).get("darkStatusIcon") != 0); } public void setDarkStatusIcon(int textDrawableIndex, Boolean darkStatusIcon) { preferences.edit() .putBoolean("darkStatusIcon" + textDrawableIndex, darkStatusIcon) .apply(); } public int getScreenTimeOut() { return screenTimeOut; } public void setScreenTimeOut(int screenTimeOut) { this.screenTimeOut = screenTimeOut; preferences.edit() .putInt("screenTimeOut", screenTimeOut) .apply(); } public int getPaddingLeft() { return paddingLeft; } public void setPaddingLeft(int paddingLeft) { this.paddingLeft = paddingLeft; preferences.edit() .putInt("paddingLeft", paddingLeft) .apply(); } public int getPaddingTop() { return paddingTop; } public void setPaddingTop(int paddingTop) { this.paddingTop = paddingTop; preferences.edit() .putInt("paddingTop", paddingTop) .apply(); } public int getPaddingRight() { return paddingRight; } public void setPaddingRight(int paddingRight) { this.paddingRight = paddingRight; preferences.edit() .putInt("paddingRight", paddingRight) .apply(); } public int getPaddingBottom() { return paddingBottom; } public void setPaddingBottom(int paddingBottom) { this.paddingBottom = paddingBottom; preferences.edit() .putInt("paddingBottom", paddingBottom) .apply(); } public int getTipPaddingLeft() { return tipPaddingLeft; } public void setTipPaddingLeft(int tipPaddingLeft) { this.tipPaddingLeft = tipPaddingLeft; preferences.edit() .putInt("tipPaddingLeft", tipPaddingLeft) .apply(); } public boolean isCanSelectText() { return canSelectText; } public void setCanSelectText(boolean canSelectText) { this.canSelectText = canSelectText; preferences.edit() .putBoolean("canSelectText", canSelectText) .apply(); } public int getTipPaddingTop() { return tipPaddingTop; } public void setTipPaddingTop(int tipPaddingTop) { this.tipPaddingTop = tipPaddingTop; preferences.edit() .putInt("tipPaddingTop", tipPaddingTop) .apply(); } public int getTipPaddingRight() { return tipPaddingRight; } public void setTipPaddingRight(int tipPaddingRight) { this.tipPaddingRight = tipPaddingRight; preferences.edit() .putInt("tipPaddingRight", tipPaddingRight) .apply(); } public int getTipPaddingBottom() { return tipPaddingBottom; } public void setTipPaddingBottom(int tipPaddingBottom) { this.tipPaddingBottom = tipPaddingBottom; preferences.edit() .putInt("tipPaddingBottom", tipPaddingBottom) .apply(); } public int getPageMode() { return pageMode; } public void setPageMode(int pageMode) { this.pageMode = pageMode; preferences.edit() .putInt("pageMode", pageMode) .apply(); } public int getScreenDirection() { return screenDirection; } public void setScreenDirection(int screenDirection) { this.screenDirection = screenDirection; preferences.edit() .putInt("screenDirection", screenDirection) .apply(); } public void setIndent(int indent) { this.indent = indent; preferences.edit() .putInt("indent", indent) .apply(); } public int getIndent() { return indent; } public int getLight() { return preferences.getInt("light", getScreenBrightness()); } public void setLight(int light) { preferences.edit() .putInt("light", light) .apply(); } public Boolean getLightFollowSys() { return preferences.getBoolean("lightFollowSys", true); } public void setLightFollowSys(boolean isFollowSys) { preferences.edit() .putBoolean("lightFollowSys", isFollowSys) .apply(); } private int getScreenBrightness() { int value = 0; ContentResolver cr = MApplication.getInstance().getContentResolver(); try { value = Settings.System.getInt(cr, Settings.System.SCREEN_BRIGHTNESS); } catch (Settings.SettingNotFoundException ignored) { } return value; } public boolean disableScrollClickTurn() { return preferences.getBoolean("disableScrollClickTurn", false); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/SSLSocketClient.java ================================================ package com.kunfei.bookshelf.help; import android.annotation.SuppressLint; import java.security.SecureRandom; import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * Created by GKF on 2018/3/1. * 忽略证书 */ public class SSLSocketClient { //获取这个SSLSocketFactory public static SSLSocketFactory getSSLSocketFactory() { try { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, new TrustManager[]{createTrustAllManager()}, new SecureRandom()); return sslContext.getSocketFactory(); } catch (Exception e) { throw new RuntimeException(e); } } 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; } //获取HostnameVerifier public static HostnameVerifier getHostnameVerifier() { return (s, sslSession) -> true; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/SourceHelp.kt ================================================ package com.kunfei.bookshelf.help import android.os.Handler import android.os.Looper import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.bean.BookSourceBean import com.kunfei.bookshelf.model.BookSourceManager import com.kunfei.bookshelf.utils.EncoderUtils import com.kunfei.bookshelf.utils.splitNotBlank import org.jetbrains.anko.toast object SourceHelp { private val handler = Handler(Looper.getMainLooper()) private val list18Plus by lazy { try { return@lazy String(MApplication.getInstance().assets.open("18PlusList.txt").readBytes()) .splitNotBlank("\n") } catch (e: Exception) { return@lazy arrayOf() } } fun insertBookSource(vararg bookSources: BookSourceBean) { bookSources.forEach { bookSource -> if (is18Plus(bookSource.bookSourceUrl)) { handler.post { MApplication.getInstance().toast("${bookSource.bookSourceName}是18+网址,禁止导入.") } } else { BookSourceManager.addBookSource(bookSource) } } } private fun is18Plus(url: String?): Boolean { url ?: return false val baseUrl = getBaseUrl(url) baseUrl ?: return false try { val host = baseUrl.split("//", ".") val base64Url = EncoderUtils.base64Encode("${host[host.lastIndex - 1]}.${host.last()}") list18Plus.forEach { if (base64Url == it) { return true } } } catch (e: Exception) { } return false } fun getBaseUrl(url: String?): String? { if (url == null || !url.startsWith("http")) return null val index = url.indexOf("/", 9) return if (index == -1) { url } else url.substring(0, index) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/UTF8BOMFighter.java ================================================ package com.kunfei.bookshelf.help; public class UTF8BOMFighter { private static final byte[] UTF8_BOM_BYTES = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; private UTF8BOMFighter() { } static public String removeUTF8BOM(String xmlText) { byte[] bytes = xmlText.getBytes(); boolean containsBOM = bytes.length > 3 && bytes[0] == UTF8_BOM_BYTES[0] && bytes[1] == UTF8_BOM_BYTES[1] && bytes[2] == UTF8_BOM_BYTES[2]; if (containsBOM) { xmlText = new String(bytes, 3, bytes.length - 3); } return xmlText; } static public byte[] removeUTF8BOM(byte[] bytes) { boolean containsBOM = bytes.length > 3 && bytes[0] == UTF8_BOM_BYTES[0] && bytes[1] == UTF8_BOM_BYTES[1] && bytes[2] == UTF8_BOM_BYTES[2]; if (containsBOM) { byte[] copy = new byte[bytes.length - 3]; System.arraycopy(bytes, 3, copy, 0, bytes.length - 3); return copy; } return bytes; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/UpdateManager.java ================================================ package com.kunfei.bookshelf.help; import static android.content.Context.DOWNLOAD_SERVICE; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.util.Log; import android.widget.Toast; import androidx.core.content.FileProvider; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.kunfei.bookshelf.BuildConfig; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.UpdateInfoBean; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders; import com.kunfei.bookshelf.model.impl.IHttpGetApi; import java.io.File; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; public class UpdateManager { private Activity activity; public static UpdateManager getInstance(Activity activity) { return new UpdateManager(activity); } private UpdateManager(Activity activity) { this.activity = activity; } public void checkUpdate(boolean showMsg) { BaseModelImpl.getInstance().getRetrofitString("https://api.github.com") .create(IHttpGetApi.class) .get(MApplication.getInstance().getString(R.string.latest_release_api), AnalyzeHeaders.getDefaultHeader()) .flatMap(response -> analyzeLastReleaseApi(response.body())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(UpdateInfoBean updateInfo) { if (updateInfo.getUpDate()) { } else if (showMsg) { Toast.makeText(activity, "已是最新版本", Toast.LENGTH_SHORT).show(); } } @Override public void onError(Throwable e) { if (showMsg) { Toast.makeText(activity, "检测新版本出错", Toast.LENGTH_SHORT).show(); } } }); } private Observable analyzeLastReleaseApi(String jsonStr) { return Observable.create(emitter -> { try { UpdateInfoBean updateInfo = new UpdateInfoBean(); JsonObject version = new JsonParser().parse(jsonStr).getAsJsonObject(); if (version.get("prerelease").getAsBoolean()) return; JsonArray assets = version.get("assets").getAsJsonArray(); if (assets.size() > 0) { String lastVersion = version.get("tag_name").getAsString(); String url = assets.get(0).getAsJsonObject().get("browser_download_url").getAsString(); String detail = version.get("body").getAsString(); String thisVersion = MApplication.getVersionName().split("\\s")[0]; updateInfo.setUrl(url); updateInfo.setLastVersion(lastVersion); updateInfo.setDetail("# " + lastVersion + "\n" + detail); if (Integer.valueOf(lastVersion.split("\\.")[2]) > Integer.valueOf(thisVersion.split("\\.")[2])) { updateInfo.setUpDate(true); } else { updateInfo.setUpDate(false); } } emitter.onNext(updateInfo); emitter.onComplete(); } catch (Exception e) { emitter.onError(e); emitter.onComplete(); } }); } /** * 安装apk */ public void installApk(File apkFile) { if (!apkFile.exists()) { return; } Intent intent = new Intent(); //执行动作 intent.setAction(Intent.ACTION_VIEW); //判读版本是否在7.0以上 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Uri apkUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileProvider", apkFile); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); } else { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive"); } try { activity.startActivity(intent); } catch (Exception e) { Log.d("wwd", "Failed to launcher installing activity"); } } public static String getSavePath(String fileName) { return Environment.getExternalStoragePublicDirectory(DOWNLOAD_SERVICE).getPath() + fileName; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/coroutine/CompositeCoroutine.kt ================================================ package io.legado.app.help.coroutine @Suppress("unused") class CompositeCoroutine : CoroutineContainer { private var resources: HashSet>? = null val size: Int get() = resources?.size ?: 0 val isEmpty: Boolean get() = size == 0 constructor() constructor(vararg coroutines: Coroutine<*>) { this.resources = hashSetOf(*coroutines) } constructor(coroutines: Iterable>) { this.resources = hashSetOf() for (d in coroutines) { this.resources?.add(d) } } override fun add(coroutine: Coroutine<*>): Boolean { synchronized(this) { var set: HashSet>? = resources if (resources == null) { set = hashSetOf() resources = set } return set!!.add(coroutine) } } override fun addAll(vararg coroutines: Coroutine<*>): Boolean { synchronized(this) { var set: HashSet>? = resources if (resources == null) { set = hashSetOf() resources = set } for (coroutine in coroutines) { val add = set!!.add(coroutine) if (!add) { return false } } } return true } override fun remove(coroutine: Coroutine<*>): Boolean { if (delete(coroutine)) { coroutine.cancel() return true } return false } override fun delete(coroutine: Coroutine<*>): Boolean { synchronized(this) { val set = resources if (set == null || !set.remove(coroutine)) { return false } } return true } override fun clear() { val set: HashSet>? synchronized(this) { set = resources resources = null } set?.forEachIndexed { _, coroutine -> coroutine.cancel() } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/coroutine/Coroutine.kt ================================================ package io.legado.app.help.coroutine import kotlinx.coroutines.* import timber.log.Timber import kotlin.coroutines.CoroutineContext @Suppress("unused") class Coroutine( val scope: CoroutineScope, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ) { companion object { private val DEFAULT = MainScope() fun async( scope: CoroutineScope = DEFAULT, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ): Coroutine { return Coroutine(scope, context, block) } } private val job: Job private var start: VoidCallback? = null private var success: Callback? = null private var error: Callback? = null private var finally: VoidCallback? = null private var cancel: VoidCallback? = null private var timeMillis: Long? = null private var errorReturn: Result? = null val isCancelled: Boolean get() = job.isCancelled val isActive: Boolean get() = job.isActive val isCompleted: Boolean get() = job.isCompleted init { this.job = executeInternal(context, block) } fun timeout(timeMillis: () -> Long): Coroutine { this.timeMillis = timeMillis() return this@Coroutine } fun timeout(timeMillis: Long): Coroutine { this.timeMillis = timeMillis return this@Coroutine } fun onErrorReturn(value: () -> T?): Coroutine { this.errorReturn = Result(value()) return this@Coroutine } fun onErrorReturn(value: T?): Coroutine { this.errorReturn = Result(value) return this@Coroutine } fun onStart( context: CoroutineContext? = null, block: (suspend CoroutineScope.() -> Unit) ): Coroutine { this.start = VoidCallback(context, block) return this@Coroutine } fun onSuccess( context: CoroutineContext? = null, block: suspend CoroutineScope.(T) -> Unit ): Coroutine { this.success = Callback(context, block) return this@Coroutine } fun onError( context: CoroutineContext? = null, block: suspend CoroutineScope.(Throwable) -> Unit ): Coroutine { this.error = Callback(context, block) return this@Coroutine } fun onFinally( context: CoroutineContext? = null, block: suspend CoroutineScope.() -> Unit ): Coroutine { this.finally = VoidCallback(context, block) return this@Coroutine } fun onCancel( context: CoroutineContext? = null, block: suspend CoroutineScope.() -> Unit ): Coroutine { this.cancel = VoidCallback(context, block) return this@Coroutine } //取消当前任务 fun cancel(cause: CancellationException? = null) { job.cancel(cause) cancel?.let { MainScope().launch { if (null == it.context) { it.block.invoke(scope) } else { withContext(scope.coroutineContext.plus(it.context)) { it.block.invoke(this) } } } } } fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle { return job.invokeOnCompletion(handler) } private fun executeInternal( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): Job { return scope.plus(Dispatchers.Main).launch { try { start?.let { dispatchVoidCallback(this, it) } val value = executeBlock(scope, context, timeMillis ?: 0L, block) if (isActive) { success?.let { dispatchCallback(this, value, it) } } } catch (e: Throwable) { Timber.e(e) val consume: Boolean = errorReturn?.value?.let { value -> if (isActive) { success?.let { dispatchCallback(this, value, it) } } true } ?: false if (!consume && isActive) { error?.let { dispatchCallback(this, e, it) } } } finally { if (isActive) { finally?.let { dispatchVoidCallback(this, it) } } } } } private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) { if (null == callback.context) { callback.block.invoke(scope) } else { withContext(scope.coroutineContext.plus(callback.context)) { callback.block.invoke(this) } } } private suspend inline fun dispatchCallback( scope: CoroutineScope, value: R, callback: Callback ) { if (!scope.isActive) return if (null == callback.context) { callback.block.invoke(scope, value) } else { withContext(scope.coroutineContext.plus(callback.context)) { callback.block.invoke(this, value) } } } private suspend inline fun executeBlock( scope: CoroutineScope, context: CoroutineContext, timeMillis: Long, noinline block: suspend CoroutineScope.() -> T ): T { return withContext(scope.coroutineContext.plus(context)) { if (timeMillis > 0L) withTimeout(timeMillis) { block() } else { block() } } } private data class Result(val value: T?) private inner class VoidCallback( val context: CoroutineContext?, val block: suspend CoroutineScope.() -> Unit ) private inner class Callback( val context: CoroutineContext?, val block: suspend CoroutineScope.(VALUE) -> Unit ) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/coroutine/CoroutineContainer.kt ================================================ package io.legado.app.help.coroutine internal interface CoroutineContainer { fun add(coroutine: Coroutine<*>): Boolean fun addAll(vararg coroutines: Coroutine<*>): Boolean fun remove(coroutine: Coroutine<*>): Boolean fun delete(coroutine: Coroutine<*>): Boolean fun clear() } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/glide/ImageLoader.kt ================================================ package com.kunfei.bookshelf.help.glide 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) -> GlideApp.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) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpGlideModule.kt ================================================ package com.kunfei.bookshelf.help.glide import android.content.Context import com.bumptech.glide.Glide import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.AppGlideModule import java.io.InputStream @GlideModule class OkHttpGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { registry.replace( GlideUrl::class.java, InputStream::class.java, OkHttpModeLoaderFactory ) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpModeLoaderFactory.kt ================================================ package com.kunfei.bookshelf.help.glide import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import java.io.InputStream object OkHttpModeLoaderFactory : ModelLoaderFactory { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { return OkHttpModelLoader } override fun teardown() { // Do nothing, this instance doesn't own the client. } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpModelLoader.kt ================================================ package com.kunfei.bookshelf.help.glide import com.bumptech.glide.load.Options import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.ModelLoader import java.io.InputStream object OkHttpModelLoader : ModelLoader { override fun buildLoadData( model: GlideUrl, width: Int, height: Int, options: Options ): ModelLoader.LoadData { return ModelLoader.LoadData(model, OkHttpStreamFetcher(model)) } override fun handles(model: GlideUrl): Boolean { return true } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpStreamFetcher.kt ================================================ package com.kunfei.bookshelf.help.glide import com.bumptech.glide.Priority import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.HttpException import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.util.ContentLengthInputStream import com.bumptech.glide.util.Preconditions import com.kunfei.bookshelf.base.BaseModelImpl import okhttp3.Call import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody import java.io.IOException import java.io.InputStream class OkHttpStreamFetcher(private val url: GlideUrl) : DataFetcher, okhttp3.Callback { private var stream: InputStream? = null private var responseBody: ResponseBody? = null private var callback: DataFetcher.DataCallback? = null private var call: Call? = null override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { val requestBuilder: Request.Builder = Request.Builder().url(url.toStringUrl()) for ((key, value) in url.headers.entries) { requestBuilder.addHeader(key, value) } val request: Request = requestBuilder.build() this.callback = callback call = BaseModelImpl.getClient().newCall(request) call?.enqueue(this) } override fun cleanup() { try { stream?.close() } catch (e: IOException) { // Ignored } responseBody?.close() callback = null } override fun cancel() { call?.cancel() } override fun getDataClass(): Class { return InputStream::class.java } override fun getDataSource(): DataSource { return DataSource.REMOTE } override fun onFailure(call: Call, e: IOException) { callback?.onLoadFailed(e) } override fun onResponse(call: Call, response: Response) { responseBody = response.body if (response.isSuccessful) { val contentLength: Long = Preconditions.checkNotNull(responseBody).contentLength() stream = ContentLengthInputStream.obtain(responseBody!!.byteStream(), contentLength) callback!!.onDataReady(stream) } else { callback!!.onLoadFailed(HttpException(response.message, response.code)) } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/media/LoaderCreator.java ================================================ package com.kunfei.bookshelf.help.media; import android.content.Context; import android.os.Bundle; import androidx.loader.content.CursorLoader; /** * Created by newbiechen on 2018/1/14. */ public class LoaderCreator { public static final int ALL_BOOK_FILE = 1; public static CursorLoader create(Context context, int id, Bundle bundle) { LocalFileLoader loader = null; if (id == ALL_BOOK_FILE) { loader = new LocalFileLoader(context); } else { loader = null; } if (loader != null) { return loader; } throw new IllegalArgumentException("The id of Loader is invalid!"); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/media/LocalFileLoader.java ================================================ package com.kunfei.bookshelf.help.media; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.loader.content.CursorLoader; import java.io.File; import java.sql.Blob; import java.util.ArrayList; import java.util.List; /** * Created by newbiechen on 2018/1/14. */ public class LocalFileLoader extends CursorLoader { private static final String TAG = "LocalFileLoader"; private static final Uri FILE_URI = Uri.parse("content://media/external/file"); private static final String SELECTION = MediaStore.Files.FileColumns.DATA + " like ? or " + MediaStore.Files.FileColumns.DATA + " like ?"; private static final String[] SEARCH_TYPE = new String[]{"%.txt", "%.epub"}; private static final String SORT_ORDER = MediaStore.Files.FileColumns.DISPLAY_NAME + " DESC"; private static final String[] FILE_PROJECTION = { MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.DISPLAY_NAME }; public LocalFileLoader(Context context) { super(context); initLoader(); } /** * 为 Cursor 设置默认参数 */ private void initLoader() { setUri(FILE_URI); setProjection(FILE_PROJECTION); setSelection(SELECTION); setSelectionArgs(SEARCH_TYPE); setSortOrder(SORT_ORDER); } public void parseData(Cursor cursor, final MediaStoreHelper.MediaResultCallback resultCallback) { List files = new ArrayList<>(); // 判断是否存在数据 if (cursor == null) { // 暂时直接返回空数据 resultCallback.onResultCallback(files); return; } // 重复使用Loader时,需要重置cursor的position; cursor.moveToPosition(-1); while (cursor.moveToNext()) { String path; path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)); // 路径无效 if (!TextUtils.isEmpty(path)) { File file = new File(path); if (!file.isDirectory() && file.exists() && file.length() > 1024) { files.add(file); } } } if (resultCallback != null) { resultCallback.onResultCallback(files); } } /** * 从Cursor中读取对应columnName的值 * * @param cursor * @param columnName * @param defaultValue * @return 当columnName无效时返回默认值; */ protected Object getValueFromCursor(@NonNull Cursor cursor, String columnName, Object defaultValue) { try { int index = cursor.getColumnIndexOrThrow(columnName); int type = cursor.getType(index); switch (type) { case Cursor.FIELD_TYPE_STRING: // TO SOLVE:某些手机的数据库将数值类型存为String类型 String value = cursor.getString(index); try { if (defaultValue instanceof String) { return value; } else if (defaultValue instanceof Long) { return Long.valueOf(value); } else if (defaultValue instanceof Integer) { return Integer.valueOf(value); } else if (defaultValue instanceof Double) { return Double.valueOf(value); } else if (defaultValue instanceof Float) { return Float.valueOf(value); } } catch (NumberFormatException e) { return defaultValue; } case Cursor.FIELD_TYPE_INTEGER: if (defaultValue instanceof Long) { return cursor.getLong(index); } else if (defaultValue instanceof Integer) { return cursor.getInt(index); } case Cursor.FIELD_TYPE_FLOAT: if (defaultValue instanceof Float) { return cursor.getFloat(index); } else if (defaultValue instanceof Double) { return cursor.getDouble(index); } case Cursor.FIELD_TYPE_BLOB: if (defaultValue instanceof Blob) { return cursor.getBlob(index); } case Cursor.FIELD_TYPE_NULL: default: return defaultValue; } } catch (IllegalArgumentException e) { return defaultValue; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/media/MediaStoreHelper.java ================================================ package com.kunfei.bookshelf.help.media; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import java.io.File; import java.lang.ref.WeakReference; import java.util.List; /** * Created by newbiechen on 2018/1/14. * 获取媒体库的数据。 */ public class MediaStoreHelper { /** * 获取媒体库中所有的书籍文件 *

* 暂时只支持 TXT * * @param activity * @param resultCallback */ public static void getAllBookFile(FragmentActivity activity, MediaResultCallback resultCallback) { // 将文件的获取处理交给 LoaderManager。 activity.getSupportLoaderManager() .initLoader(LoaderCreator.ALL_BOOK_FILE, null, new MediaLoaderCallbacks(activity, resultCallback)); } public interface MediaResultCallback { void onResultCallback(List files); } /** * Loader 回调处理 */ static class MediaLoaderCallbacks implements LoaderManager.LoaderCallbacks { protected WeakReference mContext; protected MediaResultCallback mResultCallback; public MediaLoaderCallbacks(Context context, MediaResultCallback resultCallback) { mContext = new WeakReference<>(context); mResultCallback = resultCallback; } @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { return LoaderCreator.create(mContext.get(), id, args); } @Override public void onLoadFinished(@NonNull Loader loader, Cursor data) { LocalFileLoader localFileLoader = (LocalFileLoader) loader; localFileLoader.parseData(data, mResultCallback); } @Override public void onLoaderReset(@NonNull Loader loader) { } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/ActivitySource.kt ================================================ package com.kunfei.bookshelf.help.permission import android.app.Activity import android.content.Context import android.content.Intent import java.lang.ref.WeakReference internal class ActivitySource(activity: Activity) : RequestSource { private val actRef: WeakReference = WeakReference(activity) override val context: Context? get() = actRef.get() override fun startActivity(intent: Intent) { actRef.get()?.startActivity(intent) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/FragmentSource.kt ================================================ package com.kunfei.bookshelf.help.permission import android.content.Context import android.content.Intent import androidx.fragment.app.Fragment import java.lang.ref.WeakReference internal class FragmentSource(fragment: Fragment) : RequestSource { private val fragRef: WeakReference = WeakReference(fragment) override val context: Context? get() = fragRef.get()?.requireContext() override fun startActivity(intent: Intent) { fragRef.get()?.startActivity(intent) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/OnPermissionsDeniedCallback.kt ================================================ package com.kunfei.bookshelf.help.permission interface OnPermissionsDeniedCallback { fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/OnPermissionsGrantedCallback.kt ================================================ package com.kunfei.bookshelf.help.permission interface OnPermissionsGrantedCallback { fun onPermissionsGranted(requestCode: Int) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/OnPermissionsResultCallback.kt ================================================ package com.kunfei.bookshelf.help.permission interface OnPermissionsResultCallback { fun onPermissionsGranted(requestCode: Int) fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/OnRequestPermissionsResultCallback.kt ================================================ package com.kunfei.bookshelf.help.permission import android.content.Intent interface OnRequestPermissionsResultCallback { fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/PermissionActivity.kt ================================================ package com.kunfei.bookshelf.help.permission import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.Settings import android.view.KeyEvent import android.widget.Toast import androidx.core.app.ActivityCompat import com.kunfei.bookshelf.R class PermissionActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) when (intent.getIntExtra(KEY_INPUT_REQUEST_TYPE, Request.TYPE_REQUEST_PERMISSION)) { Request.TYPE_REQUEST_PERMISSION//权限请求 -> { val requestCode = intent.getIntExtra(KEY_INPUT_PERMISSIONS_CODE, 1000) val permissions = intent.getStringArrayExtra(KEY_INPUT_PERMISSIONS) if (permissions != null) { ActivityCompat.requestPermissions(this, permissions, requestCode) } else { finish() } } Request.TYPE_REQUEST_SETTING//跳转到设置界面 -> try { val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) settingIntent.data = Uri.fromParts("package", packageName, null) startActivityForResult(settingIntent, Request.TYPE_REQUEST_SETTING) } catch (e: Exception) { Toast.makeText(this, R.string.tip_cannot_jump_setting_page, Toast.LENGTH_SHORT).show() finish() } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) RequestPlugins.sRequestCallback?.onRequestPermissionsResult(requestCode, permissions, grantResults) finish() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) RequestPlugins.sRequestCallback?.onActivityResult(requestCode, resultCode, data) finish() } override fun startActivity(intent: Intent) { super.startActivity(intent) overridePendingTransition(0, 0) } override fun finish() { super.finish() overridePendingTransition(0, 0) } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { return if (keyCode == KeyEvent.KEYCODE_BACK) { true } else super.onKeyDown(keyCode, event) } companion object { const val KEY_INPUT_REQUEST_TYPE = "KEY_INPUT_REQUEST_TYPE" const val KEY_INPUT_PERMISSIONS_CODE = "KEY_INPUT_PERMISSIONS_CODE" const val KEY_INPUT_PERMISSIONS = "KEY_INPUT_PERMISSIONS" } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/Permissions.kt ================================================ package com.kunfei.bookshelf.help.permission object Permissions { const val READ_CALENDAR = "android.permission.READ_CALENDAR" const val WRITE_CALENDAR = "android.permission.WRITE_CALENDAR" const val CAMERA = "android.permission.CAMERA" const val READ_CONTACTS = "android.permission.READ_CONTACTS" const val WRITE_CONTACTS = "android.permission.WRITE_CONTACTS" const val GET_ACCOUNTS = "android.permission.GET_ACCOUNTS" const val ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION" const val ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION" const val RECORD_AUDIO = "android.permission.RECORD_AUDIO" const val READ_PHONE_STATE = "android.permission.READ_PHONE_STATE" const val CALL_PHONE = "android.permission.CALL_PHONE" const val READ_CALL_LOG = "android.permission.READ_CALL_LOG" const val WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG" const val ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL" const val USE_SIP = "android.permission.USE_SIP" const val PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS" const val BODY_SENSORS = "android.permission.BODY_SENSORS" const val SEND_SMS = "android.permission.SEND_SMS" const val RECEIVE_SMS = "android.permission.RECEIVE_SMS" const val READ_SMS = "android.permission.READ_SMS" const val RECEIVE_WAP_PUSH = "android.permission.RECEIVE_WAP_PUSH" const val RECEIVE_MMS = "android.permission.RECEIVE_MMS" const val READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE" const val WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE" object Group { val CALENDAR = arrayOf(READ_CALENDAR, WRITE_CALENDAR) val CAMERA = arrayOf(Permissions.CAMERA) val CONTACTS = arrayOf(READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS) val LOCATION = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) val MICROPHONE = arrayOf(RECORD_AUDIO) val PHONE = arrayOf( READ_PHONE_STATE, CALL_PHONE, READ_CALL_LOG, WRITE_CALL_LOG, ADD_VOICEMAIL, USE_SIP, PROCESS_OUTGOING_CALLS ) val SENSORS = arrayOf(BODY_SENSORS) val SMS = arrayOf( SEND_SMS, RECEIVE_SMS, READ_SMS, RECEIVE_WAP_PUSH, RECEIVE_MMS ) val STORAGE = arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/PermissionsCompat.kt ================================================ package com.kunfei.bookshelf.help.permission import android.app.Activity import androidx.annotation.StringRes import androidx.fragment.app.Fragment class PermissionsCompat private constructor() { private var request: Request? = null fun request() { RequestManager.pushRequest(request) } class Builder { private val request: Request constructor(activity: Activity) { request = Request(activity) } constructor(fragment: Fragment) { request = Request(fragment) } fun addPermissions(vararg permissions: String): Builder { request.addPermissions(*permissions) return this } fun requestCode(requestCode: Int): Builder { request.setRequestCode(requestCode) return this } fun onGranted(callback: (requestCode: Int) -> Unit): Builder { request.setOnGrantedCallback(object : OnPermissionsGrantedCallback { override fun onPermissionsGranted(requestCode: Int) { callback(requestCode) } }) return this } fun onDenied(callback: (requestCode: Int, deniedPermissions: Array) -> Unit): Builder { request.setOnDeniedCallback(object : OnPermissionsDeniedCallback { override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { callback(requestCode, deniedPermissions) } }) return this } fun rationale(rationale: CharSequence): Builder { request.setRationale(rationale) return this } fun rationale(@StringRes resId: Int): Builder { request.setRationale(resId) return this } fun build(): PermissionsCompat { val compat = PermissionsCompat() compat.request = request return compat } fun request(): PermissionsCompat { val compat = build() compat.request = request compat.request() return compat } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/Request.kt ================================================ package com.kunfei.bookshelf.help.permission import android.app.Activity import android.content.Intent import android.content.pm.PackageManager import android.os.Build import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.kunfei.bookshelf.R import org.jetbrains.anko.startActivity import java.util.* internal class Request : OnRequestPermissionsResultCallback { internal val requestTime: Long private var requestCode: Int = TYPE_REQUEST_PERMISSION private var source: RequestSource? = null private var permissions: ArrayList? = null private var grantedCallback: OnPermissionsGrantedCallback? = null private var deniedCallback: OnPermissionsDeniedCallback? = null private var rationaleResId: Int = 0 private var rationale: CharSequence? = null private var rationaleDialog: AlertDialog? = null private val deniedPermissions: Array? get() { return getDeniedPermissions(this.permissions?.toTypedArray()) } constructor(activity: Activity) { source = ActivitySource(activity) permissions = ArrayList() requestTime = System.currentTimeMillis() } constructor(fragment: Fragment) { source = FragmentSource(fragment) permissions = ArrayList() requestTime = System.currentTimeMillis() } fun addPermissions(vararg permissions: String) { this.permissions?.addAll(Arrays.asList(*permissions)) } fun setRequestCode(requestCode: Int) { this.requestCode = requestCode } fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) { grantedCallback = callback } fun setOnDeniedCallback(callback: OnPermissionsDeniedCallback) { deniedCallback = callback } fun setRationale(@StringRes resId: Int) { rationaleResId = resId rationale = null } fun setRationale(rationale: CharSequence) { this.rationale = rationale rationaleResId = 0 } fun start() { RequestPlugins.setOnRequestPermissionsCallback(this) val deniedPermissions = deniedPermissions if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (deniedPermissions == null) { onPermissionsGranted(requestCode) } else { val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationale != null) { showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } } else { onPermissionsDenied(requestCode, deniedPermissions) } } } else { if (deniedPermissions != null) { source?.context?.startActivity( Pair(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_PERMISSION), Pair(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode), Pair(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) ) } else { onPermissionsGranted(requestCode) } } } fun clear() { grantedCallback = null deniedCallback = null } private fun getDeniedPermissions(permissions: Array?): Array? { if (permissions != null) { val deniedPermissionList = ArrayList() for (permission in permissions) { if (source?.context?.let { ContextCompat.checkSelfPermission( it, permission ) } != PackageManager.PERMISSION_GRANTED ) { deniedPermissionList.add(permission) } } val size = deniedPermissionList.size if (size > 0) { return deniedPermissionList.toTypedArray() } } return null } private fun showSettingDialog(rationale: CharSequence, cancel: () -> Unit) { rationaleDialog?.dismiss() source?.context?.let { runCatching { rationaleDialog = AlertDialog.Builder(it) .setTitle(R.string.dialog_title) .setMessage(rationale) .setPositiveButton(R.string.dialog_setting) { _, _ -> it.startActivity( Pair( PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_SETTING ) ) } .setNegativeButton(R.string.dialog_cancel) { _, _ -> cancel() } .show() } } } private fun onPermissionsGranted(requestCode: Int) { try { grantedCallback?.onPermissionsGranted(requestCode) } catch (ignore: Exception) { } RequestPlugins.sResultCallback?.onPermissionsGranted(requestCode) } private fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { try { deniedCallback?.onPermissionsDenied(requestCode, deniedPermissions) } catch (ignore: Exception) { } RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { val deniedPermissions = getDeniedPermissions(permissions) if (deniedPermissions != null) { val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationale != null) { showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } } else { onPermissionsDenied(requestCode, deniedPermissions) } } else { onPermissionsGranted(requestCode) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val deniedPermissions = deniedPermissions if (deniedPermissions == null) { onPermissionsGranted(this.requestCode) } else { onPermissionsDenied(this.requestCode, deniedPermissions) } } companion object { const val TYPE_REQUEST_PERMISSION = 1 const val TYPE_REQUEST_SETTING = 2 } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/RequestManager.kt ================================================ package com.kunfei.bookshelf.help.permission import android.os.Handler import android.os.Looper import java.util.* internal object RequestManager : OnPermissionsResultCallback { private var requests: Stack? = null private var request: Request? = null private val handler = Handler(Looper.getMainLooper()) private val requestRunnable = Runnable { request?.start() } private val isCurrentRequestInvalid: Boolean get() = request?.let { System.currentTimeMillis() - it.requestTime > 5 * 1000L } ?: true init { RequestPlugins.setOnPermissionsResultCallback(this) } fun pushRequest(request: Request?) { if (request == null) return if (requests == null) { requests = Stack() } requests?.let { val index = it.indexOf(request) if (index >= 0) { val to = it.size - 1 if (index != to) { Collections.swap(requests, index, to) } } else { it.push(request) } if (!it.empty() && isCurrentRequestInvalid) { this.request = it.pop() handler.post(requestRunnable) } } } private fun startNextRequest() { request?.clear() request = null requests?.let { request = if (it.empty()) null else it.pop() request?.let { handler.post(requestRunnable) } } } override fun onPermissionsGranted(requestCode: Int) { startNextRequest() } override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { startNextRequest() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/RequestPlugins.kt ================================================ package com.kunfei.bookshelf.help.permission internal object RequestPlugins { @Volatile var sRequestCallback: OnRequestPermissionsResultCallback? = null @Volatile var sResultCallback: OnPermissionsResultCallback? = null fun setOnRequestPermissionsCallback(callback: OnRequestPermissionsResultCallback) { sRequestCallback = callback } fun setOnPermissionsResultCallback(callback: OnPermissionsResultCallback) { sResultCallback = callback } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/permission/RequestSource.kt ================================================ package com.kunfei.bookshelf.help.permission import android.content.Context import android.content.Intent interface RequestSource { val context: Context? fun startActivity(intent: Intent) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/storage/Backup.kt ================================================ package com.kunfei.bookshelf.help.storage import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile import com.kunfei.bookshelf.DbHelper import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.base.observer.MySingleObserver import com.kunfei.bookshelf.help.BookshelfHelp import com.kunfei.bookshelf.help.FileHelp import com.kunfei.bookshelf.model.BookSourceManager import com.kunfei.bookshelf.model.ReplaceRuleManager import com.kunfei.bookshelf.model.TxtChapterRuleManager import com.kunfei.bookshelf.utils.DocumentUtil import com.kunfei.bookshelf.utils.FileUtils import com.kunfei.bookshelf.utils.GSON import io.reactivex.Single import io.reactivex.SingleOnSubscribe import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import java.io.File import java.util.concurrent.TimeUnit object Backup { val backupPath = MApplication.getInstance().filesDir.absolutePath + File.separator + "backup" val defaultPath by lazy { FileUtils.getSdCardPath() + File.separator + "YueDu" } val backupFileNames by lazy { arrayOf( "myBookShelf.json", "myBookSource.json", "myBookSearchHistory.json", "myBookReplaceRule.json", "myTxtChapterRule.json", "config.xml" ) } fun autoBack() { val lastBackup = MApplication.getConfigPreferences().getLong("lastBackup", 0) if (System.currentTimeMillis() - lastBackup < TimeUnit.DAYS.toMillis(1)) { return } val path = MApplication.getConfigPreferences().getString("backupPath", defaultPath) if (path == null) { backup(MApplication.getInstance(), defaultPath, null, true) } else { backup(MApplication.getInstance(), path, null, true) } } fun backup(context: Context, path: String, callBack: CallBack?, isAuto: Boolean = false) { MApplication.getConfigPreferences().edit().putLong("lastBackup", System.currentTimeMillis()).apply() Single.create(SingleOnSubscribe { e -> BookshelfHelp.getAllBook().let { if (it.isNotEmpty()) { val json = GSON.toJson(it) FileHelp.createFileIfNotExist(backupPath + File.separator + "myBookShelf.json").writeText(json) } } BookSourceManager.getAllBookSource().let { if (it.isNotEmpty()) { val json = GSON.toJson(it) FileHelp.createFileIfNotExist(backupPath + File.separator + "myBookSource.json").writeText(json) } } DbHelper.getDaoSession().searchHistoryBeanDao.queryBuilder().list().let { if (it.isNotEmpty()) { val json = GSON.toJson(it) FileHelp.createFileIfNotExist(backupPath + File.separator + "myBookSearchHistory.json") .writeText(json) } } ReplaceRuleManager.getAll().blockingGet().let { if (it.isNotEmpty()) { val json = GSON.toJson(it) FileHelp.createFileIfNotExist(backupPath + File.separator + "myBookReplaceRule.json").writeText(json) } } TxtChapterRuleManager.getAll().let { if (it.isNotEmpty()) { val json = GSON.toJson(it) FileHelp.createFileIfNotExist(backupPath + File.separator + "myTxtChapterRule.json") .writeText(json) } } Preferences.getSharedPreferences(context, backupPath, "config")?.let { sp -> val edit = sp.edit() MApplication.getConfigPreferences().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(FileHelp.createFileIfNotExist(path + File.separator + "auto" + File.separator + fileName), true) } } else { val file = File(backupPath + File.separator + fileName) if (file.exists()) { file.copyTo(FileHelp.createFileIfNotExist(path + File.separator + fileName), true) } } } } } interface CallBack { fun backupSuccess() fun backupError(msg: String) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/storage/BackupRestoreUi.kt ================================================ package com.kunfei.bookshelf.help.storage import android.app.Activity import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri import android.os.Build import android.text.TextUtils import androidx.core.content.ContextCompat import androidx.documentfile.provider.DocumentFile import com.hwangjr.rxbus.RxBus import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.R import com.kunfei.bookshelf.base.observer.MySingleObserver import com.kunfei.bookshelf.constant.RxBusTag import com.kunfei.bookshelf.help.permission.Permissions import com.kunfei.bookshelf.help.permission.PermissionsCompat import com.kunfei.bookshelf.help.storage.WebDavHelp.getWebDavFileNames import com.kunfei.bookshelf.help.storage.WebDavHelp.showRestoreDialog import com.kunfei.bookshelf.widget.filepicker.picker.FilePicker 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 java.util.* object BackupRestoreUi : Backup.CallBack, Restore.CallBack { private const val backupSelectRequestCode = 22 private const val restoreSelectRequestCode = 33 private fun getBackupPath(): String? { return MApplication.getConfigPreferences().getString("backupPath", null) } private fun setBackupPath(path: String?) { if (path.isNullOrEmpty()) { MApplication.getConfigPreferences().edit().remove("backupPath").apply() } else { MApplication.getConfigPreferences().edit().putString("backupPath", path).apply() } } override fun backupSuccess() { MApplication.getInstance().toast(R.string.backup_success) } override fun backupError(msg: String) { MApplication.getInstance().toast(msg) } override fun restoreSuccess() { MApplication.getInstance().toast(R.string.restore_success) RxBus.get().post(RxBusTag.RECREATE, true) } override fun restoreError(msg: String) { MApplication.getInstance().toast(msg) } fun backup(activity: Activity) { val backupPath = getBackupPath() if (backupPath.isNullOrEmpty()) { selectBackupFolder(activity) } 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) } } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { selectBackupFolder(activity) } else { backupUsePermission(activity) } } 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) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { 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") } return } activity.alert { titleResource = R.string.select_folder items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index -> when (index) { 0 -> { 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") } } 1 -> { PermissionsCompat.Builder(activity) .addPermissions(*Permissions.Group.STORAGE) .rationale(R.string.get_storage_per) .onGranted { selectBackupFolderApp(activity, false) } .request() } 2 -> { setBackupPath(Backup.defaultPath) backupUsePermission(activity) } } } }.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) } 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) } } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { selectRestoreFolder(activity) } else { restoreUsePermission(activity) } } } }) } 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) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { 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") } return } activity.alert { titleResource = R.string.select_folder items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index -> when (index) { 0 -> { 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") } } 1 -> { PermissionsCompat.Builder(activity) .addPermissions(*Permissions.Group.STORAGE) .rationale(R.string.get_storage_per) .onGranted { selectBackupFolderApp(activity, true) } .request() } 2 -> restoreUsePermission(activity) } } }.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 ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/storage/Preferences.kt ================================================ package com.kunfei.bookshelf.help.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 } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/storage/Restore.kt ================================================ package com.kunfei.bookshelf.help.storage import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile import com.kunfei.bookshelf.DbHelper import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.R import com.kunfei.bookshelf.base.observer.MySingleObserver import com.kunfei.bookshelf.bean.* import com.kunfei.bookshelf.help.FileHelp import com.kunfei.bookshelf.help.LauncherIcon import com.kunfei.bookshelf.help.ReadBookControl import com.kunfei.bookshelf.model.BookSourceManager import com.kunfei.bookshelf.model.ReplaceRuleManager import com.kunfei.bookshelf.model.TxtChapterRuleManager import com.kunfei.bookshelf.utils.DocumentUtil import com.kunfei.bookshelf.utils.GSON import com.kunfei.bookshelf.utils.fromJsonArray import io.reactivex.Single import io.reactivex.SingleOnSubscribe import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers 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 { FileHelp.createFileIfNotExist(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 = FileHelp.createFileIfNotExist(path + File.separator + "myBookShelf.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) } } } catch (e: Exception) { e.printStackTrace() } try { val file = FileHelp.createFileIfNotExist(path + File.separator + "myBookSource.json") val json = file.readText() GSON.fromJsonArray(json)?.let { BookSourceManager.addBookSource(it) } } catch (e: Exception) { e.printStackTrace() } try { val file = FileHelp.createFileIfNotExist(path + File.separator + "myBookSearchHistory.json") val json = file.readText() GSON.fromJsonArray(json)?.let { DbHelper.getDaoSession().searchHistoryBeanDao.insertOrReplaceInTx(it) } } catch (e: Exception) { e.printStackTrace() } try { val file = FileHelp.createFileIfNotExist(path + File.separator + "myBookReplaceRule.json") val json = file.readText() GSON.fromJsonArray(json)?.let { ReplaceRuleManager.addDataS(it) } } catch (e: Exception) { e.printStackTrace() } try { val file = FileHelp.createFileIfNotExist(path + File.separator + "myTxtChapterRule.json") val json = file.readText() GSON.fromJsonArray(json)?.let { TxtChapterRuleManager.save(it) } } catch (e: Exception) { e.printStackTrace() } var donateHb = MApplication.getConfigPreferences().getLong("DonateHb", 0) donateHb = if (donateHb > System.currentTimeMillis()) 0 else donateHb Preferences.getSharedPreferences(MApplication.getInstance(), path, "config")?.all?.map { val edit = MApplication.getConfigPreferences().edit() 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.putLong("DonateHb", donateHb) edit.putInt("versionCode", MApplication.getVersionCode()) edit.apply() } e.onSuccess(true) }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : MySingleObserver() { override fun onSuccess(t: Boolean) { LauncherIcon.ChangeIcon(MApplication.getConfigPreferences().getString("launcher_icon", MApplication.getInstance().getString(R.string.icon_main))) ReadBookControl.getInstance().updateReaderSettings() MApplication.getInstance().upThemeStore() MApplication.getInstance().initNightTheme() callBack?.restoreSuccess() } override fun onError(e: Throwable) { e.printStackTrace() callBack?.restoreError(e.localizedMessage ?: "ERROR") } }) } interface CallBack { fun restoreSuccess() fun restoreError(msg: String) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/help/storage/WebDavHelp.kt ================================================ package com.kunfei.bookshelf.help.storage import android.content.Context import android.os.Handler import android.os.Looper import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.base.observer.MySingleObserver import com.kunfei.bookshelf.constant.AppConstant import com.kunfei.bookshelf.help.FileHelp import com.kunfei.bookshelf.utils.ZipUtils import com.kunfei.bookshelf.utils.webdav.WebDav import com.kunfei.bookshelf.utils.webdav.http.HttpAuth 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 org.jetbrains.anko.toast import java.io.File import java.text.SimpleDateFormat import java.util.* import kotlin.math.min object WebDavHelp { private val zipFilePath = FileHelp.getCachePath() + "/backup" + ".zip" private val unzipFilesPath by lazy { FileHelp.getCachePath() } private fun getWebDavUrl(): String { var url = MApplication.getConfigPreferences().getString("web_dav_url", AppConstant.DEFAULT_WEB_DAV_URL) if (url.isNullOrEmpty()) { url = AppConstant.DEFAULT_WEB_DAV_URL } if (!url.endsWith("/")) url += "/" return url } private fun initWebDav(): Boolean { val account = MApplication.getConfigPreferences().getString("web_dav_account", "") val password = MApplication.getConfigPreferences().getString("web_dav_password", "") 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 + "YueDu/").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 + "YueDu/" + 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] } FileHelp.deleteFile(zipFilePath) if (ZipUtils.zipFiles(paths, zipFilePath)) { WebDav(getWebDavUrl() + "YueDu").makeAsDir() val putUrl = getWebDavUrl() + "YueDu/backup" + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) .format(Date(System.currentTimeMillis())) + ".zip" WebDav(putUrl).upload(zipFilePath) } } } catch (e: Exception) { Handler(Looper.getMainLooper()).post { MApplication.getInstance().toast("WebDav\n${e.localizedMessage}") } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/BookSourceManager.java ================================================ package com.kunfei.bookshelf.model; import android.database.Cursor; import android.text.TextUtils; import androidx.annotation.Nullable; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.dao.BookSourceBeanDao; import com.kunfei.bookshelf.help.SourceHelp; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders; import com.kunfei.bookshelf.model.impl.IHttpGetApi; import com.kunfei.bookshelf.utils.GsonUtils; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.StringUtils; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; /** * Created by GKF on 2017/12/15. * 所有书源 */ public class BookSourceManager { public static List getSelectedBookSource() { return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.Enable.eq(true)) .orderRaw(BookSourceBeanDao.Properties.Weight.columnName + " DESC") .orderAsc(BookSourceBeanDao.Properties.SerialNumber) .list(); } public static List getAllBookSource() { return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .orderRaw(getBookSourceSort()) .orderAsc(BookSourceBeanDao.Properties.SerialNumber) .list(); } public static List getSelectedBookSourceBySerialNumber() { return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.Enable.eq(true)) .orderAsc(BookSourceBeanDao.Properties.SerialNumber) .list(); } public static List getAllBookSourceBySerialNumber() { return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .orderAsc(BookSourceBeanDao.Properties.SerialNumber) .list(); } public static List getEnableSourceByGroup(String group) { return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.Enable.eq(true)) .where(BookSourceBeanDao.Properties.BookSourceGroup.like("%" + group + "%")) .orderRaw(BookSourceBeanDao.Properties.Weight.columnName + " DESC") .list(); } @Nullable public static BookSourceBean getBookSourceByUrl(String url) { if (url == null) return null; return DbHelper.getDaoSession().getBookSourceBeanDao().load(url); } public static void removeBookSource(BookSourceBean sourceBean) { if (sourceBean == null) return; DbHelper.getDaoSession().getBookSourceBeanDao().delete(sourceBean); } public static String getBookSourceSort() { switch (MApplication.getConfigPreferences().getInt("SourceSort", 0)) { case 1: return BookSourceBeanDao.Properties.Weight.columnName + " DESC"; case 2: return BookSourceBeanDao.Properties.BookSourceName.columnName + " COLLATE LOCALIZED ASC"; default: return BookSourceBeanDao.Properties.SerialNumber.columnName + " ASC"; } } public static void addBookSource(List bookSourceBeans) { for (BookSourceBean bookSourceBean : bookSourceBeans) { addBookSource(bookSourceBean); } } public static void addBookSource(BookSourceBean bookSourceBean) { if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl())) return; if (bookSourceBean.getBookSourceUrl().endsWith("/")) { bookSourceBean.setBookSourceUrl(bookSourceBean.getBookSourceUrl().replaceAll("/+$", "")); } BookSourceBean temp = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.BookSourceUrl.eq(bookSourceBean.getBookSourceUrl())).unique(); if (temp != null) { bookSourceBean.setSerialNumber(temp.getSerialNumber()); } if (bookSourceBean.getSerialNumber() < 0) { bookSourceBean.setSerialNumber((int) (DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder().count() + 1)); } DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean); } public static void saveBookSource(BookSourceBean bookSourceBean) { if (bookSourceBean != null) { DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean); } } public static Single toTop(BookSourceBean sourceBean) { return Single.create((SingleOnSubscribe) e -> { List beanList = getAllBookSourceBySerialNumber(); for (int i = 0; i < beanList.size(); i++) { beanList.get(i).setSerialNumber(i + 1); } sourceBean.setSerialNumber(0); DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(beanList); DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(sourceBean); e.onSuccess(true); }).compose(RxUtils::toSimpleSingle); } public static List getEnableGroupList() { List groupList = new ArrayList<>(); String sql = "SELECT DISTINCT " + BookSourceBeanDao.Properties.BookSourceGroup.columnName + " FROM " + BookSourceBeanDao.TABLENAME + " WHERE " + BookSourceBeanDao.Properties.Enable.name + " = 1"; Cursor cursor = DbHelper.getDaoSession().getDatabase().rawQuery(sql, null); if (!cursor.moveToFirst()) return groupList; do { String group = cursor.getString(0); if (TextUtils.isEmpty(group) || TextUtils.isEmpty(group.trim())) continue; for (String item : group.split("\\s*[,;,;]\\s*")) { if (TextUtils.isEmpty(item) || groupList.contains(item)) continue; groupList.add(item); } } while (cursor.moveToNext()); Collections.sort(groupList); return groupList; } public static List getGroupList() { List groupList = new ArrayList<>(); String sql = "SELECT DISTINCT " + BookSourceBeanDao.Properties.BookSourceGroup.columnName + " FROM " + BookSourceBeanDao.TABLENAME; Cursor cursor = DbHelper.getDaoSession().getDatabase().rawQuery(sql, null); if (!cursor.moveToFirst()) return groupList; do { String group = cursor.getString(0); if (TextUtils.isEmpty(group) || TextUtils.isEmpty(group.trim())) continue; for (String item : group.split("\\s*[,;,;]\\s*")) { if (TextUtils.isEmpty(item) || groupList.contains(item)) continue; groupList.add(item); } } while (cursor.moveToNext()); Collections.sort(groupList); return groupList; } public static Observable> importSource(String string) { if (StringUtils.isTrimEmpty(string)) return null; string = string.trim(); if (NetworkUtils.isIPv4Address(string)) { string = String.format("http://%s:65501", string); } if (StringUtils.isJsonType(string)) { return importBookSourceFromJson(string.trim()) .compose(RxUtils::toSimpleSingle); }else if(StringUtils.isCompressJsonType(string)){ return importBookSourceFromJson(StringUtils.unCompressJson(string)) .compose(RxUtils::toSimpleSingle); } if (NetworkUtils.isUrl(string)) { return BaseModelImpl.getInstance().getRetrofitString(StringUtils.getBaseUrl(string), "utf-8") .create(IHttpGetApi.class) .get(string, AnalyzeHeaders.getDefaultHeader()) .flatMap(rsp -> importBookSourceFromJson(rsp.body())) .compose(RxUtils::toSimpleSingle); } return Observable.error(new Exception("不是Json或Url格式")); } private static Observable> importBookSourceFromJson(String json) { return Observable.create(e -> { List bookSourceBeans = new ArrayList<>(); if (StringUtils.isJsonArray(json)) { try { bookSourceBeans = GsonUtils.parseJArray(json, BookSourceBean.class); for (BookSourceBean bookSourceBean : bookSourceBeans) { if (bookSourceBean.containsGroup("删除")) { DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.BookSourceUrl.eq(bookSourceBean.getBookSourceUrl())) .buildDelete().executeDeleteWithoutDetachingEntities(); } else { try { new URL(bookSourceBean.getBookSourceUrl()); bookSourceBean.setSerialNumber(0); SourceHelp.INSTANCE.insertBookSource(bookSourceBean); } catch (Exception exception) { DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.BookSourceUrl.eq(bookSourceBean.getBookSourceUrl())) .buildDelete().executeDeleteWithoutDetachingEntities(); } } } e.onNext(bookSourceBeans); e.onComplete(); return; } catch (Exception ignored) { } } if (StringUtils.isJsonObject(json)) { try { BookSourceBean bookSourceBean = GsonUtils.parseJObject(json, BookSourceBean.class); addBookSource(bookSourceBean); bookSourceBeans.add(bookSourceBean); e.onNext(bookSourceBeans); e.onComplete(); return; } catch (Exception ignored) { } } e.onError(new Throwable("格式不对")); }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/Exceptions.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.model class AppException(msg: String) : Exception(msg) /** * */ class NoStackTraceException(msg: String) : Exception(msg) { override fun fillInStackTrace(): Throwable { return this } } /** * 目录为空 */ class TocEmptyException(msg: String) : Exception(msg) { override fun fillInStackTrace(): Throwable { return this } } /** * 内容为空 */ class ContentEmptyException(msg: String) : Exception(msg) { override fun fillInStackTrace(): Throwable { return this } } /** * 并发限制 */ class ConcurrentException(msg: String, val waitTime: Long) : Exception(msg) { override fun fillInStackTrace(): Throwable { return this } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/ImportBookModel.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.model; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.LocBookShelfBean; import com.kunfei.bookshelf.help.BookshelfHelp; import java.io.File; import io.reactivex.Observable; import static com.kunfei.bookshelf.utils.StringUtils.getString; public class ImportBookModel extends BaseModelImpl { public static ImportBookModel getInstance() { return new ImportBookModel(); } public Observable importBook(final File file) { return Observable.create(e -> { //判断文件是否存在 boolean isNew = false; BookShelfBean bookShelfBean = BookshelfHelp.getBook(file.getAbsolutePath()); if (bookShelfBean == null) { isNew = true; bookShelfBean = new BookShelfBean(); bookShelfBean.setHasUpdate(true); bookShelfBean.setFinalDate(System.currentTimeMillis()); bookShelfBean.setDurChapter(0); bookShelfBean.setDurChapterPage(0); bookShelfBean.setGroup(3); bookShelfBean.setTag(BookShelfBean.LOCAL_TAG); bookShelfBean.setNoteUrl(file.getAbsolutePath()); bookShelfBean.setAllowUpdate(false); BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean(); String fileName = file.getName(); int lastDotIndex = file.getName().lastIndexOf("."); if (lastDotIndex > 0) fileName = fileName.substring(0, lastDotIndex); int authorIndex = fileName.indexOf("作者"); if (authorIndex != -1) { bookInfoBean.setAuthor(fileName.substring(authorIndex)); fileName = fileName.substring(0, authorIndex).trim(); } else { bookInfoBean.setAuthor(""); } int smhStart = fileName.indexOf("《"); int smhEnd = fileName.indexOf("》"); if (smhStart != -1 && smhEnd != -1) { bookInfoBean.setName(fileName.substring(smhStart + 1, smhEnd)); } else { bookInfoBean.setName(fileName); } bookInfoBean.setFinalRefreshData(file.lastModified()); bookInfoBean.setCoverUrl(""); bookInfoBean.setNoteUrl(file.getAbsolutePath()); bookInfoBean.setTag(BookShelfBean.LOCAL_TAG); bookInfoBean.setOrigin(getString(R.string.local)); DbHelper.getDaoSession().getBookInfoBeanDao().insertOrReplace(bookInfoBean); DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean); } e.onNext(new LocBookShelfBean(isNew, bookShelfBean)); e.onComplete(); }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/ReplaceRuleManager.java ================================================ package com.kunfei.bookshelf.model; import android.text.TextUtils; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import com.kunfei.bookshelf.dao.ReplaceRuleBeanDao; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders; import com.kunfei.bookshelf.model.impl.IHttpGetApi; import com.kunfei.bookshelf.utils.GsonUtils; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.StringUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; /** * Created by GKF on 2018/2/12. * 替换规则管理 */ public class ReplaceRuleManager { private static List replaceRuleBeansEnabled; public static List getEnabled() { if (replaceRuleBeansEnabled == null) { replaceRuleBeansEnabled = DbHelper.getDaoSession() .getReplaceRuleBeanDao().queryBuilder() .where(ReplaceRuleBeanDao.Properties.Enable.eq(true)) .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber) .list(); } return replaceRuleBeansEnabled; } // 合并广告话术规则 public static Single mergeAdRules(ReplaceRuleBean replaceRuleBean) { String rule = formateAdRule(replaceRuleBean.getRegex()); /* String summary=replaceRuleBean.getReplaceSummary(); if(summary==null) summary=""; String sumary_pre=summary.split("-")[0];*/ int sn = replaceRuleBean.getSerialNumber(); if (sn == 0) { sn = (int) (DbHelper.getDaoSession().getReplaceRuleBeanDao().queryBuilder().count() + 1); replaceRuleBean.setSerialNumber(sn); } List list = DbHelper.getDaoSession() .getReplaceRuleBeanDao().queryBuilder() .where(ReplaceRuleBeanDao.Properties.Enable.eq(true)) .where(ReplaceRuleBeanDao.Properties.ReplaceSummary.eq(replaceRuleBean.getReplaceSummary())) .where(ReplaceRuleBeanDao.Properties.SerialNumber.notEq(sn)) .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber) .list(); if (list.size() < 1) { replaceRuleBean.setRegex(rule); return saveData(replaceRuleBean); } else { StringBuffer buffer = new StringBuffer(rule); for (ReplaceRuleBean li : list) { buffer.append('\n'); buffer.append(li.getRegex()); // buffer.append(formateAdRule(rule.getRegex())); } replaceRuleBean.setRegex(formateAdRule(buffer.toString())); return Single.create((SingleOnSubscribe) emitter -> { DbHelper.getDaoSession().getReplaceRuleBeanDao().insertOrReplace(replaceRuleBean); for (ReplaceRuleBean li : list) { DbHelper.getDaoSession().getReplaceRuleBeanDao().delete(li); } refreshDataS(); emitter.onSuccess(true); }).compose(RxUtils::toSimpleSingle); } } // 把输入的规则进行预处理(分段、排序、去重)。保存的是普通多行文本。 public static String formateAdRule(String rule) { if (rule == null) return ""; String result = rule.trim(); if (result.length() < 1) return ""; String string = rule // 用中文中的.视为。进行分段 .replaceAll("(?<=([^a-zA-Z\\p{P}]{4,8}))\\.+(?![^a-zA-Z\\p{P}]{4,8})","\n") // 用常见的适合分段的标点进行分段,句首句尾除外 // .replaceAll("([^\\p{P}\n^])([…,,::?。!?!~<>《》【】()()]+)([^\\p{P}\n$])", "$1\n$3") // 表达式无法解决句尾连续多个符号的问题 // .replaceAll("[…,,::?。!?!~<>《》【】()()]+(?!\\s*\n|$)", "\n") .replaceAll("(?《》【】()()]+)(?![\\p{P}\n$])", "\n") ; String[] lines = string.split("\n"); List list = new ArrayList<>(); for (String s : lines) { s = s.trim() // .replaceAll("\\s+", "\\s") ; if (!list.contains(s)) { list.add(s); } } Collections.sort(list); StringBuffer buffer = new StringBuffer(rule.length() + 1); for (int i = 0; i < list.size(); i++) { buffer.append('\n'); buffer.append(list.get(i)); } return buffer.toString().trim(); } public static Single> getAll() { return Single.create((SingleOnSubscribe>) emitter -> emitter.onSuccess(DbHelper.getDaoSession() .getReplaceRuleBeanDao().queryBuilder() .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber) .list())).compose(RxUtils::toSimpleSingle); } public static Single saveData(ReplaceRuleBean replaceRuleBean) { return Single.create((SingleOnSubscribe) emitter -> { if (replaceRuleBean.getSerialNumber() == 0) { replaceRuleBean.setSerialNumber((int) (DbHelper.getDaoSession().getReplaceRuleBeanDao().queryBuilder().count() + 1)); } DbHelper.getDaoSession().getReplaceRuleBeanDao().insertOrReplace(replaceRuleBean); refreshDataS(); emitter.onSuccess(true); }).compose(RxUtils::toSimpleSingle); } public static void delData(ReplaceRuleBean replaceRuleBean) { DbHelper.getDaoSession().getReplaceRuleBeanDao().delete(replaceRuleBean); refreshDataS(); } public static void addDataS(List replaceRuleBeans) { if (replaceRuleBeans != null && replaceRuleBeans.size() > 0) { DbHelper.getDaoSession().getReplaceRuleBeanDao().insertOrReplaceInTx(replaceRuleBeans); refreshDataS(); } } public static void delDataS(List replaceRuleBeans) { for (ReplaceRuleBean replaceRuleBean : replaceRuleBeans) { DbHelper.getDaoSession().getReplaceRuleBeanDao().delete(replaceRuleBean); } refreshDataS(); } private static void refreshDataS() { replaceRuleBeansEnabled = DbHelper.getDaoSession() .getReplaceRuleBeanDao().queryBuilder() .where(ReplaceRuleBeanDao.Properties.Enable.eq(true)) .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber) .list(); } public static Observable importReplaceRule(String text) { if (TextUtils.isEmpty(text)) return null; text = text.trim(); if (text.length() == 0) return null; if (StringUtils.isJsonType(text)) { return importReplaceRuleO(text) .compose(RxUtils::toSimpleSingle); } if (NetworkUtils.isUrl(text)) { return BaseModelImpl.getInstance().getRetrofitString(StringUtils.getBaseUrl(text), "utf-8") .create(IHttpGetApi.class) .get(text, AnalyzeHeaders.getDefaultHeader()) .flatMap(rsp -> importReplaceRuleO(rsp.body())) .compose(RxUtils::toSimpleSingle); } return Observable.error(new Exception("不是Json或Url格式")); } private static Observable importReplaceRuleO(String json) { return Observable.create(e -> { try { List replaceRuleBeans = GsonUtils.parseJArray(json, ReplaceRuleBean.class); addDataS(replaceRuleBeans); e.onNext(true); } catch (Exception e1) { e1.printStackTrace(); e.onNext(false); } e.onComplete(); }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/SavedSource.java ================================================ package com.kunfei.bookshelf.model; import com.kunfei.bookshelf.bean.BookSourceBean; public class SavedSource { public static SavedSource Instance = new SavedSource(); private String bookName; private long saveTime; private String sourceUrl; private SavedSource() { this.bookName = ""; saveTime = 0; } public String getBookName() { return this.bookName; } public void setBookName(String bookName) { this.bookName = bookName; } public long getSaveTime() { return saveTime; } public void setSaveTime(long saveTime) { this.saveTime = saveTime; } public BookSourceBean getBookSource() { if (sourceUrl == null) { return null; } return BookSourceManager.getBookSourceByUrl(sourceUrl); } public void setBookSource(BookSourceBean bookSource) { if (bookSource != null) { this.sourceUrl = bookSource.getBookSourceUrl(); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/SearchBookModel.java ================================================ package com.kunfei.bookshelf.model; import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.reactivex.Observer; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; /** * Created by GKF on 2018/1/16. * 搜索 */ public class SearchBookModel { private Handler handler = new Handler(Looper.getMainLooper()); private ExecutorService executorService; private Scheduler scheduler; private long startThisSearchTime; private List searchEngineS = new ArrayList<>(); private int threadsNum; private int search_result_filter_grade; private int page = 0; private int searchEngineIndex; private int searchSuccessNum; private CompositeDisposable compositeDisposable; private OnSearchListener searchListener; public SearchBookModel(OnSearchListener searchListener) { this(searchListener, BookSourceManager.getSelectedBookSource()); } public SearchBookModel(OnSearchListener searchListener, List sourceBeanList) { this.searchListener = searchListener; threadsNum = MApplication.getConfigPreferences().getInt(MApplication.getInstance().getString(R.string.pk_threads_num), 6); executorService = Executors.newFixedThreadPool(threadsNum); scheduler = Schedulers.from(executorService); compositeDisposable = new CompositeDisposable(); search_result_filter_grade = MApplication.getConfigPreferences().getInt(MApplication.getInstance().getString(R.string.pk_search_result_filter_grade), 0); if (sourceBeanList == null) { initSearchEngineS(BookSourceManager.getSelectedBookSource()); } else { initSearchEngineS(sourceBeanList); } } /** * 搜索引擎初始化 */ public void initSearchEngineS(@NonNull List sourceBeanList) { searchEngineS.clear(); for (BookSourceBean bookSourceBean : sourceBeanList) { if (bookSourceBean.getEnable()) { SearchEngine se = new SearchEngine(); se.setTag(bookSourceBean.getBookSourceUrl()); se.setHasMore(true); searchEngineS.add(se); } } } public void searchReNew() { compositeDisposable.dispose(); compositeDisposable = new CompositeDisposable(); page = 0; for (SearchEngine searchEngine : searchEngineS) { searchEngine.setHasMore(true); } } public void stopSearch() { compositeDisposable.dispose(); compositeDisposable = new CompositeDisposable(); handler.post(() -> { searchListener.refreshFinish(true); searchListener.loadMoreFinish(true); }); } private void searchBookError(Throwable throwable) { compositeDisposable.dispose(); compositeDisposable = new CompositeDisposable(); handler.post(() -> { searchListener.refreshFinish(true); searchListener.loadMoreFinish(true); searchListener.searchBookError(throwable); }); } public void onDestroy() { stopSearch(); executorService.shutdown(); } public void setSearchTime(long searchTime) { this.startThisSearchTime = searchTime; } public void search(final String content, final long searchTime, List bookShelfS, Boolean fromError) { if (searchTime != startThisSearchTime) { return; } if (!fromError) { page++; } if (page == 0) { page = 1; } if (page == 1) { handler.post(() -> searchListener.refreshSearchBook()); } if (searchEngineS.size() == 0) { searchBookError(new Throwable("没有选中任何书源")); return; } searchSuccessNum = 0; searchEngineIndex = -1; for (int i = 0; i < threadsNum; i++) { searchOnEngine(content, bookShelfS, searchTime); } } private synchronized void searchOnEngine(final String content, List bookShelfS, final long searchTime) { if (searchTime != startThisSearchTime) { return; } searchEngineIndex++; long startTime = System.currentTimeMillis(); if (searchEngineIndex < searchEngineS.size()) { final SearchEngine searchEngine = searchEngineS.get(searchEngineIndex); if (searchEngine.getHasMore()) { WebBookModel.getInstance() .searchBook(content, page, searchEngine.getTag()) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(List searchBookBeans) { if (searchTime == startThisSearchTime) { searchSuccessNum++; if (searchBookBeans.size() > 0) { for (SearchBookBean temp : searchBookBeans) { int searchTime = (int) (System.currentTimeMillis() - startTime) / 1000; temp.setSearchTime(searchTime); for (BookShelfBean bookShelfBean : bookShelfS) { if (Objects.equals(bookShelfBean.getNoteUrl(), temp.getNoteUrl())) { temp.setIsCurrentSource(true); break; } } } if(search_result_filter_grade>0){ for(int index=0;index searchListener.refreshFinish(false)); } for (SearchEngine engine : searchEngineS) { if (engine.hasMore) { handler.post(() -> searchListener.loadMoreFinish(false)); return; } } handler.post(() -> searchListener.loadMoreFinish(true)); } } } } public int getPage() { return page; } public void setPage(int page) { this.page = page; } public interface OnSearchListener { void refreshSearchBook(); void refreshFinish(Boolean isAll); void loadMoreFinish(Boolean isAll); void loadMoreSearchBook(List searchBookBeanList); void searchBookError(Throwable throwable); int getItemCount(); } private static class SearchEngine { private String tag; private Boolean hasMore; public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } Boolean getHasMore() { return hasMore; } void setHasMore(Boolean hasMore) { this.hasMore = hasMore; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/TxtChapterRuleManager.java ================================================ package com.kunfei.bookshelf.model; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.bean.TxtChapterRuleBean; import com.kunfei.bookshelf.dao.TxtChapterRuleBeanDao; import com.kunfei.bookshelf.utils.GsonUtils; import com.kunfei.bookshelf.utils.IOUtils; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; public class TxtChapterRuleManager { public static List getAll() { List beans = DbHelper.getDaoSession().getTxtChapterRuleBeanDao().loadAll(); if (beans.isEmpty()) { return getDefault(); } return beans; } public static List getEnabled() { List beans = DbHelper.getDaoSession().getTxtChapterRuleBeanDao().queryBuilder() .where(TxtChapterRuleBeanDao.Properties.Enable.eq(true)) .list(); if (beans.isEmpty()) { return getAll(); } return beans; } public static List enabledRuleList() { List beans = getEnabled(); List ruleList = new ArrayList<>(); for (TxtChapterRuleBean chapterRuleBean : beans) { ruleList.add(chapterRuleBean.getRule()); } return ruleList; } public static List getDefault() { String json = null; try { InputStream inputStream = MApplication.getInstance().getAssets().open("txtChapterRule.json"); json = IOUtils.toString(inputStream); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } List ruleBeanList = GsonUtils.parseJArray(json, TxtChapterRuleBean.class); if (ruleBeanList != null) { DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplaceInTx(ruleBeanList); return ruleBeanList; } return new ArrayList<>(); } public static void del(TxtChapterRuleBean txtChapterRuleBean) { DbHelper.getDaoSession().getTxtChapterRuleBeanDao().delete(txtChapterRuleBean); } public static void del(List ruleBeanList) { for (TxtChapterRuleBean ruleBean : ruleBeanList) { del(ruleBean); } } public static void save(TxtChapterRuleBean txtChapterRuleBean) { if (txtChapterRuleBean.getSerialNumber() == null) { txtChapterRuleBean.setSerialNumber((int) DbHelper.getDaoSession().getTxtChapterRuleBeanDao().queryBuilder().count()); } DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplace(txtChapterRuleBean); } public static void save(List txtChapterRuleBeans) { DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplaceInTx(txtChapterRuleBeans); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/UpLastChapterModel.java ================================================ package com.kunfei.bookshelf.model; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import com.hwangjr.rxbus.RxBus; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.dao.SearchBookBeanDao; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.utils.RxUtils; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.Observer; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; /** * 更新换源列表里最新章节 */ public class UpLastChapterModel { public static UpLastChapterModel model; private CompositeDisposable compositeDisposable; private ExecutorService executorService; private Scheduler scheduler; private Handler handler = new Handler(Looper.getMainLooper()); private List searchBookBeanList; private int upIndex; public static UpLastChapterModel getInstance() { if (model == null) { model = new UpLastChapterModel(); } return model; } private UpLastChapterModel() { executorService = Executors.newFixedThreadPool(5); scheduler = Schedulers.from(executorService); compositeDisposable = new CompositeDisposable(); searchBookBeanList = new ArrayList<>(); } public void startUpdate() { if (!MApplication.getConfigPreferences().getBoolean("upChangeSourceLastChapter", false)) return; if (compositeDisposable.size() > 0) return; List beanList = new ArrayList<>(); Observable.create((ObservableOnSubscribe) e -> { List bookShelfBeans = BookshelfHelp.getAllBook(); for (BookShelfBean bookShelfBean : bookShelfBeans) { if (!Objects.equals(bookShelfBean.getTag(), BookShelfBean.LOCAL_TAG)) { e.onNext(bookShelfBean); } } e.onComplete(); }).flatMap(this::findSearchBookBean) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(SearchBookBean searchBookBean) { beanList.add(searchBookBean); } @Override public void onError(Throwable e) { e.printStackTrace(); } @Override public void onComplete() { startUpdate(beanList); } }); } public synchronized void startUpdate(List beanList) { compositeDisposable.dispose(); executorService.shutdown(); executorService = Executors.newFixedThreadPool(5); scheduler = Schedulers.from(executorService); compositeDisposable = new CompositeDisposable(); this.searchBookBeanList = beanList; upIndex = -1; for (int i = 0; i < 5; i++) { doUpdate(); } } private synchronized void doUpdate() { upIndex++; if (upIndex < searchBookBeanList.size()) { toBookshelf(searchBookBeanList.get(upIndex)) .flatMap(this::getChapterList) .flatMap(this::saveSearchBookBean) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); handler.postDelayed(() -> { if (!d.isDisposed()) { d.dispose(); doUpdate(); } }, 20 * 1000); } @Override public void onNext(SearchBookBean searchBookBean) { RxBus.get().post(RxBusTag.UP_SEARCH_BOOK, searchBookBean); doUpdate(); } @Override public void onError(Throwable e) { doUpdate(); } @Override public void onComplete() { } }); } } private void stopUp() { if (compositeDisposable != null && !compositeDisposable.isDisposed()) { compositeDisposable.dispose(); } compositeDisposable = new CompositeDisposable(); } public static void destroy() { if (model != null) { model.stopUp(); model.executorService.shutdownNow(); model = null; } } private Observable findSearchBookBean(BookShelfBean bookShelf) { return Observable.create(e -> { List searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder() .where(SearchBookBeanDao.Properties.Name.eq(bookShelf.getBookInfoBean().getName())).list(); for (SearchBookBean searchBookBean : searchBookBeans) { BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(searchBookBean.getTag()); if (sourceBean == null) { DbHelper.getDaoSession().getSearchBookBeanDao().delete(searchBookBean); } else if (System.currentTimeMillis() - searchBookBean.getUpTime() > 1000 * 60 * 60 && sourceBean.getEnable()) { e.onNext(searchBookBean); } } e.onComplete(); }); } private Observable toBookshelf(SearchBookBean searchBookBean) { return Observable.create(e -> { BookShelfBean bookShelfBean = BookshelfHelp.getBookFromSearchBook(searchBookBean); e.onNext(bookShelfBean); e.onComplete(); }); } private Observable> getChapterList(BookShelfBean bookShelfBean) { if (TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getChapterUrl())) { return WebBookModel.getInstance().getBookInfo(bookShelfBean) .flatMap(bookShelf -> WebBookModel.getInstance().getChapterList(bookShelf)); } else { return WebBookModel.getInstance().getChapterList(bookShelfBean); } } private Observable saveSearchBookBean(List chapterBeanList) { return Observable.create(e -> { BookChapterBean chapterBean = chapterBeanList.get(chapterBeanList.size() - 1); SearchBookBean searchBookBean = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder() .where(SearchBookBeanDao.Properties.NoteUrl.eq(chapterBean.getNoteUrl())) .unique(); if (searchBookBean != null) { searchBookBean.setLastChapter(chapterBean.getDurChapterName()); searchBookBean.setAddTime(System.currentTimeMillis()); DbHelper.getDaoSession().getSearchBookBeanDao().insertOrReplace(searchBookBean); e.onNext(searchBookBean); } e.onComplete(); }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/WebBookModel.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.model; import android.annotation.SuppressLint; import com.hwangjr.rxbus.RxBus; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.bean.BaseChapterBean; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.content.WebBook; import java.util.List; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import static android.text.TextUtils.isEmpty; import static com.kunfei.bookshelf.constant.AppConstant.TIME_OUT; public class WebBookModel { public static WebBookModel getInstance() { return new WebBookModel(); } /** * 网络请求并解析书籍信息 * return BookShelfBean */ public Observable getBookInfo(BookShelfBean bookShelfBean) { return WebBook.getInstance(bookShelfBean.getTag()) .getBookInfo(bookShelfBean) .timeout(TIME_OUT, TimeUnit.SECONDS); } /** * 网络解析图书目录 * return BookShelfBean */ public Observable> getChapterList(final BookShelfBean bookShelfBean) { return WebBook.getInstance(bookShelfBean.getTag()) .getChapterList(bookShelfBean) .flatMap((chapterList) -> upChapterList(bookShelfBean, chapterList)) .timeout(TIME_OUT, TimeUnit.SECONDS); } /** * 章节缓存 */ public Observable getBookContent(BookShelfBean bookShelfBean, BaseChapterBean chapterBean, BaseChapterBean nextChapterBean) { return WebBook.getInstance(chapterBean.getTag()) .getBookContent(chapterBean, nextChapterBean, bookShelfBean) .flatMap((bookContentBean -> saveContent(bookShelfBean.getBookInfoBean(), chapterBean, bookContentBean))) .timeout(TIME_OUT, TimeUnit.SECONDS); } /** * 搜索 */ public Observable> searchBook(String content, int page, String tag) { return WebBook.getInstance(tag) .searchBook(content, page) .timeout(TIME_OUT, TimeUnit.SECONDS); } /** * 发现页 */ public Observable> findBook(String url, int page, String tag) { return WebBook.getInstance(tag) .findBook(url, page) .timeout(TIME_OUT, TimeUnit.SECONDS); } /** * 更新目录 */ private Observable> upChapterList(BookShelfBean bookShelfBean, List chapterList) { return Observable.create(e -> { for (int i = 0; i < chapterList.size(); i++) { BookChapterBean chapter = chapterList.get(i); chapter.setDurChapterIndex(i); chapter.setTag(bookShelfBean.getTag()); chapter.setNoteUrl(bookShelfBean.getNoteUrl()); } if (bookShelfBean.getChapterListSize() < chapterList.size()) { bookShelfBean.setHasUpdate(true); bookShelfBean.setFinalRefreshData(System.currentTimeMillis()); bookShelfBean.getBookInfoBean().setFinalRefreshData(System.currentTimeMillis()); } if (!chapterList.isEmpty()) { bookShelfBean.setChapterListSize(chapterList.size()); bookShelfBean.setDurChapter(Math.min(bookShelfBean.getDurChapter(), bookShelfBean.getChapterListSize() - 1)); bookShelfBean.setDurChapterName(chapterList.get(bookShelfBean.getDurChapter()).getDurChapterName()); bookShelfBean.setLastChapterName(chapterList.get(chapterList.size() - 1).getDurChapterName()); } e.onNext(chapterList); e.onComplete(); }); } /** * 保存章节 */ @SuppressLint("DefaultLocale") private Observable saveContent(BookInfoBean infoBean, BaseChapterBean chapterBean, BookContentBean bookContentBean) { return Observable.create(e -> { bookContentBean.setNoteUrl(chapterBean.getNoteUrl()); if (isEmpty(bookContentBean.getDurChapterContent())) { e.onError(new Throwable("下载章节出错")); } else if (infoBean.isAudio()) { bookContentBean.setTimeMillis(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); DbHelper.getDaoSession().getBookContentBeanDao().insertOrReplace(bookContentBean); e.onNext(bookContentBean); } else if (BookshelfHelp.saveChapterInfo(infoBean.getName() + "-" + chapterBean.getTag(), chapterBean.getDurChapterIndex(), chapterBean.getDurChapterName(), bookContentBean.getDurChapterContent())) { RxBus.get().post(RxBusTag.CHAPTER_CHANGE, chapterBean); e.onNext(bookContentBean); } else { e.onError(new Throwable("保存章节出错")); } e.onComplete(); }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByJSonPath.java ================================================ package com.kunfei.bookshelf.model.analyzeRule; import android.text.TextUtils; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.ReadContext; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class AnalyzeByJSonPath { private static final Pattern jsonRulePattern = Pattern.compile("(?<=\\{)\\$\\..+?(?=\\})"); private ReadContext ctx; public AnalyzeByJSonPath parse(Object json) { if (json instanceof String) { ctx = JsonPath.parse((String) json); } else { ctx = JsonPath.parse(json); } return this; } public String getString(String rule) { if (TextUtils.isEmpty(rule)) return null; String result = ""; String[] rules; String elementsType; if (rule.contains("{$.")) { result = rule; Matcher matcher = jsonRulePattern.matcher(rule); while (matcher.find()) { result = result.replace(String.format("{%s}", matcher.group()), getString(matcher.group().trim())); } return result; } if (rule.contains("&&")) { rules = rule.split("&&"); elementsType = "&"; } else { rules = rule.split("\\|\\|"); elementsType = "|"; } if (rules.length == 1) { try { Object object = ctx.read(rule); if (object instanceof List) { StringBuilder builder = new StringBuilder(); //noinspection rawtypes for (Object o : (List) object) { builder.append(o).append("\n"); } result = builder.toString().replaceAll("\\n$", ""); } else { result = String.valueOf(object); } } catch (Exception e) { if (!rule.startsWith("$.")) { return rule; } } return result; } else { List textS = new ArrayList<>(); for (String rl : rules) { String temp = getString(rl); if (!TextUtils.isEmpty(temp)) { textS.add(temp); if (elementsType.equals("|")) { break; } } } return TextUtils.join(",", textS).trim(); } } List getStringList(String rule) { List result = new ArrayList<>(); if (TextUtils.isEmpty(rule)) return result; String[] rules; String elementsType; if (rule.contains("&&")) { rules = rule.split("&&"); elementsType = "&"; } else if (rule.contains("%%")) { rules = rule.split("%%"); elementsType = "%"; } else { rules = rule.split("\\|\\|"); elementsType = "|"; } if (rules.length == 1) { if (!rule.contains("{$.")) { try { Object object = ctx.read(rule); if (object == null) return result; if (object instanceof List) { //noinspection rawtypes for (Object o : ((List) object)) result.add(String.valueOf(o)); } else { result.add(String.valueOf(object)); } } catch (Exception ignored) { } } else { Matcher matcher = jsonRulePattern.matcher(rule); while (matcher.find()) { List stringList = getStringList(matcher.group()); for (String s : stringList) { result.add(rule.replace(String.format("{%s}", matcher.group()), s)); } } } } else { List> results = new ArrayList<>(); for (String rl : rules) { List temp = getStringList(rl); if (temp != null && !temp.isEmpty()) { results.add(temp); if (temp.size() > 0 && elementsType.equals("|")) { break; } } } if (results.size() > 0) { if ("%".equals(elementsType)) { for (int i = 0; i < results.get(0).size(); i++) { for (List temp : results) { if (i < temp.size()) { result.add(temp.get(i)); } } } } else { for (List temp : results) { result.addAll(temp); } } } } return result; } Object getObject(String rule) { return ctx.read(rule); } List getList(String rule) { List result = new ArrayList<>(); if (TextUtils.isEmpty(rule)) return result; String elementsType; String[] rules; if (rule.contains("&&")) { rules = rule.split("&&"); elementsType = "&"; } else if (rule.contains("%%")) { rules = rule.split("%%"); elementsType = "%"; } else { rules = rule.split("\\|\\|"); elementsType = "|"; } if (rules.length == 1) { try { return ctx.read(rules[0]); } catch (Exception e) { return null; } } else { List> results = new ArrayList<>(); for (String rl : rules) { List temp = getList(rl); if (temp != null && !temp.isEmpty()) { results.add(temp); if (temp.size() > 0 && elementsType.equals("|")) { break; } } } if (results.size() > 0) { if ("%".equals(elementsType)) { for (int i = 0; i < results.get(0).size(); i++) { for (List temp : results) { if (i < temp.size()) { result.add(temp.get(i)); } } } } else { for (List temp : results) { result.addAll(temp); } } } } return result; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByJSoup.java ================================================ package com.kunfei.bookshelf.model.analyzeRule; import android.text.TextUtils; import com.kunfei.bookshelf.utils.StringUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import org.jsoup.nodes.TextNode; import org.jsoup.select.Collector; import org.jsoup.select.Elements; import org.jsoup.select.Evaluator; import org.seimicrawler.xpath.JXNode; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static android.text.TextUtils.isEmpty; /** * Created by GKF on 2018/1/25. * 书源规则解析 */ public class AnalyzeByJSoup { private Element element; public AnalyzeByJSoup parse(Object doc) { if (doc instanceof Element) { element = (Element) doc; } else if (doc instanceof JXNode) { JXNode jxNode = (JXNode) doc; if (jxNode.isElement()) { element = jxNode.asElement(); } else { element = Jsoup.parse(jxNode.value().toString()); } } else { element = Jsoup.parse(doc.toString()); } return this; } /** * 获取列表 */ Elements getElements(String rule) { return getElements(element, rule); } /** * 合并内容列表,得到内容 */ String getString(String ruleStr) { if (isEmpty(ruleStr)) { return null; } List textS = getStringList(ruleStr); if (textS.size() == 0) { return null; } return TextUtils.join(",", textS).trim(); } /** * 获取一个字符串 **/ String getString0(String ruleStr) { List urlList = getStringList(ruleStr); if (!urlList.isEmpty()) { return urlList.get(0); } return ""; } /** * 获取所有内容列表 */ List getStringList(String ruleStr) { List textS = new ArrayList<>(); if (isEmpty(ruleStr)) { return textS; } //拆分规则 SourceRule sourceRule = new SourceRule(ruleStr); if (isEmpty(sourceRule.elementsRule)) { textS.add(element.data()); } else { String elementsType; String[] ruleStrS; if (sourceRule.elementsRule.contains("&")) { elementsType = "&"; ruleStrS = sourceRule.elementsRule.split("&+"); } else if (sourceRule.elementsRule.contains("%%")) { elementsType = "%"; ruleStrS = sourceRule.elementsRule.split("%%"); } else { elementsType = "|"; if (sourceRule.isCss) { ruleStrS = sourceRule.elementsRule.split("\\|\\|"); } else { ruleStrS = sourceRule.elementsRule.split("\\|+"); } } List> results = new ArrayList<>(); for (String ruleStrX : ruleStrS) { List temp; if (sourceRule.isCss) { int lastIndex = ruleStrX.lastIndexOf('@'); temp = getResultLast(element.select(ruleStrX.substring(0, lastIndex)), ruleStrX.substring(lastIndex + 1)); } else { temp = getResultList(ruleStrX); } if (temp != null && !temp.isEmpty()) { results.add(temp); if (!results.isEmpty() && elementsType.equals("|")) { break; } } } if (results.size() > 0) { if ("%".equals(elementsType)) { for (int i = 0; i < results.get(0).size(); i++) { for (List temp : results) { if (i < temp.size()) { textS.add(temp.get(i)); } } } } else { for (List temp : results) { textS.addAll(temp); } } } } if (!isEmpty(sourceRule.replaceRegex)) { List tempList = new ArrayList<>(textS); textS.clear(); for (String text : tempList) { text = text.replaceAll(sourceRule.replaceRegex, sourceRule.replacement); if (text.length() > 0) { textS.add(text); } } } return textS; } /** * 获取Elements */ private Elements getElements(Element temp, String rule) { Elements elements = new Elements(); if (temp == null || isEmpty(rule)) { return elements; } SourceRule sourceRule = new SourceRule(rule); String elementsType; String[] ruleStrS; if (sourceRule.elementsRule.contains("&")) { elementsType = "&"; ruleStrS = sourceRule.elementsRule.split("&+"); } else if (sourceRule.elementsRule.contains("%")) { elementsType = "%"; ruleStrS = sourceRule.elementsRule.split("%+"); } else { elementsType = "|"; if (sourceRule.isCss) { ruleStrS = sourceRule.elementsRule.split("\\|\\|"); } else { ruleStrS = sourceRule.elementsRule.split("\\|+"); } } List elementsList = new ArrayList<>(); if (sourceRule.isCss) { for (String ruleStr : ruleStrS) { Elements tempS = temp.select(ruleStr); elementsList.add(tempS); if (tempS.size() > 0 && elementsType.equals("|")) { break; } } } else { for (String ruleStr : ruleStrS) { Elements tempS = getElementsSingle(temp, ruleStr); elementsList.add(tempS); if (tempS.size() > 0 && elementsType.equals("|")) { break; } } } if (elementsList.size() > 0) { if ("%".equals(elementsType)) { for (int i = 0; i < elementsList.get(0).size(); i++) { for (Elements es : elementsList) { if (i < es.size()) { elements.add(es.get(i)); } } } } else { for (Elements es : elementsList) { elements.addAll(es); } } } return elements; } private Elements filterElements(Elements elements, String[] rules) { if (rules == null || rules.length < 2) return elements; Elements selectedEls = new Elements(); for (Element ele : elements) { boolean isOk = false; switch (rules[0]) { case "class": isOk = ele.getElementsByClass(rules[1]).size() > 0; break; case "id": isOk = ele.getElementById(rules[1]) != null; break; case "tag": isOk = ele.getElementsByTag(rules[1]).size() > 0; break; case "text": isOk = ele.getElementsContainingOwnText(rules[1]).size() > 0; break; } if (isOk) { selectedEls.add(ele); } } return selectedEls; } /** * 获取Elements按照一个规则 */ private Elements getElementsSingle(Element temp, String rule) { Elements elements = new Elements(); try { String[] rs = rule.trim().split("@"); if (rs.length > 1) { elements.add(temp); for (String rl : rs) { Elements es = new Elements(); for (Element et : elements) { es.addAll(getElements(et, rl)); } elements.clear(); elements.addAll(es); } } else { String[] rulePcx = rule.split("!"); String[] rulePc = rulePcx[0].trim().split(">"); String[] rules = rulePc[0].trim().split("\\."); String[] filterRules = null; boolean needFilterElements = rulePc.length > 1 && !isEmpty(rulePc[1].trim()); if (needFilterElements) { filterRules = rulePc[1].trim().split("\\."); filterRules[0] = filterRules[0].trim(); List validKeys = Arrays.asList("class", "id", "tag", "text"); if (filterRules.length < 2 || !validKeys.contains(filterRules[0]) || isEmpty(filterRules[1].trim())) { needFilterElements = false; } filterRules[1] = filterRules[1].trim(); } switch (rules[0]) { case "children": Elements children = temp.children(); if (needFilterElements) children = filterElements(children, filterRules); elements.addAll(children); break; case "class": Elements elementsByClass = temp.getElementsByClass(rules[1]); if (rules.length == 3) { int index = Integer.parseInt(rules[2]); if (index < 0) { elements.add(elementsByClass.get(elementsByClass.size() + index)); } else { elements.add(elementsByClass.get(index)); } } else { if (needFilterElements) elementsByClass = filterElements(elementsByClass, filterRules); elements.addAll(elementsByClass); } break; case "tag": Elements elementsByTag = temp.getElementsByTag(rules[1]); if (rules.length == 3) { int index = Integer.parseInt(rules[2]); if (index < 0) { elements.add(elementsByTag.get(elementsByTag.size() + index)); } else { elements.add(elementsByTag.get(index)); } } else { if (needFilterElements) elementsByTag = filterElements(elementsByTag, filterRules); elements.addAll(elementsByTag); } break; case "id": Elements elementsById = Collector.collect(new Evaluator.Id(rules[1]), temp); if (rules.length == 3) { int index = Integer.parseInt(rules[2]); if (index < 0) { elements.add(elementsById.get(elementsById.size() + index)); } else { elements.add(elementsById.get(index)); } } else { if (needFilterElements) elementsById = filterElements(elementsById, filterRules); elements.addAll(elementsById); } break; case "text": Elements elementsByText = temp.getElementsContainingOwnText(rules[1]); if (needFilterElements) elementsByText = filterElements(elementsByText, filterRules); elements.addAll(elementsByText); break; default: elements.addAll(temp.select(rulePcx[0])); } if (rulePcx.length > 1) { String[] rulePcs = rulePcx[1].split(":"); for (String pc : rulePcs) { int pcInt = Integer.parseInt(pc); if (pcInt < 0 && elements.size() + pcInt >= 0) { elements.set(elements.size() + pcInt, null); } else if (Integer.parseInt(pc) < elements.size()) { elements.set(Integer.parseInt(pc), null); } } Elements es = new Elements(); es.add(null); elements.removeAll(es); } } } catch (Exception ignore) { } return elements; } /** * 获取内容列表 */ private List getResultList(String ruleStr) { if (isEmpty(ruleStr)) { return null; } Elements elements = new Elements(); elements.add(element); String[] rules = ruleStr.split("@"); for (int i = 0; i < rules.length - 1; i++) { Elements es = new Elements(); for (Element elt : elements) { es.addAll(getElementsSingle(elt, rules[i])); } elements.clear(); elements = es; } if (elements.isEmpty()) { return null; } return getResultLast(elements, rules[rules.length - 1]); } /** * 根据最后一个规则获取内容 */ private List getResultLast(Elements elements, String lastRule) { List textS = new ArrayList<>(); List cText = new ArrayList<>(); try { switch (lastRule) { case "text": for (Element element : elements) { String text = element.text(); cText.add(text); } textS.add(TextUtils.join("\n", cText)); break; case "textNodes": for (Element element : elements) { List contentEs = element.textNodes(); for (int i = 0; i < contentEs.size(); i++) { String temp = contentEs.get(i).text().trim(); if (!isEmpty(temp)) { cText.add(temp); } } } textS.add(TextUtils.join("\n", cText)); break; case "ownText": for (Element element : elements) { cText.add(element.ownText()); } textS.add(TextUtils.join("\n", cText)); break; case "html": elements.select("script, style").remove(); String html = elements.html(); textS.add(html); break; case "all": textS.add(elements.outerHtml()); break; default: for (Element element : elements) { String url = element.attr(lastRule); if (!TextUtils.isEmpty(url) && !textS.contains(url)) { textS.add(url); } } } } catch (Exception ignore) { } return textS; } class SourceRule { boolean isCss = false; String elementsRule; String replaceRegex = ""; String replacement = ""; SourceRule(String ruleStr) { if (StringUtils.startWithIgnoreCase(ruleStr, "@CSS:")) { isCss = true; elementsRule = ruleStr.substring(5).trim(); return; } String[] ruleStrS; //分离正则表达式 ruleStrS = ruleStr.trim().split("#"); elementsRule = ruleStrS[0]; if (ruleStrS.length > 1) { replaceRegex = ruleStrS[1]; } if (ruleStrS.length > 2) { replacement = ruleStrS[2]; } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByRegex.java ================================================ package com.kunfei.bookshelf.model.analyzeRule; import android.os.Build; import android.text.TextUtils; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.model.content.Debug; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.StringUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import static android.text.TextUtils.isEmpty; public class AnalyzeByRegex { // 纯java模式正则表达式获取书籍详情信息 public static void getInfoOfRegex(String res, String[] regs, int index, BookShelfBean bookShelfBean, AnalyzeRule analyzer, BookSourceBean bookSourceBean, String tag) throws Exception { Matcher resM = Pattern.compile(regs[index]).matcher(res); String baseUrl = bookShelfBean.getNoteUrl(); // 创建详情信息存储容器 BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean(); // 判断规则是否有效,当搜索列表规则无效时跳过详情页处理 if (!resM.find()) { Debug.printLog(tag, "└详情预处理失败,跳过详情页解析"); Debug.printLog(tag, "┌获取目录网址"); bookInfoBean.setChapterUrl(baseUrl); bookInfoBean.setChapterListHtml(res); Debug.printLog(tag, "└" + baseUrl); return; } // 判断索引的规则是最后一个规则 if (index + 1 == regs.length) { // 获取规则列表 HashMap ruleMap = new HashMap<>(); ruleMap.put("BookName", bookSourceBean.getRuleBookName()); ruleMap.put("BookAuthor", bookSourceBean.getRuleBookAuthor()); ruleMap.put("BookKind", bookSourceBean.getRuleBookKind()); ruleMap.put("LastChapter", bookSourceBean.getRuleBookLastChapter()); ruleMap.put("Introduce", bookSourceBean.getRuleIntroduce()); ruleMap.put("CoverUrl", bookSourceBean.getRuleCoverUrl()); ruleMap.put("ChapterUrl", bookSourceBean.getRuleChapterUrl()); // 分离规则参数 List ruleName = new ArrayList<>(); List> ruleParams = new ArrayList<>(); // 创建规则参数容器 List> ruleTypes = new ArrayList<>(); // 创建规则类型容器 List hasVarParams = new ArrayList<>(); // 创建put&get标志容器 for (String key : ruleMap.keySet()) { String val = ruleMap.get(key); ruleName.add(key); hasVarParams.add(!TextUtils.isEmpty(val) && (val.contains("@put") || val.contains("@get"))); List ruleParam = new ArrayList<>(); List ruleType = new ArrayList<>(); AnalyzeByRegex.splitRegexRule(val, ruleParam, ruleType); ruleParams.add(ruleParam); ruleTypes.add(ruleType); } // 提取规则内容 HashMap ruleVal = new HashMap<>(); StringBuilder infoVal = new StringBuilder(); for (int i = ruleParams.size(); i-- > 0; ) { List ruleParam = ruleParams.get(i); List ruleType = ruleTypes.get(i); infoVal.setLength(0); for (int j = ruleParam.size(); j-- > 0; ) { int regType = ruleType.get(j); if (regType > 0) { infoVal.insert(0, resM.group(regType)); } else if (regType < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { infoVal.insert(0, resM.group(ruleParam.get(j))); } else { infoVal.insert(0, ruleParam.get(j)); } } ruleVal.put(ruleName.get(i), hasVarParams.get(i) ? AnalyzeByRegex.checkKeys(infoVal.toString(), analyzer) : infoVal.toString()); } // 保存详情信息 if (!isEmpty(ruleVal.get("BookName"))) bookInfoBean.setName(StringUtils.formatHtml(ruleVal.get("BookName"))); if (!isEmpty(ruleVal.get("BookAuthor"))) bookInfoBean.setAuthor(StringUtils.formatHtml(ruleVal.get("BookAuthor"))); if (!isEmpty(ruleVal.get("LastChapter"))) bookShelfBean.setLastChapterName(ruleVal.get("LastChapter")); if (!isEmpty(ruleVal.get("Introduce"))) bookInfoBean.setIntroduce(StringUtils.formatHtml(ruleVal.get("Introduce"))); if (!isEmpty(ruleVal.get("CoverUrl"))) bookInfoBean.setCoverUrl(ruleVal.get("CoverUrl")); if (!isEmpty(ruleVal.get("ChapterUrl"))) bookInfoBean.setChapterUrl(NetworkUtils.getAbsoluteURL(baseUrl, ruleVal.get("ChapterUrl"))); else bookInfoBean.setChapterUrl(baseUrl); //如果目录页和详情页相同,暂存页面内容供获取目录用 if (bookInfoBean.getChapterUrl().equals(baseUrl)) bookInfoBean.setChapterListHtml(res); // 输出调试信息 Debug.printLog(tag, "└详情预处理完成"); Debug.printLog(tag, "┌获取书籍名称"); Debug.printLog(tag, "└" + bookInfoBean.getName()); Debug.printLog(tag, "┌获取作者名称"); Debug.printLog(tag, "└" + bookInfoBean.getAuthor()); Debug.printLog(tag, "┌获取最新章节"); Debug.printLog(tag, "└" + bookShelfBean.getLastChapterName()); Debug.printLog(tag, "┌获取简介内容"); Debug.printLog(tag, 1, "└" + bookInfoBean.getIntroduce(), true, true); Debug.printLog(tag, "┌获取封面网址"); Debug.printLog(tag, "└" + bookInfoBean.getCoverUrl()); Debug.printLog(tag, "┌获取目录网址"); Debug.printLog(tag, "└" + bookInfoBean.getChapterUrl()); Debug.printLog(tag, "-详情页解析完成"); } else { StringBuilder result = new StringBuilder(); do { result.append(resM.group()); } while (resM.find()); getInfoOfRegex(result.toString(), regs, ++index, bookShelfBean, analyzer, bookSourceBean, tag); } } // 正则表达式解析规则数据的通用方法(暂未使用,技术储备型代码) public static void getInfoByRegex(String res, String[] regList, int regIndex, HashMap ruleMap, final List> ruleVals) throws Exception { Matcher resM = Pattern.compile(regList[regIndex]).matcher(res); // 判断规则是否有效 if (!resM.find()) { return; } // 判断索引规则是否为最后一个 if (regIndex + 1 == regList.length) { // 分离规则参数 List ruleName = new ArrayList<>(); List> ruleParams = new ArrayList<>(); // 创建规则参数容器 List> ruleTypes = new ArrayList<>(); // 创建规则类型容器 List hasVarParams = new ArrayList<>(); // 创建put&get标志容器 for (String key : ruleMap.keySet()) { String val = ruleMap.get(key); ruleName.add(key); hasVarParams.add(val.contains("@put") || val.contains("@get")); List ruleParam = new ArrayList<>(); List ruleType = new ArrayList<>(); splitRegexRule(val, ruleParam, ruleType); ruleParams.add(ruleParam); ruleTypes.add(ruleType); } // 提取规则结果 do { HashMap ruleVal = new HashMap<>(); StringBuilder infoVal = new StringBuilder(); for (int i = ruleParams.size(); i-- > 0; ) { List ruleParam = ruleParams.get(i); List ruleType = ruleTypes.get(i); infoVal.setLength(0); for (int j = ruleParam.size(); j-- > 0; ) { int regType = ruleType.get(j); if (regType > 0) { if (j == 0 && Objects.equals(ruleName.get(0), "ruleChapterName")) { infoVal.insert(0, resM.group(regType) == null ? "" : "\uD83D\uDD12"); } else { infoVal.insert(0, resM.group(regType)); } } else if (regType < 0) { if (j == 0 && Objects.equals(ruleName.get(0), "ruleChapterName")) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { infoVal.insert(0, resM.group(ruleParam.get(j)) == null ? "" : "\uD83D\uDD12"); } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { infoVal.insert(0, resM.group(ruleParam.get(j))); } } } else { infoVal.insert(0, ruleParam.get(j)); } } ruleVal.put(ruleName.get(i), infoVal.toString()); } ruleVals.add(ruleVal); } while (resM.find()); } else { StringBuilder result = new StringBuilder(); do { result.append(resM.group(0)); } while (resM.find()); getInfoByRegex(result.toString(), regList, ++regIndex, ruleMap, ruleVals); } } // 拆分正则表达式替换规则(如:$\d{1,2}或${name}) /*注意:千万别用正则表达式拆分字符串,效率太低了!*/ public static void splitRegexRule(String str, final List ruleParam, final List ruleType) throws Exception { if (TextUtils.isEmpty(str)) { ruleParam.add(""); ruleType.add(0); return; } int index = 0, start = 0, len = str.length(); while (index < len) { if (str.charAt(index) == '$') { if (str.charAt(index + 1) == '{') { if (index > start) { ruleParam.add(str.substring(start, index)); ruleType.add(0); start = index; } for (index += 2; index < len; index++) { if (str.charAt(index) == '}') { ruleParam.add(str.substring(start + 2, index)); ruleType.add(-1); start = ++index; break; } else if (str.charAt(index) == '$' || str.charAt(index) == '@') { break; } } } else if ((str.charAt(index + 1) >= '0') && (str.charAt(index + 1) <= '9')) { if (index > start) { ruleParam.add(str.substring(start, index)); ruleType.add(0); start = index; } if ((index + 2 < len) && (str.charAt(index + 2) >= '0') && (str.charAt(index + 2) <= '9')) { ruleParam.add(str.substring(start, index + 3)); ruleType.add(string2Int(ruleParam.get(ruleParam.size() - 1))); start = index += 3; } else { ruleParam.add(str.substring(start, index + 2)); ruleType.add(string2Int(ruleParam.get(ruleParam.size() - 1))); start = index += 2; } } else { ++index; } } else { ++index; } } if (index > start) { ruleParam.add(str.substring(start, index)); ruleType.add(0); } } // 存取字符串中的put&get参数 public static String checkKeys(String str, AnalyzeRule analyzer) throws Exception { if (str.contains("@put:{")) { Matcher putMatcher = Pattern.compile("@put:\\{([^,]*):([^\\}]*)\\}").matcher(str); while (putMatcher.find()) { str = str.replace(putMatcher.group(0), ""); analyzer.put(putMatcher.group(1), putMatcher.group(2)); } } if (str.contains("@get:{")) { Matcher getMatcher = Pattern.compile("@get:\\{([^\\}]*)\\}").matcher(str); while (getMatcher.find()) { str = str.replace(getMatcher.group(), analyzer.get(getMatcher.group(1))); } } return str; } // String数字转int数字的高效方法(利用ASCII值判断) public static int string2Int(String s) { int r = 0; char n; for (int i = 0, l = s.length(); i < l; i++) { n = s.charAt(i); if (n >= '0' && n <= '9') { r = r * 10 + (n - 0x30); //'0-9'的ASCII值为0x30-0x39 } } return r; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByXPath.java ================================================ package com.kunfei.bookshelf.model.analyzeRule; import android.text.TextUtils; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.seimicrawler.xpath.JXDocument; import org.seimicrawler.xpath.JXNode; import java.util.ArrayList; import java.util.List; public class AnalyzeByXPath { private JXDocument jxDocument; private JXNode jxNode; public AnalyzeByXPath parse(Object doc) { if (doc instanceof JXNode) { jxNode = (JXNode) doc; if (!jxNode.isElement()) { jxDocument = strToJXDocument(doc.toString()); jxNode = null; } } else if (doc instanceof Document) { jxDocument = JXDocument.create((Document) doc); jxNode = null; } else if (doc instanceof Element) { jxDocument = JXDocument.create(new Elements((Element) doc)); jxNode = null; } else if (doc instanceof Elements) { jxDocument = JXDocument.create((Elements) doc); jxNode = null; } else { jxDocument = strToJXDocument(doc.toString()); jxNode = null; } return this; } private JXDocument strToJXDocument(String html) { if (html.endsWith("")) { html = String.format("%s", html); } if (html.endsWith("") || html.endsWith("")) { html = String.format("%s
", html); } return JXDocument.create(html); } List getElements(String xPath) { if (TextUtils.isEmpty(xPath)) { return null; } List jxNodes = new ArrayList<>(); String elementsType; String[] rules; if (xPath.contains("&&")) { rules = xPath.split("&&"); elementsType = "&"; } else if (xPath.contains("%%")) { rules = xPath.split("%%"); elementsType = "%"; } else { rules = xPath.split("\\|\\|"); elementsType = "|"; } if (rules.length == 1) { if (jxNode != null) { return jxNode.sel(rules[0]); } return jxDocument.selN(rules[0]); } else { List> results = new ArrayList<>(); for (String rl : rules) { List temp = getElements(rl); if (temp != null && !temp.isEmpty()) { results.add(temp); if (temp.size() > 0 && elementsType.equals("|")) { break; } } } if (results.size() > 0) { if ("%".equals(elementsType)) { for (int i = 0; i < results.get(0).size(); i++) { for (List temp : results) { if (i < temp.size()) { jxNodes.add(temp.get(i)); } } } } else { for (List temp : results) { jxNodes.addAll(temp); } } } } return jxNodes; } List getStringList(String xPath) { List result = new ArrayList<>(); String elementsType; String[] rules; if (xPath.contains("&&")) { rules = xPath.split("&&"); elementsType = "&"; } else if (xPath.contains("%%")) { rules = xPath.split("%%"); elementsType = "%"; } else { rules = xPath.split("\\|\\|"); elementsType = "|"; } if (rules.length == 1) { List jxNodes; if (jxNode != null) { jxNodes = jxNode.sel(xPath); } else { jxNodes = jxDocument.selN(xPath); } for (JXNode jxNode : jxNodes) { /*if(jxNode.isString()){ result.add(String.valueOf(jxNode)); }*/ result.add(String.valueOf(jxNode)); } return result; } else { List> results = new ArrayList<>(); for (String rl : rules) { List temp = getStringList(rl); if (temp != null && !temp.isEmpty()) { results.add(temp); if (temp.size() > 0 && elementsType.equals("|")) { break; } } } if (results.size() > 0) { if ("%".equals(elementsType)) { for (int i = 0; i < results.get(0).size(); i++) { for (List temp : results) { if (i < temp.size()) { result.add(temp.get(i)); } } } } else { for (List temp : results) { result.addAll(temp); } } } } return result; } public String getString(String rule) { String[] rules; String elementsType; if (rule.contains("&&")) { rules = rule.split("&&"); elementsType = "&"; } else { rules = rule.split("\\|\\|"); elementsType = "|"; } if (rules.length == 1) { List jxNodes; if (jxNode != null) { jxNodes = jxNode.sel(rule); } else { jxNodes = jxDocument.selN(rule); } if (jxNodes == null) return null; return TextUtils.join(",", jxNodes); } else { List textS = new ArrayList<>(); for (String rl : rules) { String temp = getString(rl); if (!TextUtils.isEmpty(temp)) { textS.add(temp); if (elementsType.equals("|")) { break; } } } return TextUtils.join(",", textS).trim(); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeHeaders.java ================================================ package com.kunfei.bookshelf.model.analyzeRule; import static com.kunfei.bookshelf.constant.AppConstant.DEFAULT_USER_AGENT; import android.content.SharedPreferences; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import java.util.HashMap; import java.util.Map; /** * Created by GKF on 2018/3/2. * 解析Headers */ public class AnalyzeHeaders { private static SharedPreferences preferences = MApplication.getConfigPreferences(); public static Map getDefaultHeader() { Map headerMap = new HashMap<>(); headerMap.put("User-Agent", getDefaultUserAgent()); return headerMap; } public static String getDefaultUserAgent() { return preferences.getString(MApplication.getInstance().getString(R.string.pk_user_agent), DEFAULT_USER_AGENT); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeRule.java ================================================ package com.kunfei.bookshelf.model.analyzeRule; import static android.text.TextUtils.isEmpty; import static com.kunfei.bookshelf.constant.AppConstant.EXP_PATTERN; import static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN; import static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING; import static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE; import static com.kunfei.bookshelf.utils.NetworkUtils.headerPattern; import android.annotation.SuppressLint; import androidx.annotation.Keep; import com.google.gson.Gson; import com.kunfei.bookshelf.bean.BaseBookBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.help.JsExtensions; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.StringUtils; import org.jsoup.nodes.Entities; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.script.SimpleBindings; /** * Created by REFGD. * 统一解析接口 */ @Keep @SuppressWarnings({"unused", "WeakerAccess"}) public class AnalyzeRule implements JsExtensions { private static final Pattern putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE); private static final Pattern getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE); private final BookSourceBean bookSource; private BaseBookBean book; private Object object; private Boolean isJSON = false; private String baseUrl = null; private AnalyzeByXPath analyzeByXPath = null; private AnalyzeByJSoup analyzeByJSoup = null; private AnalyzeByJSonPath analyzeByJSonPath = null; private boolean objectChangedXP = false; private boolean objectChangedJS = false; private boolean objectChangedJP = false; public AnalyzeRule(BaseBookBean bookBean, BookSourceBean bookSourceBean) { book = bookBean; bookSource = bookSourceBean; } public void setBook(BaseBookBean book) { this.book = book; } public AnalyzeRule setContent(Object body) { return setContent(body, baseUrl); } public Object getContent() { return object; } public AnalyzeRule setContent(Object body, String baseUrl) { if (body == null) throw new AssertionError("Content cannot be null"); isJSON = StringUtils.isJsonType(String.valueOf(body)); object = body; if (baseUrl != null) { this.baseUrl = headerPattern.matcher(baseUrl).replaceAll(""); } objectChangedXP = true; objectChangedJS = true; objectChangedJP = true; return this; } public String getBaseUrl() { return this.baseUrl; } /** * 获取XPath解析类 */ private AnalyzeByXPath getAnalyzeByXPath(Object o) { if (o != object) { return new AnalyzeByXPath().parse(o); } return getAnalyzeByXPath(); } private AnalyzeByXPath getAnalyzeByXPath() { if (analyzeByXPath == null || objectChangedXP) { analyzeByXPath = new AnalyzeByXPath(); analyzeByXPath.parse(object); objectChangedXP = false; } return analyzeByXPath; } /** * 获取JSOUP解析类 */ private AnalyzeByJSoup getAnalyzeByJSoup(Object o) { if (o != object) { return new AnalyzeByJSoup().parse(o); } return getAnalyzeByJSoup(); } private AnalyzeByJSoup getAnalyzeByJSoup() { if (analyzeByJSoup == null || objectChangedJS) { analyzeByJSoup = new AnalyzeByJSoup(); analyzeByJSoup.parse(object); objectChangedJS = false; } return analyzeByJSoup; } /** * 获取JSON解析类 */ private AnalyzeByJSonPath getAnalyzeByJSonPath(Object o) { if (o != object) { return new AnalyzeByJSonPath().parse(o); } return getAnalyzeByJSonPath(); } private AnalyzeByJSonPath getAnalyzeByJSonPath() { if (analyzeByJSonPath == null || objectChangedJP) { analyzeByJSonPath = new AnalyzeByJSonPath(); analyzeByJSonPath.parse(object); objectChangedJP = false; } return analyzeByJSonPath; } /** * 获取文本列表 */ public List getStringList(String rule) throws Exception { return getStringList(rule, false); } public List getStringList(String rule, boolean isUrl) throws Exception { if (isEmpty(rule)) return null; List ruleList = splitSourceRule(rule); return getStringList(ruleList, isUrl); } @SuppressWarnings({"unchecked"}) public List getStringList(List ruleList, boolean isUrl) throws Exception { Object result = null; if (!ruleList.isEmpty()) result = object; for (SourceRule rule : ruleList) { if (!isEmpty(rule.rule)) { switch (rule.mode) { case Js: result = evalJS(rule.rule, result); break; case JSon: result = getAnalyzeByJSonPath(result).getStringList(rule.rule); break; case XPath: result = getAnalyzeByXPath(result).getStringList(rule.rule); break; default: result = getAnalyzeByJSoup(result).getStringList(rule.rule); } } if (!isEmpty(rule.replaceRegex) && result instanceof List) { List newList = new ArrayList<>(); //noinspection rawtypes for (Object item : (List) result) { newList.add(replaceRegex(String.valueOf(item), rule)); } result = newList; } else if (!isEmpty(rule.replaceRegex)) { result = replaceRegex(String.valueOf(result), rule); } } if (result == null) return new ArrayList<>(); if (result instanceof String) { result = Arrays.asList(StringUtils.formatHtml((String) result).split("\n")); } if (isUrl && !isEmpty(baseUrl)) { List urlList = new ArrayList<>(); //noinspection rawtypes for (Object url : (List) result) { String absoluteURL = NetworkUtils.getAbsoluteURL(baseUrl, String.valueOf(url)); if (!urlList.contains(absoluteURL) && !isEmpty(absoluteURL)) { urlList.add(absoluteURL); } } return urlList; } return (List) result; } /** * 获取文本 */ public String getString(String rule) throws Exception { return getString(rule, false); } public String getString(String ruleStr, boolean isUrl) throws Exception { if (isEmpty(ruleStr)) return null; List ruleList = splitSourceRule(ruleStr); return getString(ruleList, isUrl); } public String getString(List ruleList) throws Exception { return getString(ruleList, false); } public String getString(List ruleList, boolean isUrl) throws Exception { Object result = null; if (!ruleList.isEmpty()) result = object; for (SourceRule rule : ruleList) { if (!StringUtils.isTrimEmpty(rule.rule)) { switch (rule.mode) { case Js: result = evalJS(rule.rule, result); break; case JSon: result = getAnalyzeByJSonPath(result).getString(rule.rule); break; case XPath: result = getAnalyzeByXPath(result).getString(rule.rule); break; case Default: if (isUrl && !isEmpty(baseUrl)) { result = getAnalyzeByJSoup(result).getString0(rule.rule); } else { result = getAnalyzeByJSoup(result).getString(rule.rule); } } } if (!isEmpty(rule.replaceRegex)) { result = replaceRegex(String.valueOf(result), rule); } } if (result == null) return ""; if (isUrl && !StringUtils.isTrimEmpty(baseUrl)) { return NetworkUtils.getAbsoluteURL(baseUrl, Entities.unescape(String.valueOf(result))); } try { return Entities.unescape(String.valueOf(result)); } catch (Exception e) { return String.valueOf(result); } } /** * 获取Element */ public Object getElement(String ruleStr) throws Exception { List ruleList = splitSourceRule(ruleStr); Object result = object; for (SourceRule rule : ruleList) { switch (rule.mode) { case Js: result = evalJS(rule.rule, result); break; case JSon: result = getAnalyzeByJSonPath(result).getObject(rule.rule); break; case XPath: result = getAnalyzeByXPath(result).getElements(rule.rule); break; default: result = getAnalyzeByJSoup(result).getElements(rule.rule); } if (!isEmpty(rule.replaceRegex) && result instanceof String) { result = replaceRegex(String.valueOf(result), rule); } } return result; } /** * 获取列表 */ @SuppressWarnings("unchecked") public List getElements(String ruleStr) throws Exception { List ruleList = splitSourceRule(ruleStr); Object result = null; if (!ruleList.isEmpty()) result = object; for (SourceRule rule : ruleList) { switch (rule.mode) { case Js: result = evalJS(rule.rule, result); break; case JSon: result = getAnalyzeByJSonPath(result).getList(rule.rule); break; case XPath: result = getAnalyzeByXPath(result).getElements(rule.rule); break; default: result = getAnalyzeByJSoup(result).getElements(rule.rule); } if (!isEmpty(rule.replaceRegex) && result instanceof String) { result = replaceRegex(String.valueOf(result), rule); } } if (result == null) { return new ArrayList<>(); } return (List) result; } /** * 保存变量 */ private void putRule(Map map) throws Exception { for (Map.Entry entry : map.entrySet()) { if (book != null) { book.putVariable(entry.getKey(), getString(entry.getValue())); } } } /** * 分离并执行put规则 */ private String splitPutRule(String ruleStr) throws Exception { Matcher putMatcher = putPattern.matcher(ruleStr); while (putMatcher.find()) { ruleStr = ruleStr.replace(putMatcher.group(), ""); Map map = new Gson().fromJson(putMatcher.group(1), MAP_STRING); putRule(map); } return ruleStr; } /** * 替换@get */ public String replaceGet(String ruleStr) { Matcher getMatcher = getPattern.matcher(ruleStr); while (getMatcher.find()) { String value = ""; if (book != null && book.getVariableMap() != null) { value = book.getVariableMap().get(getMatcher.group(1)); if (value == null) value = ""; } ruleStr = ruleStr.replace(getMatcher.group(), value); } return ruleStr; } /** * 正则替换 */ private String replaceRegex(String result, SourceRule rule) { if (!isEmpty(rule.replaceRegex)) { if (rule.replaceFirst) { Pattern pattern = Pattern.compile(rule.replaceRegex); Matcher matcher = pattern.matcher(String.valueOf(result)); if (matcher.find()) { result = matcher.group(0).replaceFirst(rule.replaceRegex, rule.replacement); } else { result = ""; } } else { result = String.valueOf(result).replaceAll(rule.replaceRegex, rule.replacement); } } return result; } /** * 替换JS */ @SuppressLint("DefaultLocale") private String replaceJs(String ruleStr) throws Exception { if (ruleStr.contains("{{") && ruleStr.contains("}}")) { Object jsEval; StringBuffer sb = new StringBuffer(ruleStr.length()); Matcher expMatcher = EXP_PATTERN.matcher(ruleStr); while (expMatcher.find()) { jsEval = evalJS(expMatcher.group(1), object); if (jsEval instanceof String) { expMatcher.appendReplacement(sb, (String) jsEval); } else if (jsEval instanceof Double && ((Double) jsEval) % 1.0 == 0) { expMatcher.appendReplacement(sb, String.format("%.0f", (Double) jsEval)); } else { expMatcher.appendReplacement(sb, String.valueOf(jsEval)); } } expMatcher.appendTail(sb); ruleStr = sb.toString(); } return ruleStr; } /** * 分解规则生成规则列表 */ public List splitSourceRule(String ruleStr) throws Exception { List ruleList = new ArrayList<>(); if (isEmpty(ruleStr)) return ruleList; //检测Mode Mode mode; if (StringUtils.startWithIgnoreCase(ruleStr, "@XPath:")) { mode = Mode.XPath; ruleStr = ruleStr.substring(7); } else if (StringUtils.startWithIgnoreCase(ruleStr, "@JSon:")) { mode = Mode.JSon; ruleStr = ruleStr.substring(6); } else { if (isJSON) { mode = Mode.JSon; } else { mode = Mode.Default; } } //分离put规则 ruleStr = splitPutRule(ruleStr); //替换get值 ruleStr = replaceGet(ruleStr); //替换js ruleStr = replaceJs(ruleStr); //拆分为列表 int start = 0; String tmp; Matcher jsMatcher = JS_PATTERN.matcher(ruleStr); while (jsMatcher.find()) { if (jsMatcher.start() > start) { tmp = ruleStr.substring(start, jsMatcher.start()).replaceAll("\n", "").trim(); if (!isEmpty(tmp)) { ruleList.add(new SourceRule(tmp, mode)); } } ruleList.add(new SourceRule(jsMatcher.group(), Mode.Js)); start = jsMatcher.end(); } if (ruleStr.length() > start) { tmp = ruleStr.substring(start).replaceAll("\n", "").trim(); if (!isEmpty(tmp)) { ruleList.add(new SourceRule(tmp, mode)); } } return ruleList; } /** * 规则类 */ public static class SourceRule { Mode mode; String rule; String replaceRegex = ""; String replacement = ""; boolean replaceFirst = false; SourceRule(String ruleStr, Mode mainMode) { this.mode = mainMode; if (mode == Mode.Js) { if (ruleStr.startsWith("")) { rule = ruleStr.substring(4, ruleStr.lastIndexOf("<")); } else { rule = ruleStr.substring(4); } } else { if (StringUtils.startWithIgnoreCase(ruleStr, "@XPath:")) { mode = Mode.XPath; rule = ruleStr.substring(7); } else if (StringUtils.startWithIgnoreCase(ruleStr, "//")) {//XPath特征很明显,无需配置单独的识别标头 mode = Mode.XPath; rule = ruleStr; } else if (StringUtils.startWithIgnoreCase(ruleStr, "@JSon:")) { mode = Mode.JSon; rule = ruleStr.substring(6); } else if (ruleStr.startsWith("$.")) { mode = Mode.JSon; rule = ruleStr; } else { rule = ruleStr; } //分离正则表达式 String[] ruleStrS = rule.trim().split("##"); rule = ruleStrS[0]; if (ruleStrS.length > 1) { replaceRegex = ruleStrS[1]; } if (ruleStrS.length > 2) { replacement = ruleStrS[2]; } if (ruleStrS.length > 3) { replaceFirst = true; } } } } private enum Mode { XPath, JSon, Default, Js } public String put(String key, String value) { if (book != null) { book.putVariable(key, value); } return value; } public String get(String key) { if (book == null) { return null; } if (book.getVariableMap() == null) { return null; } return book.getVariableMap().get(key); } /** * 执行JS */ public Object evalJS(String jsStr, Object result) throws Exception { SimpleBindings bindings = new SimpleBindings(); bindings.put("java", this); bindings.put("source", bookSource); bindings.put("result", result); bindings.put("baseUrl", baseUrl); return SCRIPT_ENGINE.eval(jsStr, bindings); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeUrl.java ================================================ package com.kunfei.bookshelf.model.analyzeRule; import static com.kunfei.bookshelf.constant.AppConstant.EXP_PATTERN; import static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN; import static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING; import static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE; import static com.kunfei.bookshelf.utils.NetworkUtils.headerPattern; import android.annotation.SuppressLint; import android.text.TextUtils; import androidx.annotation.Keep; import com.google.gson.Gson; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.help.JsExtensions; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.UrlEncoderUtils; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.script.SimpleBindings; import okhttp3.MediaType; import okhttp3.RequestBody; /** * Created by GKF on 2018/1/24. * 搜索URL规则解析 */ @Keep public class AnalyzeUrl implements JsExtensions { private static final Pattern pagePattern = Pattern.compile("(? queryMap = new LinkedHashMap<>(); private final Map headerMap = new HashMap<>(); private String charCode = null; private UrlMode urlMode = UrlMode.DEFAULT; private String jsonBody = null; private final String searchKey; private final int searchPage; public AnalyzeUrl(String urlRule, Map headerMap) throws Exception { this(urlRule, null, headerMap); } public AnalyzeUrl(String urlRule, String baseUrl, Map headerMap) throws Exception { this(urlRule, baseUrl, null, headerMap); } public AnalyzeUrl(String urlRule, String baseUrl, BookSourceBean bookSource, Map headerMap) throws Exception { this(urlRule, baseUrl, bookSource, null, 1, headerMap); } @SuppressLint("DefaultLocale") public AnalyzeUrl(String urlRule, String baseUrl, BookSourceBean bookSource, final String key, final int page, Map headerMap) throws Exception { if (!TextUtils.isEmpty(baseUrl)) { this.baseUrl = headerPattern.matcher(baseUrl).replaceAll(""); } this.bookSource = bookSource; this.searchKey = key; this.searchPage = page; ruleUrl = urlRule; //替换关键字 if (!StringUtils.isTrimEmpty(key)) { // 处理searchKey=searchKey的情况 if (ruleUrl.matches("=[\\s{(]*searchKey")) ruleUrl = ruleUrl.replaceFirst("=[\\s{(]*searchKey", "=" + key); else ruleUrl = ruleUrl.replace("searchKey", key); } //判断是否有下一页 if (page > 1 && !ruleUrl.contains("searchPage")) throw new Exception("没有下一页"); //替换js ruleUrl = replaceJs(ruleUrl); //解析Header ruleUrl = analyzeHeader(ruleUrl, headerMap); //分离编码规则 ruleUrl = splitCharCode(ruleUrl); //设置页数 ruleUrl = analyzePage(ruleUrl, page); //执行规则列表 List ruleList = splitRule(ruleUrl); for (String rule : ruleList) { if (rule.startsWith("")) { rule = rule.substring(4, rule.lastIndexOf("<")); ruleUrl = (String) evalJS(rule, ruleUrl); } else if (rule.startsWith("@js:")) { rule = rule.substring(4); ruleUrl = (String) evalJS(rule, ruleUrl); } else { ruleUrl = rule.replace("@result", ruleUrl); } } //分离post参数 String[] ruleUrlS = ruleUrl.split("@"); if (ruleUrlS.length > 1) { urlMode = UrlMode.POST; } else { //分离get参数 ruleUrlS = ruleUrlS[0].split("\\?"); if (ruleUrlS.length > 1) { urlMode = UrlMode.GET; } } generateUrlPath(ruleUrlS[0]); if (urlMode == UrlMode.GET) { analyzeQuery(ruleUrlS[1]); } else if (urlMode == UrlMode.POST) { if (StringUtils.isJsonType(ruleUrlS[1])) { jsonBody = ruleUrlS[1]; } else { analyzeQuery(ruleUrlS[1]); } } } /** * 解析Header */ private String analyzeHeader(String ruleUrl, Map headerMapF) { if (headerMapF != null) { headerMap.putAll(headerMapF); } Matcher matcher = headerPattern.matcher(ruleUrl); if (matcher.find()) { String find = matcher.group(0); ruleUrl = ruleUrl.replace(find, ""); find = find.substring(8); try { Map map = new Gson().fromJson(find, MAP_STRING); headerMap.putAll(map); } catch (Exception ignored) { } } return ruleUrl; } /** * 分离编码规则 */ private String splitCharCode(String rule) { String[] ruleUrlS = rule.split("\\|"); if (ruleUrlS.length > 1) { if (!TextUtils.isEmpty(ruleUrlS[1])) { String[] qtS = ruleUrlS[1].split("&"); for (String qt : qtS) { String[] gz = qt.split("="); if (gz[0].equals("char")) { charCode = gz[1]; } } } } return ruleUrlS[0]; } /** * 解析页数 */ private String analyzePage(String ruleUrl, final Integer searchPage) { if (searchPage == null) return ruleUrl; Matcher matcher = pagePattern.matcher(ruleUrl); while (matcher.find()) { String[] pages = matcher.group(1).split(","); if (searchPage <= pages.length) { ruleUrl = ruleUrl.replace(matcher.group(), pages[searchPage - 1].trim()); } else { ruleUrl = ruleUrl.replace(matcher.group(), pages[pages.length - 1].trim()); } } return ruleUrl.replace("searchPage-1", String.valueOf(searchPage - 1)) .replace("searchPage+1", String.valueOf(searchPage + 1)) .replace("searchPage", String.valueOf(searchPage)); } /** * 替换js */ @SuppressLint("DefaultLocale") private String replaceJs(String ruleUrl) throws Exception { if (ruleUrl.contains("{{") && ruleUrl.contains("}}")) { Object jsEval; StringBuffer sb = new StringBuffer(ruleUrl.length()); Matcher expMatcher = EXP_PATTERN.matcher(ruleUrl); while (expMatcher.find()) { jsEval = evalJS(expMatcher.group(1), ruleUrl); if (jsEval instanceof String) { expMatcher.appendReplacement(sb, (String) jsEval); } else if (jsEval instanceof Double && ((Double) jsEval) % 1.0 == 0) { expMatcher.appendReplacement(sb, String.format("%.0f", (Double) jsEval)); } else { expMatcher.appendReplacement(sb, String.valueOf(jsEval)); } } expMatcher.appendTail(sb); ruleUrl = sb.toString(); } return ruleUrl; } /** * 解析QueryMap */ private void analyzeQuery(String allQuery) throws Exception { queryStr = allQuery; String[] queryS = allQuery.split("&"); for (String query : queryS) { String[] queryM = query.split("="); String value = queryM.length > 1 ? queryM[1] : ""; if (TextUtils.isEmpty(charCode)) { if (UrlEncoderUtils.hasUrlEncoded(value)) { queryMap.put(queryM[0], value); } else { queryMap.put(queryM[0], URLEncoder.encode(value, "UTF-8")); } } else if (charCode.equals("escape")) { queryMap.put(queryM[0], StringUtils.escape(value)); } else { queryMap.put(queryM[0], URLEncoder.encode(value, charCode)); } } } /** * 拆分规则 */ private List splitRule(String ruleStr) { List ruleList = new ArrayList<>(); Matcher jsMatcher = JS_PATTERN.matcher(ruleStr); int start = 0; String tmp; while (jsMatcher.find()) { if (jsMatcher.start() > start) { tmp = ruleStr.substring(start, jsMatcher.start()).replaceAll("\n", "").trim(); if (!TextUtils.isEmpty(tmp)) { ruleList.add(tmp); } } ruleList.add(jsMatcher.group()); start = jsMatcher.end(); } if (ruleStr.length() > start) { tmp = ruleStr.substring(start).replaceAll("\n", "").trim(); if (!TextUtils.isEmpty(tmp)) { ruleList.add(tmp); } } return ruleList; } /** * 分解URL */ private void generateUrlPath(String ruleUrl) { url = NetworkUtils.getAbsoluteURL(baseUrl, ruleUrl); host = StringUtils.getBaseUrl(url); urlPath = url.substring(host.length()); } /** * 执行JS */ private Object evalJS(String jsStr, Object result) throws Exception { SimpleBindings bindings = new SimpleBindings(); bindings.put("java", this); bindings.put("baseUrl", baseUrl); bindings.put("searchPage", searchPage); bindings.put("searchKey", searchKey); bindings.put("source", bookSource); bindings.put("result", result); return SCRIPT_ENGINE.eval(jsStr, bindings); } public String getCharCode() { return charCode; } public String getHost() { return host; } public String getPath() { return urlPath; } public String getRuleUrl() { return ruleUrl; } public String getUrl() { return url; } public Map getQueryMap() { return queryMap; } public Map getHeaderMap() { return headerMap; } public String getQueryStr() { return queryStr; } public String getJsonBody() { return jsonBody; } public byte[] getPostData() { StringBuilder builder = new StringBuilder(); Set keys = queryMap.keySet(); for (String key : keys) { builder.append(String.format("%s=%s&", key, queryMap.get(key))); } builder.deleteCharAt(builder.lastIndexOf("&")); return builder.toString().getBytes(); } public RequestBody getPostBody() { MediaType mediaType = MediaType.parse("application/json; charset=UTF-8"); return RequestBody.create(mediaType, jsonBody); } public UrlMode getUrlMode() { return urlMode; } public enum UrlMode { GET, POST, DEFAULT } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/content/BookChapterList.java ================================================ package com.kunfei.bookshelf.model.content; import android.os.Build; import android.text.TextUtils; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.WebChapterBean; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeByRegex; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl; import com.kunfei.bookshelf.model.task.AnalyzeNextUrlTask; import org.jsoup.nodes.Element; import org.mozilla.javascript.NativeObject; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import io.reactivex.Emitter; import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import retrofit2.Response; public class BookChapterList { private String tag; private BookSourceBean bookSourceBean; private List webChapterBeans; private boolean dx = false; private boolean analyzeNextUrl; private CompositeDisposable compositeDisposable; private String chapterListUrl; BookChapterList(String tag, BookSourceBean bookSourceBean, boolean analyzeNextUrl) { this.tag = tag; this.bookSourceBean = bookSourceBean; this.analyzeNextUrl = analyzeNextUrl; } public Observable> analyzeChapterList(final String s, final BookShelfBean bookShelfBean, Map headerMap) { return Observable.create(e -> { if (TextUtils.isEmpty(s)) { e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_chapter_list_error) + bookShelfBean.getBookInfoBean().getChapterUrl())); return; } else { Debug.printLog(tag, 1, "┌成功获取目录页", analyzeNextUrl); Debug.printLog(tag, 1, "└" + bookShelfBean.getBookInfoBean().getChapterUrl(), analyzeNextUrl); } bookShelfBean.setTag(tag); AnalyzeRule analyzer = new AnalyzeRule(bookShelfBean, bookSourceBean); String ruleChapterList = bookSourceBean.getRuleChapterList(); if (ruleChapterList != null && ruleChapterList.startsWith("-")) { dx = true; ruleChapterList = ruleChapterList.substring(1); } chapterListUrl = bookShelfBean.getBookInfoBean().getChapterUrl(); WebChapterBean webChapterBean = analyzeChapterList(s, chapterListUrl, ruleChapterList, analyzeNextUrl, analyzer, dx); final List chapterList = webChapterBean.getData(); final List chapterUrlS = new ArrayList<>(webChapterBean.getNextUrlList()); if (chapterUrlS.isEmpty() || !analyzeNextUrl) { finish(chapterList, e); } //下一页为单页 else if (chapterUrlS.size() == 1) { List usedUrl = new ArrayList<>(); usedUrl.add(bookShelfBean.getBookInfoBean().getChapterUrl()); //循环获取直到下一页为空 Debug.printLog(tag, "正在加载下一页"); while (!chapterUrlS.isEmpty() && !usedUrl.contains(chapterUrlS.get(0))) { usedUrl.add(chapterUrlS.get(0)); AnalyzeUrl analyzeUrl = new AnalyzeUrl( chapterUrlS.get(0), tag, bookSourceBean, bookSourceBean.getHeaderMap(true) ); try { String body; Response response = BaseModelImpl.getInstance().getResponseO(analyzeUrl) .blockingFirst(); body = response.body(); webChapterBean = analyzeChapterList(body, chapterUrlS.get(0), ruleChapterList, false, analyzer, dx); chapterList.addAll(webChapterBean.getData()); chapterUrlS.clear(); chapterUrlS.addAll(webChapterBean.getNextUrlList()); } catch (Exception exception) { if (!e.isDisposed()) { e.onError(exception); } } } Debug.printLog(tag, "下一页加载完成共" + usedUrl.size() + "页"); finish(chapterList, e); } //下一页为多页 else { Debug.printLog(tag, "正在加载其它" + chapterUrlS.size() + "页"); compositeDisposable = new CompositeDisposable(); webChapterBeans = new ArrayList<>(); AnalyzeNextUrlTask.Callback callback = new AnalyzeNextUrlTask.Callback() { @Override public void addDisposable(Disposable disposable) { compositeDisposable.add(disposable); } @Override public void analyzeFinish(WebChapterBean bean, List chapterListBeans) { if (nextUrlFinish(bean, chapterListBeans)) { for (WebChapterBean chapterBean : webChapterBeans) { chapterList.addAll(chapterBean.getData()); } Debug.printLog(tag, "其它页加载完成,目录共" + chapterList.size() + "条"); finish(chapterList, e); } } @Override public void onError(Throwable throwable) { compositeDisposable.dispose(); e.onError(throwable); } }; for (String url : chapterUrlS) { final WebChapterBean bean = new WebChapterBean(url); webChapterBeans.add(bean); } for (WebChapterBean bean : webChapterBeans) { BookChapterList bookChapterList = new BookChapterList(tag, bookSourceBean, false); AnalyzeUrl analyzeUrl = new AnalyzeUrl( bean.getUrl(), tag, bookSourceBean, bookSourceBean.getHeaderMap(true) ); new AnalyzeNextUrlTask(bookChapterList, bean, bookShelfBean, headerMap) .setCallback(callback) .analyzeUrl(analyzeUrl); } } }); } private synchronized boolean nextUrlFinish(WebChapterBean webChapterBean, List bookChapterBeans) { webChapterBean.setData(bookChapterBeans); for (WebChapterBean bean : webChapterBeans) { if (bean.noData()) return false; } return true; } private void finish(List chapterList, Emitter> emitter) { //去除重复,保留后面的,先倒序,从后面往前判断 if (!dx) { Collections.reverse(chapterList); } LinkedHashSet lh = new LinkedHashSet<>(chapterList); chapterList = new ArrayList<>(lh); Collections.reverse(chapterList); Debug.printLog(tag, 1, "-目录解析完成", analyzeNextUrl); emitter.onNext(chapterList); emitter.onComplete(); } private WebChapterBean analyzeChapterList(String s, String chapterUrl, String ruleChapterList, boolean printLog, AnalyzeRule analyzer, boolean dx) throws Exception { List nextUrlList = new ArrayList<>(); analyzer.setContent(s, chapterUrl); if (!TextUtils.isEmpty(bookSourceBean.getRuleChapterUrlNext()) && analyzeNextUrl) { Debug.printLog(tag, 1, "┌获取目录下一页网址", printLog); nextUrlList = analyzer.getStringList(bookSourceBean.getRuleChapterUrlNext(), true); int thisUrlIndex = nextUrlList.indexOf(chapterUrl); if (thisUrlIndex != -1) { nextUrlList.remove(thisUrlIndex); } Debug.printLog(tag, 1, "└" + nextUrlList.toString(), printLog); } List chapterBeans = new ArrayList<>(); Debug.printLog(tag, 1, "┌解析目录列表", printLog); // 仅使用java正则表达式提取目录列表 if (ruleChapterList.startsWith(":")) { ruleChapterList = ruleChapterList.substring(1); regexChapter(s, ruleChapterList.split("&&"), 0, analyzer, chapterBeans); if (chapterBeans.size() == 0) { Debug.printLog(tag, 1, "└找到 0 个章节", printLog); return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList)); } } // 使用AllInOne规则模式提取目录列表 else if (ruleChapterList.startsWith("+")) { ruleChapterList = ruleChapterList.substring(1); List collections = analyzer.getElements(ruleChapterList); if (collections.size() == 0) { Debug.printLog(tag, 1, "└找到 0 个章节", printLog); return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList)); } String nameRule = bookSourceBean.getRuleChapterName(); String linkRule = bookSourceBean.getRuleContentUrl(); String vipRule = bookSourceBean.getRuleChapterVip(); String payRule = bookSourceBean.getRuleChapterPay(); String name = ""; String link = ""; boolean isVip = false; boolean isPay = false; String vipResult = ""; String payResult = ""; for (Object object : collections) { if (object instanceof NativeObject) { name = String.valueOf(((NativeObject) object).get(nameRule)); link = String.valueOf(((NativeObject) object).get(linkRule)); vipResult = String.valueOf(((NativeObject) object).get(vipRule)); payResult = String.valueOf(((NativeObject) object).get(payRule)); } else if (object instanceof Element) { name = ((Element) object).text(); link = ((Element) object).attr(linkRule); } if (!TextUtils.isEmpty(vipResult) && !vipResult.matches("\\s*(?i)(null|false|0)\\s*")) { isVip = true; } if (!TextUtils.isEmpty(payResult) && !payResult.matches("\\s*(?i)(null|false|0)\\s*")) { isPay = true; } addChapter(chapterBeans, name, link, isVip, isPay); } } // 使用默认规则解析流程提取目录列表 else { List collections = analyzer.getElements(ruleChapterList); if (collections.size() == 0) { Debug.printLog(tag, 1, "└找到 0 个章节", printLog); return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList)); } List nameRule = analyzer.splitSourceRule(bookSourceBean.getRuleChapterName()); List linkRule = analyzer.splitSourceRule(bookSourceBean.getRuleContentUrl()); List vipRule = analyzer.splitSourceRule(bookSourceBean.getRuleChapterVip()); List payRule = analyzer.splitSourceRule(bookSourceBean.getRuleChapterPay()); for (Object object : collections) { analyzer.setContent(object, chapterUrl); String name = analyzer.getString(nameRule); String url = analyzer.getString(linkRule); boolean isVip = false; boolean isPay = false; String vipResult = analyzer.getString(vipRule); String payResult = analyzer.getString(payRule); if (!TextUtils.isEmpty(vipResult) && !vipResult.matches("\\s*(?i)(null|false|0)\\s*")) { isVip = true; } if (!TextUtils.isEmpty(payResult) && !payResult.matches("\\s*(?i)(null|false|0)\\s*")) { isPay = true; } addChapter(chapterBeans, name, url, isVip, isPay); } } Debug.printLog(tag, 1, "└找到 " + chapterBeans.size() + " 个章节", printLog); BookChapterBean firstChapter; if (dx) { Debug.printLog(tag, 1, "-倒序", printLog); firstChapter = chapterBeans.get(chapterBeans.size() - 1); } else { firstChapter = chapterBeans.get(0); } Debug.printLog(tag, 1, "┌获取章节名称", printLog); Debug.printLog(tag, 1, "└" + firstChapter.getDurChapterName(), printLog); Debug.printLog(tag, 1, "┌获取章节网址", printLog); Debug.printLog(tag, 1, "└" + firstChapter.getDurChapterUrl(), printLog); return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList)); } private void addChapter(final List chapterBeans, String name, String link, boolean isVip, boolean isPay ) { if (TextUtils.isEmpty(name)) return; if (TextUtils.isEmpty(link)) link = chapterListUrl; chapterBeans.add(new BookChapterBean(tag, name, link, isVip, isPay)); } // region 纯java模式正则表达式获取目录列表 private void regexChapter(String str, String[] regex, int index, AnalyzeRule analyzer, final List chapterBeans) throws Exception { Matcher resM = Pattern.compile(regex[index]).matcher(str); if (!resM.find()) { return; } if (index + 1 == regex.length) { // 获取解析规则 String nameRule = bookSourceBean.getRuleChapterName(); String linkRule = bookSourceBean.getRuleContentUrl(); if (TextUtils.isEmpty(nameRule) || TextUtils.isEmpty(linkRule)) return; // 替换@get规则 nameRule = analyzer.replaceGet(bookSourceBean.getRuleChapterName()); linkRule = analyzer.replaceGet(bookSourceBean.getRuleContentUrl()); // 分离规则参数 List nameParams = new ArrayList<>(); List nameGroups = new ArrayList<>(); AnalyzeByRegex.splitRegexRule(nameRule, nameParams, nameGroups); List linkParams = new ArrayList<>(); List linkGroups = new ArrayList<>(); AnalyzeByRegex.splitRegexRule(linkRule, linkParams, linkGroups); // 是否包含VIP规则(hasVipRule>1 时视为包含vip规则) int hasVipRule = 0; for (int i = nameGroups.size(); i-- > 0; ) { if (nameGroups.get(i) != 0) { ++hasVipRule; } } String vipNameGroup = ""; int vipNumGroup = 0; if ((nameGroups.get(0) != 0) && (hasVipRule > 1)) { vipNumGroup = nameGroups.remove(0); vipNameGroup = nameParams.remove(0); } // 创建结果缓存 StringBuilder cName = new StringBuilder(); StringBuilder cLink = new StringBuilder(); // 提取书籍目录 if (vipNumGroup != 0) { do { cName.setLength(0); cLink.setLength(0); for (int i = nameParams.size(); i-- > 0; ) { if (nameGroups.get(i) > 0) { cName.insert(0, resM.group(nameGroups.get(i))); } else if (nameGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { cName.insert(0, resM.group(nameParams.get(i))); } else { cName.insert(0, nameParams.get(i)); } } if (vipNumGroup > 0) { cName.insert(0, resM.group(vipNumGroup) == null ? "" : "\uD83D\uDD12"); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { cName.insert(0, resM.group(vipNameGroup) == null ? "" : "\uD83D\uDD12"); } else { cName.insert(0, vipNameGroup); } for (int i = linkParams.size(); i-- > 0; ) { if (linkGroups.get(i) > 0) { cLink.insert(0, resM.group(linkGroups.get(i))); } else if (linkGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { cLink.insert(0, resM.group(linkParams.get(i))); } else { cLink.insert(0, linkParams.get(i)); } } addChapter(chapterBeans, cName.toString(), cLink.toString(), false, false); } while (resM.find()); } else { do { cName.setLength(0); cLink.setLength(0); for (int i = nameParams.size(); i-- > 0; ) { if (nameGroups.get(i) > 0) { cName.insert(0, resM.group(nameGroups.get(i))); } else if (nameGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { cName.insert(0, resM.group(nameParams.get(i))); } else { cName.insert(0, nameParams.get(i)); } } for (int i = linkParams.size(); i-- > 0; ) { if (linkGroups.get(i) > 0) { cLink.insert(0, resM.group(linkGroups.get(i))); } else if (linkGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { cLink.insert(0, resM.group(linkParams.get(i))); } else { cLink.insert(0, linkParams.get(i)); } } addChapter(chapterBeans, cName.toString(), cLink.toString(), false, false); } while (resM.find()); } } else { StringBuilder result = new StringBuilder(); do { result.append(resM.group(0)); } while (resM.find()); regexChapter(result.toString(), regex, ++index, analyzer, chapterBeans); } } // endregion } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/content/BookContent.java ================================================ package com.kunfei.bookshelf.model.content; import static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN; import android.text.TextUtils; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.bean.BaseChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.dao.BookChapterBeanDao; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import io.reactivex.Observable; import retrofit2.Response; class BookContent { private String tag; private BookSourceBean bookSourceBean; private String ruleBookContent; private String baseUrl; BookContent(String tag, BookSourceBean bookSourceBean) { this.tag = tag; this.bookSourceBean = bookSourceBean; ruleBookContent = bookSourceBean.getRuleBookContent(); if (ruleBookContent.startsWith("$") && !ruleBookContent.startsWith("$.")) { ruleBookContent = ruleBookContent.substring(1); Matcher jsMatcher = JS_PATTERN.matcher(ruleBookContent); if (jsMatcher.find()) { ruleBookContent = ruleBookContent.replace(jsMatcher.group(), ""); } } } Observable analyzeBookContent(final Response response, final BaseChapterBean chapterBean, final BaseChapterBean nextChapterBean, BookShelfBean bookShelfBean, Map headerMap) { baseUrl = NetworkUtils.getUrl(response); return analyzeBookContent(response.body(), chapterBean, nextChapterBean, bookShelfBean, headerMap); } Observable analyzeBookContent(final String s, final BaseChapterBean chapterBean, final BaseChapterBean nextChapterBean, BookShelfBean bookShelfBean, Map headerMap) { return Observable.create(e -> { if (TextUtils.isEmpty(s)) { e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_content_error) + chapterBean.getDurChapterUrl())); return; } if (TextUtils.isEmpty(baseUrl)) { baseUrl = NetworkUtils.getAbsoluteURL(bookShelfBean.getBookInfoBean().getChapterUrl(), chapterBean.getDurChapterUrl()); } if (StringUtils.isJsonType(s) && !MApplication.getInstance().getDonateHb()) { e.onError(new VipThrowable()); e.onComplete(); return; } Debug.printLog(tag, "┌成功获取正文页"); Debug.printLog(tag, "└" + baseUrl); BookContentBean bookContentBean = new BookContentBean(); bookContentBean.setDurChapterIndex(chapterBean.getDurChapterIndex()); bookContentBean.setDurChapterUrl(chapterBean.getDurChapterUrl()); bookContentBean.setTag(tag); AnalyzeRule analyzer = new AnalyzeRule(bookShelfBean, bookSourceBean); WebContentBean webContentBean = analyzeBookContent(analyzer, s, chapterBean.getDurChapterUrl(), baseUrl); bookContentBean.setDurChapterContent(webContentBean.content); /* * 处理分页 */ if (!TextUtils.isEmpty(webContentBean.nextUrl)) { List usedUrlList = new ArrayList<>(); usedUrlList.add(chapterBean.getDurChapterUrl()); BaseChapterBean nextChapter; if (nextChapterBean != null) { nextChapter = nextChapterBean; } else { nextChapter = DbHelper.getDaoSession().getBookChapterBeanDao().queryBuilder() .where(BookChapterBeanDao.Properties.NoteUrl.eq(chapterBean.getNoteUrl()), BookChapterBeanDao.Properties.DurChapterIndex.eq(chapterBean.getDurChapterIndex() + 1)) .build().unique(); } while (!TextUtils.isEmpty(webContentBean.nextUrl) && !usedUrlList.contains(webContentBean.nextUrl)) { usedUrlList.add(webContentBean.nextUrl); if (nextChapter != null && NetworkUtils.getAbsoluteURL( baseUrl, webContentBean.nextUrl ).equals(NetworkUtils.getAbsoluteURL(baseUrl, nextChapter.getDurChapterUrl())) ) { break; } AnalyzeUrl analyzeUrl = new AnalyzeUrl( webContentBean.nextUrl, tag, bookSourceBean, bookSourceBean.getHeaderMap(true) ); try { String body; Response response = BaseModelImpl.getInstance().getResponseO(analyzeUrl).blockingFirst(); body = response.body(); webContentBean = analyzeBookContent(analyzer, body, webContentBean.nextUrl, baseUrl); if (!TextUtils.isEmpty(webContentBean.content)) { bookContentBean.setDurChapterContent(bookContentBean.getDurChapterContent() + "\n" + webContentBean.content); } } catch (Exception exception) { if (!e.isDisposed()) { e.onError(exception); } } } } String replaceRule = bookSourceBean.getRuleBookContentReplace(); if (replaceRule != null && replaceRule.trim().length() > 0) { analyzer.setContent(bookContentBean.getDurChapterContent()); bookContentBean.setDurChapterContent(analyzer.getString(replaceRule)); } e.onNext(bookContentBean); e.onComplete(); }); } private WebContentBean analyzeBookContent(AnalyzeRule analyzer, final String s, final String chapterUrl, String baseUrl) throws Exception { WebContentBean webContentBean = new WebContentBean(); analyzer.setContent(s, NetworkUtils.getAbsoluteURL(baseUrl, chapterUrl)); Debug.printLog(tag, 1, "┌解析正文内容"); if (ruleBookContent.equals("all") || ruleBookContent.contains("@all")) { webContentBean.content = analyzer.getString(ruleBookContent); } else { webContentBean.content = StringUtils.formatHtml(analyzer.getString(ruleBookContent)); } Debug.printLog(tag, 1, "└" + webContentBean.content); String nextUrlRule = bookSourceBean.getRuleContentUrlNext(); if (!TextUtils.isEmpty(nextUrlRule)) { Debug.printLog(tag, 1, "┌解析下一页url"); webContentBean.nextUrl = analyzer.getString(nextUrlRule, true); Debug.printLog(tag, 1, "└" + webContentBean.nextUrl); } return webContentBean; } private static class WebContentBean { private String content; private String nextUrl; private WebContentBean() { } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/content/BookInfo.java ================================================ package com.kunfei.bookshelf.model.content; import static android.text.TextUtils.isEmpty; import android.text.TextUtils; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeByRegex; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule; import com.kunfei.bookshelf.utils.StringUtils; import io.reactivex.Observable; class BookInfo { private String tag; private String sourceName; private BookSourceBean bookSourceBean; BookInfo(String tag, String sourceName, BookSourceBean bookSourceBean) { this.tag = tag; this.sourceName = sourceName; this.bookSourceBean = bookSourceBean; } Observable analyzeBookInfo(String s, final BookShelfBean bookShelfBean) { return Observable.create(e -> { String baseUrl = bookShelfBean.getNoteUrl(); if (TextUtils.isEmpty(s)) { e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_book_info_error) + baseUrl)); return; } else { Debug.printLog(tag, "┌成功获取详情页"); Debug.printLog(tag, "└" + baseUrl); } bookShelfBean.setTag(tag); BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean(); bookInfoBean.setNoteUrl(baseUrl); //id bookInfoBean.setTag(tag); bookInfoBean.setOrigin(sourceName); bookInfoBean.setBookSourceType(bookSourceBean.getBookSourceType()); // 是否为有声读物 AnalyzeRule analyzer = new AnalyzeRule(bookShelfBean, bookSourceBean); analyzer.setContent(s, baseUrl); // 获取详情页预处理规则 String ruleInfoInit = bookSourceBean.getRuleBookInfoInit(); boolean isRegex = false; if (!isEmpty(ruleInfoInit)) { // 仅使用java正则表达式提取书籍详情 if (ruleInfoInit.startsWith(":")) { isRegex = true; ruleInfoInit = ruleInfoInit.substring(1); Debug.printLog(tag, "┌详情信息预处理"); AnalyzeByRegex.getInfoOfRegex(s, ruleInfoInit.split("&&"), 0, bookShelfBean, analyzer, bookSourceBean, tag); } else { Object object = analyzer.getElement(ruleInfoInit); if (object != null) { analyzer.setContent(object); } } } if (!isRegex) { Debug.printLog(tag, "┌详情信息预处理"); Object object = analyzer.getElement(ruleInfoInit); if (object != null) analyzer.setContent(object); Debug.printLog(tag, "└详情预处理完成"); Debug.printLog(tag, "┌获取书名"); String bookName = StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookName())); if (!isEmpty(bookName)) bookInfoBean.setName(bookName); Debug.printLog(tag, "└" + bookName); Debug.printLog(tag, "┌获取作者"); String bookAuthor = StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookAuthor())); if (!isEmpty(bookAuthor)) bookInfoBean.setAuthor(bookAuthor); Debug.printLog(tag, "└" + bookAuthor); Debug.printLog(tag, "┌获取分类"); String bookKind = analyzer.getString(bookSourceBean.getRuleBookKind()); Debug.printLog(tag, 111, "└" + bookKind); Debug.printLog(tag, "┌获取最新章节"); String bookLastChapter = analyzer.getString(bookSourceBean.getRuleBookLastChapter()); if (!isEmpty(bookLastChapter)) bookShelfBean.setLastChapterName(bookLastChapter); Debug.printLog(tag, "└" + bookLastChapter); Debug.printLog(tag, "┌获取简介"); String bookIntroduce = analyzer.getString(bookSourceBean.getRuleIntroduce()); if (!isEmpty(bookIntroduce)) bookInfoBean.setIntroduce(bookIntroduce); Debug.printLog(tag, 1, "└" + bookIntroduce, true, true); Debug.printLog(tag, "┌获取封面"); String bookCoverUrl = analyzer.getString(bookSourceBean.getRuleCoverUrl(), true); if (!isEmpty(bookCoverUrl)) bookInfoBean.setCoverUrl(bookCoverUrl); Debug.printLog(tag, "└" + bookCoverUrl); Debug.printLog(tag, "┌获取目录网址"); String bookCatalogUrl = analyzer.getString(bookSourceBean.getRuleChapterUrl(), true); if (isEmpty(bookCatalogUrl)) bookCatalogUrl = baseUrl; bookInfoBean.setChapterUrl(bookCatalogUrl); //如果目录页和详情页相同,暂存页面内容供获取目录用 if (bookCatalogUrl.equals(baseUrl)) bookInfoBean.setChapterListHtml(s); Debug.printLog(tag, "└" + bookInfoBean.getChapterUrl()); bookShelfBean.setBookInfoBean(bookInfoBean); Debug.printLog(tag, "-详情页解析完成"); } e.onNext(bookShelfBean); e.onComplete(); }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/content/BookList.java ================================================ package com.kunfei.bookshelf.model.content; import static android.text.TextUtils.isEmpty; import android.os.Build; import android.text.TextUtils; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeByRegex; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.StringUtils; import org.mozilla.javascript.NativeObject; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import io.reactivex.Observable; import retrofit2.Response; class BookList { private final String tag; private final String sourceName; private final BookSourceBean bookSourceBean; private final boolean isFind; //规则 private String ruleList; private String ruleName; private String ruleAuthor; private String ruleKind; private String ruleIntroduce; private String ruleLastChapter; private String ruleCoverUrl; private String ruleNoteUrl; BookList(String tag, String sourceName, BookSourceBean bookSourceBean, boolean isFind) { this.tag = tag; this.sourceName = sourceName; this.bookSourceBean = bookSourceBean; this.isFind = isFind; } Observable> analyzeSearchBook(final Response response) { return Observable.create(e -> { String baseUrl; baseUrl = NetworkUtils.getUrl(response); if (TextUtils.isEmpty(response.body())) { e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_web_content_error, baseUrl))); return; } else { Debug.printLog(tag, "┌成功获取搜索结果"); Debug.printLog(tag, "└" + baseUrl); } String body = response.body(); List books = new ArrayList<>(); AnalyzeRule analyzer = new AnalyzeRule(null, bookSourceBean); analyzer.setContent(body, baseUrl); //如果符合详情页url规则 if (!isEmpty(bookSourceBean.getRuleBookUrlPattern()) && baseUrl.matches(bookSourceBean.getRuleBookUrlPattern())) { Debug.printLog(tag, ">搜索结果为详情页"); SearchBookBean item = getItem(analyzer, baseUrl); if (item != null) { item.setBookInfoHtml(body); books.add(item); } } else { initRule(); List collections; boolean reverse = false; boolean allInOne = false; if (ruleList.startsWith("-")) { reverse = true; ruleList = ruleList.substring(1); } // 仅使用java正则表达式提取书籍列表 if (ruleList.startsWith(":")) { ruleList = ruleList.substring(1); Debug.printLog(tag, "┌解析搜索列表"); getBooksOfRegex(body, ruleList.split("&&"), 0, analyzer, books); } else { if (ruleList.startsWith("+")) { allInOne = true; ruleList = ruleList.substring(1); } //获取列表 Debug.printLog(tag, "┌解析搜索列表"); collections = analyzer.getElements(ruleList); if (collections.size() == 0 && isEmpty(bookSourceBean.getRuleBookUrlPattern())) { Debug.printLog(tag, "└搜索列表为空,当做详情页处理"); SearchBookBean item = getItem(analyzer, baseUrl); if (item != null) { item.setBookInfoHtml(body); books.add(item); } } else { Debug.printLog(tag, "└找到 " + collections.size() + " 个匹配的结果"); if (allInOne) { for (int i = 0; i < collections.size(); i++) { Object object = collections.get(i); SearchBookBean item = getItemAllInOne(analyzer, object, baseUrl, i == 0); if (item != null) { //如果网址相同则缓存 if (baseUrl.equals(item.getNoteUrl())) { item.setBookInfoHtml(body); } books.add(item); } } } else { for (int i = 0; i < collections.size(); i++) { Object object = collections.get(i); analyzer.setContent(object, baseUrl); SearchBookBean item = getItemInList(analyzer, baseUrl, i == 0); if (item != null) { //如果网址相同则缓存 if (baseUrl.equals(item.getNoteUrl())) { item.setBookInfoHtml(body); } books.add(item); } } } } } if (books.size() > 1 && reverse) { Collections.reverse(books); } } if (books.isEmpty()) { e.onError(new Throwable(MApplication.getInstance().getString(R.string.no_book_name))); return; } Debug.printLog(tag, "-书籍列表解析结束"); e.onNext(books); e.onComplete(); }); } private void initRule() { if (isFind && !TextUtils.isEmpty(bookSourceBean.getRuleFindList())) { ruleList = bookSourceBean.getRuleFindList(); ruleName = bookSourceBean.getRuleFindName(); ruleAuthor = bookSourceBean.getRuleFindAuthor(); ruleKind = bookSourceBean.getRuleFindKind(); ruleIntroduce = bookSourceBean.getRuleFindIntroduce(); ruleCoverUrl = bookSourceBean.getRuleFindCoverUrl(); ruleLastChapter = bookSourceBean.getRuleFindLastChapter(); ruleNoteUrl = bookSourceBean.getRuleFindNoteUrl(); } else { ruleList = bookSourceBean.getRuleSearchList(); ruleName = bookSourceBean.getRuleSearchName(); ruleAuthor = bookSourceBean.getRuleSearchAuthor(); ruleKind = bookSourceBean.getRuleSearchKind(); ruleIntroduce = bookSourceBean.getRuleSearchIntroduce(); ruleCoverUrl = bookSourceBean.getRuleSearchCoverUrl(); ruleLastChapter = bookSourceBean.getRuleSearchLastChapter(); ruleNoteUrl = bookSourceBean.getRuleSearchNoteUrl(); } } /** * 详情页 */ private SearchBookBean getItem(AnalyzeRule analyzer, String baseUrl) throws Exception { SearchBookBean item = new SearchBookBean(); analyzer.setBook(item); item.setTag(tag); item.setOrigin(sourceName); item.setNoteUrl(baseUrl); // 获取详情页预处理规则 String ruleInfoInit = bookSourceBean.getRuleBookInfoInit(); if (!isEmpty(ruleInfoInit)) { // 仅使用java正则表达式提取书籍详情 if (ruleInfoInit.startsWith(":")) { ruleInfoInit = ruleInfoInit.substring(1); Debug.printLog(tag, "┌详情信息预处理"); BookShelfBean bookShelfBean = new BookShelfBean(); bookShelfBean.setTag(tag); bookShelfBean.setNoteUrl(baseUrl); AnalyzeByRegex.getInfoOfRegex(String.valueOf(analyzer.getContent()), ruleInfoInit.split("&&"), 0, bookShelfBean, analyzer, bookSourceBean, tag); if (isEmpty(bookShelfBean.getBookInfoBean().getName())) return null; item.setName(bookShelfBean.getBookInfoBean().getName()); item.setAuthor(bookShelfBean.getBookInfoBean().getAuthor()); item.setCoverUrl(bookShelfBean.getBookInfoBean().getCoverUrl()); item.setLastChapter(bookShelfBean.getLastChapterName()); item.setIntroduce(bookShelfBean.getBookInfoBean().getIntroduce()); return item; } else { Object object = analyzer.getElement(ruleInfoInit); if (object != null) { analyzer.setContent(object); } } } Debug.printLog(tag, ">书籍网址:" + baseUrl); Debug.printLog(tag, "┌获取书名"); String bookName = StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookName())); Debug.printLog(tag, "└" + bookName); if (!TextUtils.isEmpty(bookName)) { item.setName(bookName); Debug.printLog(tag, "┌获取作者"); item.setAuthor(StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookAuthor()))); Debug.printLog(tag, "└" + item.getAuthor()); Debug.printLog(tag, "┌获取分类"); item.setKind(analyzer.getString(bookSourceBean.getRuleBookKind())); Debug.printLog(tag, 111, "└" + item.getKind()); Debug.printLog(tag, "┌获取最新章节"); item.setLastChapter(analyzer.getString(bookSourceBean.getRuleBookLastChapter())); Debug.printLog(tag, "└" + item.getLastChapter()); Debug.printLog(tag, "┌获取简介"); item.setIntroduce(analyzer.getString(bookSourceBean.getRuleIntroduce())); Debug.printLog(tag, 1, "└" + item.getIntroduce(), true, true); Debug.printLog(tag, "┌获取封面"); item.setCoverUrl(analyzer.getString(bookSourceBean.getRuleCoverUrl(), true)); Debug.printLog(tag, "└" + item.getCoverUrl()); return item; } return null; } private SearchBookBean getItemAllInOne(AnalyzeRule analyzer, Object object, String baseUrl, boolean printLog) { SearchBookBean item = new SearchBookBean(); analyzer.setBook(item); NativeObject nativeObject = (NativeObject) object; Debug.printLog(tag, 1, "┌获取书名", printLog); String bookName = StringUtils.formatHtml(String.valueOf(nativeObject.get(ruleName))); Debug.printLog(tag, 1, "└" + bookName, printLog); if (!isEmpty(bookName)) { item.setTag(tag); item.setOrigin(sourceName); item.setName(bookName); Debug.printLog(tag, 1, "┌获取作者", printLog); item.setAuthor(StringUtils.formatHtml(String.valueOf(nativeObject.get(ruleAuthor)))); Debug.printLog(tag, 1, "└" + item.getAuthor(), printLog); Debug.printLog(tag, 1, "┌获取分类", printLog); item.setKind(String.valueOf(nativeObject.get(ruleKind))); Debug.printLog(tag, 111, "└" + item.getKind(), printLog); Debug.printLog(tag, 1, "┌获取最新章节", printLog); item.setLastChapter(String.valueOf(nativeObject.get(ruleLastChapter))); Debug.printLog(tag, 1, "└" + item.getLastChapter(), printLog); Debug.printLog(tag, 1, "┌获取简介", printLog); item.setIntroduce(String.valueOf(nativeObject.get(ruleIntroduce))); Debug.printLog(tag, 1, "└" + item.getIntroduce(), printLog, true); Debug.printLog(tag, 1, "┌获取封面", printLog); if (!isEmpty(ruleCoverUrl)) item.setCoverUrl(NetworkUtils.getAbsoluteURL(baseUrl, String.valueOf(nativeObject.get(ruleCoverUrl)))); Debug.printLog(tag, 1, "└" + item.getCoverUrl(), printLog); Debug.printLog(tag, 1, "┌获取书籍网址", printLog); String resultUrl = String.valueOf(nativeObject.get(ruleNoteUrl)); if (isEmpty(resultUrl)) resultUrl = baseUrl; item.setNoteUrl(resultUrl); Debug.printLog(tag, 1, "└" + item.getNoteUrl(), printLog); return item; } return null; } private SearchBookBean getItemInList(AnalyzeRule analyzer, String baseUrl, boolean printLog) throws Exception { SearchBookBean item = new SearchBookBean(); analyzer.setBook(item); Debug.printLog(tag, 1, "┌获取书名", printLog); String bookName = StringUtils.formatHtml(analyzer.getString(ruleName)); Debug.printLog(tag, 1, "└" + bookName, printLog); if (!TextUtils.isEmpty(bookName)) { item.setTag(tag); item.setOrigin(sourceName); item.setName(bookName); Debug.printLog(tag, 1, "┌获取作者", printLog); item.setAuthor(StringUtils.formatHtml(analyzer.getString(ruleAuthor))); Debug.printLog(tag, 1, "└" + item.getAuthor(), printLog); Debug.printLog(tag, 1, "┌获取分类", printLog); item.setKind(analyzer.getString(ruleKind)); Debug.printLog(tag, 111, "└" + item.getKind(), printLog); Debug.printLog(tag, 1, "┌获取最新章节", printLog); item.setLastChapter(analyzer.getString(ruleLastChapter)); Debug.printLog(tag, 1, "└" + item.getLastChapter(), printLog); Debug.printLog(tag, 1, "┌获取简介", printLog); item.setIntroduce(analyzer.getString(ruleIntroduce)); Debug.printLog(tag, 1, "└" + item.getIntroduce(), printLog, true); Debug.printLog(tag, 1, "┌获取封面", printLog); item.setCoverUrl(analyzer.getString(ruleCoverUrl, true)); Debug.printLog(tag, 1, "└" + item.getCoverUrl(), printLog); Debug.printLog(tag, 1, "┌获取书籍网址", printLog); String resultUrl = analyzer.getString(ruleNoteUrl, true); if (isEmpty(resultUrl)) resultUrl = baseUrl; item.setNoteUrl(resultUrl); Debug.printLog(tag, 1, "└" + item.getNoteUrl(), printLog); return item; } return null; } // 纯java模式正则表达式获取书籍列表 private void getBooksOfRegex(String res, String[] regs, int index, AnalyzeRule analyzer, final List books) throws Exception { Matcher resM = Pattern.compile(regs[index]).matcher(res); String baseUrl = analyzer.getBaseUrl(); // 判断规则是否有效,当搜索列表规则无效时当作详情页处理 if (!resM.find()) { books.add(getItem(analyzer, baseUrl)); return; } // 判断索引的规则是最后一个规则 if (index + 1 == regs.length) { // 获取规则列表 HashMap ruleMap = new HashMap<>(); ruleMap.put("ruleName", ruleName); ruleMap.put("ruleAuthor", ruleAuthor); ruleMap.put("ruleKind", ruleKind); ruleMap.put("ruleLastChapter", ruleLastChapter); ruleMap.put("ruleIntroduce", ruleIntroduce); ruleMap.put("ruleCoverUrl", ruleCoverUrl); ruleMap.put("ruleNoteUrl", ruleNoteUrl); // 分离规则参数 List ruleName = new ArrayList<>(); List> ruleParams = new ArrayList<>(); // 创建规则参数容器 List> ruleTypes = new ArrayList<>(); // 创建规则类型容器 List hasVarParams = new ArrayList<>(); // 创建put&get标志容器 for (String key : ruleMap.keySet()) { String val = ruleMap.get(key); ruleName.add(key); hasVarParams.add(!TextUtils.isEmpty(val) && (val.contains("@put") || val.contains("@get"))); List ruleParam = new ArrayList<>(); List ruleType = new ArrayList<>(); AnalyzeByRegex.splitRegexRule(val, ruleParam, ruleType); ruleParams.add(ruleParam); ruleTypes.add(ruleType); } // 提取书籍列表 do { // 新建书籍容器 SearchBookBean item = new SearchBookBean(tag, sourceName); analyzer.setBook(item); // 提取规则内容 HashMap ruleVal = new HashMap<>(); StringBuilder infoVal = new StringBuilder(); for (int i = ruleParams.size(); i-- > 0; ) { List ruleParam = ruleParams.get(i); List ruleType = ruleTypes.get(i); infoVal.setLength(0); for (int j = ruleParam.size(); j-- > 0; ) { int regType = ruleType.get(j); if (regType > 0) { infoVal.insert(0, resM.group(regType)); } else if (regType < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { infoVal.insert(0, resM.group(ruleParam.get(j))); } else { infoVal.insert(0, ruleParam.get(j)); } } ruleVal.put(ruleName.get(i), hasVarParams.get(i) ? AnalyzeByRegex.checkKeys(infoVal.toString(), analyzer) : infoVal.toString()); } // 保存当前节点的书籍信息 item.setSearchInfo( StringUtils.formatHtml(ruleVal.get("ruleName")), // 保存书名 StringUtils.formatHtml(ruleVal.get("ruleAuthor")), // 保存作者 ruleVal.get("ruleKind"), // 保存分类 ruleVal.get("ruleLastChapter"), // 保存终章 ruleVal.get("ruleIntroduce"), // 保存简介 ruleVal.get("ruleCoverUrl"), // 保存封面 NetworkUtils.getAbsoluteURL(baseUrl, ruleVal.get("ruleNoteUrl")) // 保存详情 ); books.add(item); // 判断搜索结果是否为详情页 if (books.size() == 1 && (isEmpty(ruleVal.get("ruleNoteUrl")) || ruleVal.get("ruleNoteUrl").equals(baseUrl))) { books.get(0).setNoteUrl(baseUrl); books.get(0).setBookInfoHtml(res); return; } } while (resM.find()); // 输出调试信息 Debug.printLog(tag, "└找到 " + books.size() + " 个匹配的结果"); Debug.printLog(tag, "┌获取书名"); Debug.printLog(tag, "└" + books.get(0).getName()); Debug.printLog(tag, "┌获取作者"); Debug.printLog(tag, "└" + books.get(0).getAuthor()); Debug.printLog(tag, "┌获取分类"); Debug.printLog(tag, 111, "└" + books.get(0).getKind()); Debug.printLog(tag, "┌获取最新章节"); Debug.printLog(tag, "└" + books.get(0).getLastChapter()); Debug.printLog(tag, "┌获取简介"); Debug.printLog(tag, 1, "└" + books.get(0).getIntroduce(), true, true); Debug.printLog(tag, "┌获取封面"); Debug.printLog(tag, "└" + books.get(0).getCoverUrl()); Debug.printLog(tag, "┌获取书籍"); Debug.printLog(tag, "└" + books.get(0).getNoteUrl()); } else { StringBuilder result = new StringBuilder(); do { result.append(resM.group()); } while (resM.find()); getBooksOfRegex(result.toString(), regs, ++index, analyzer, books); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/content/Debug.java ================================================ package com.kunfei.bookshelf.model.content; import android.annotation.SuppressLint; import android.text.TextUtils; import androidx.annotation.NonNull; import com.hwangjr.rxbus.RxBus; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.UpLastChapterModel; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.TimeUtils; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.List; import java.util.Locale; import java.util.Objects; import io.reactivex.Observer; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; public class Debug { public static String SOURCE_DEBUG_TAG; @SuppressLint("ConstantLocale") private static final DateFormat DEBUG_TIME_FORMAT = new SimpleDateFormat("[mm:ss.SSS]", Locale.getDefault()); private static long startTime; private static String getDoTime() { return TimeUtils.millis2String(System.currentTimeMillis() - startTime, DEBUG_TIME_FORMAT); } public static void printLog(String tag, String msg) { printLog(tag, 1, msg, true); } static void printLog(String tag, int state, String msg) { printLog(tag, state, msg, true); } static void printLog(String tag, int state, String msg, boolean print) { printLog(tag, state, msg, print, false); } public static void printLog(String tag, int state, String msg, boolean print, boolean formatHtml) { if (print && Objects.equals(SOURCE_DEBUG_TAG, tag)) { if (formatHtml) { msg = StringUtils.formatHtml(msg); } if (state == 111) { msg = msg.replace("\n", ","); } msg = String.format("%s %s", getDoTime(), msg); RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, msg); } } public static void newDebug(String tag, String key, @NonNull CompositeDisposable compositeDisposable) { new Debug(tag, key, compositeDisposable); } private final CompositeDisposable compositeDisposable; private Debug(String tag, String key, CompositeDisposable compositeDisposable) { UpLastChapterModel.destroy(); startTime = System.currentTimeMillis(); SOURCE_DEBUG_TAG = tag; this.compositeDisposable = compositeDisposable; if (NetworkUtils.isUrl(key)) { printLog(String.format("%s %s", getDoTime(), "⇒开始访问详情页:" + key)); BookShelfBean bookShelfBean = new BookShelfBean(); bookShelfBean.setTag(Debug.SOURCE_DEBUG_TAG); bookShelfBean.setNoteUrl(key); bookShelfBean.setDurChapter(0); bookShelfBean.setGroup(0); bookShelfBean.setDurChapterPage(0); bookShelfBean.setFinalDate(System.currentTimeMillis()); bookInfoDebug(bookShelfBean); } else if (key.contains("::")) { String url = key.substring(key.indexOf("::") + 2); printLog(String.format("%s %s", getDoTime(), "⇒开始访问发现页:" + url)); findDebug(url); } else { printLog(String.format("%s %s", getDoTime(), "⇒开始搜索关键字:" + key)); searchDebug(key); } } private void findDebug(String url) { printLog(String.format("\n%s ≡开始获取发现页", getDoTime())); WebBookModel.getInstance().findBook(url, 1, Debug.SOURCE_DEBUG_TAG) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @SuppressLint("DefaultLocale") @Override public void onNext(List searchBookBeans) { SearchBookBean searchBookBean = searchBookBeans.get(0); if (!TextUtils.isEmpty(searchBookBean.getNoteUrl())) { bookInfoDebug(BookshelfHelp.getBookFromSearchBook(searchBookBean)); } } @Override public void onError(Throwable e) { printError(e.getMessage()); } @Override public void onComplete() { } }); } private void searchDebug(String key) { printLog(String.format("\n%s ≡开始获取搜索页", getDoTime())); WebBookModel.getInstance().searchBook(key, 1, Debug.SOURCE_DEBUG_TAG) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @SuppressLint("DefaultLocale") @Override public void onNext(List searchBookBeans) { SearchBookBean searchBookBean = searchBookBeans.get(0); if (!TextUtils.isEmpty(searchBookBean.getNoteUrl())) { bookInfoDebug(BookshelfHelp.getBookFromSearchBook(searchBookBean)); } } @Override public void onError(Throwable e) { printError(e.getMessage()); } @Override public void onComplete() { } }); } private void bookInfoDebug(BookShelfBean bookShelfBean) { printLog(String.format("\n%s ≡开始获取详情页", getDoTime())); WebBookModel.getInstance().getBookInfo(bookShelfBean) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(BookShelfBean bookShelfBean) { bookChapterListDebug(bookShelfBean); } @Override public void onError(Throwable e) { printError(e.getMessage()); } @Override public void onComplete() { } }); } private void bookChapterListDebug(BookShelfBean bookShelfBean) { printLog(String.format("\n%s ≡开始获取目录页", getDoTime())); WebBookModel.getInstance().getChapterList(bookShelfBean) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @SuppressLint("DefaultLocale") @Override public void onNext(List chapterBeanList) { if (chapterBeanList.size() > 0) { BookChapterBean nextChapter = chapterBeanList.size() > 2 ? chapterBeanList.get(1) : null; bookContentDebug(bookShelfBean, chapterBeanList.get(0), nextChapter); } else { printError("获取到的目录为空"); } } @Override public void onError(Throwable e) { printError(e.getMessage()); } @Override public void onComplete() { } }); } private void bookContentDebug(BookShelfBean bookShelfBean, BookChapterBean bookChapterBean, BookChapterBean nextChapterBean) { printLog(String.format("\n%s ≡开始获取正文页", getDoTime())); WebBookModel.getInstance().getBookContent(bookShelfBean, bookChapterBean, nextChapterBean) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(BookContentBean bookContentBean) { } @Override public void onError(Throwable e) { printError(e.getMessage()); } @Override public void onComplete() { finish(); } }); } private void printLog(String log) { RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, log); } private void printError(String msg) { RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, msg); finish(); } private void finish() { RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, "finish"); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/content/VipThrowable.java ================================================ package com.kunfei.bookshelf.model.content; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; public class VipThrowable extends Throwable { private final static String tag = "VIP_THROWABLE"; VipThrowable() { super(MApplication.getInstance().getString(R.string.donate_s)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/content/WebBook.java ================================================ package com.kunfei.bookshelf.model.content; import static android.text.TextUtils.isEmpty; import static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN; import static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE; import android.text.TextUtils; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.bean.BaseChapterBean; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.help.JsExtensions; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import javax.script.SimpleBindings; import io.reactivex.Observable; import retrofit2.Response; /** * 默认检索规则 */ public class WebBook extends BaseModelImpl implements JsExtensions { private final String tag; private String name; private final BookSourceBean bookSourceBean; private Map headerMap; public static WebBook getInstance(String tag) { return new WebBook(tag); } private WebBook(String tag) { this.tag = tag; try { URL url = new URL(tag); name = url.getHost(); } catch (MalformedURLException e) { name = tag; } bookSourceBean = BookSourceManager.getBookSourceByUrl(tag); if (bookSourceBean != null) { name = bookSourceBean.getBookSourceName(); headerMap = bookSourceBean.getHeaderMap(true); } } /** * 发现 */ public Observable> findBook(String url, int page) { if (bookSourceBean == null) { return Observable.error(new NoSourceThrowable(tag)); } BookList bookList = new BookList(tag, name, bookSourceBean, true); try { AnalyzeUrl analyzeUrl = new AnalyzeUrl( url, tag, bookSourceBean, null, page, bookSourceBean.getHeaderMap(true) ); return getResponseO(analyzeUrl) .flatMap(response -> checkLogin(response, url, tag)) .flatMap(bookList::analyzeSearchBook); } catch (Exception e) { return Observable.error(new Throwable(String.format("%s错误:%s", url, e.getLocalizedMessage()))); } } /** * 搜索 */ public Observable> searchBook(String content, int page) { if (bookSourceBean == null || isEmpty(bookSourceBean.getRuleSearchUrl())) { return Observable.create(emitter -> { emitter.onNext(new ArrayList<>()); emitter.onComplete(); }); } BookList bookList = new BookList(tag, name, bookSourceBean, false); try { AnalyzeUrl analyzeUrl = new AnalyzeUrl( bookSourceBean.getRuleSearchUrl(), tag, bookSourceBean, content, page, bookSourceBean.getHeaderMap(true) ); return getResponseO(analyzeUrl) .flatMap(response -> checkLogin(response, bookSourceBean.getRuleSearchUrl(), tag)) .flatMap(bookList::analyzeSearchBook); } catch (Exception e) { return Observable.error(e); } } /** * 获取书籍信息 */ public Observable getBookInfo(final BookShelfBean bookShelfBean) { if (bookSourceBean == null) { return Observable.error(new NoSourceThrowable(tag)); } BookInfo bookInfo = new BookInfo(tag, name, bookSourceBean); if (!TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getBookInfoHtml())) { return bookInfo.analyzeBookInfo(bookShelfBean.getBookInfoBean().getBookInfoHtml(), bookShelfBean); } try { AnalyzeUrl analyzeUrl = new AnalyzeUrl( bookShelfBean.getNoteUrl(), tag, bookSourceBean, bookSourceBean.getHeaderMap(true) ); return getResponseO(analyzeUrl) .flatMap(response -> setCookie(response, tag)) .flatMap(response -> checkLogin(response, bookShelfBean.getNoteUrl(), tag)) .flatMap(response -> bookInfo.analyzeBookInfo(response.body(), bookShelfBean)); } catch (Exception e) { return Observable.error(new Throwable(String.format("url错误:%s", bookShelfBean.getNoteUrl()))); } } /** * 获取目录 */ public Observable> getChapterList(final BookShelfBean bookShelfBean) { if (bookSourceBean == null) { return Observable.error(new NoSourceThrowable(bookShelfBean.getBookInfoBean().getName())); } BookChapterList bookChapterList = new BookChapterList(tag, bookSourceBean, true); if (!TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getChapterListHtml())) { return bookChapterList.analyzeChapterList(bookShelfBean.getBookInfoBean().getChapterListHtml(), bookShelfBean, headerMap); } try { AnalyzeUrl analyzeUrl = new AnalyzeUrl( bookShelfBean.getBookInfoBean().getChapterUrl(), bookShelfBean.getNoteUrl(), bookSourceBean, bookSourceBean.getHeaderMap(true) ); return getResponseO(analyzeUrl) .flatMap(response -> setCookie(response, tag)) .flatMap(stringResponse -> checkLogin(stringResponse, bookShelfBean.getBookInfoBean().getChapterUrl(), bookShelfBean.getNoteUrl())) .flatMap(response -> bookChapterList.analyzeChapterList(response.body(), bookShelfBean, headerMap)); } catch (Exception e) { return Observable.error(new Throwable(String.format("url错误:%s", bookShelfBean.getBookInfoBean().getChapterUrl()))); } } /** * 获取正文 */ public Observable getBookContent(final BaseChapterBean chapterBean, final BaseChapterBean nextChapterBean, final BookShelfBean bookShelfBean) { if (bookSourceBean == null) { return Observable.error(new NoSourceThrowable(chapterBean.getTag())); } if (isEmpty(bookSourceBean.getRuleBookContent())) { return Observable.create(emitter -> { BookContentBean bookContentBean = new BookContentBean(); bookContentBean.setDurChapterContent(chapterBean.getDurChapterUrl()); bookContentBean.setDurChapterIndex(chapterBean.getDurChapterIndex()); bookContentBean.setTag(bookShelfBean.getTag()); bookContentBean.setDurChapterUrl(chapterBean.getDurChapterUrl()); emitter.onNext(bookContentBean); emitter.onComplete(); }); } BookContent bookContent = new BookContent(tag, bookSourceBean); if (Objects.equals(chapterBean.getDurChapterUrl(), bookShelfBean.getBookInfoBean().getChapterUrl()) && !TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getChapterListHtml())) { return bookContent.analyzeBookContent(bookShelfBean.getBookInfoBean().getChapterListHtml(), chapterBean, nextChapterBean, bookShelfBean, headerMap); } try { AnalyzeUrl analyzeUrl = new AnalyzeUrl( chapterBean.getDurChapterUrl(), bookShelfBean.getBookInfoBean().getChapterUrl(), bookSourceBean, bookSourceBean.getHeaderMap(true)); String contentRule = bookSourceBean.getRuleBookContent(); if (contentRule.startsWith("$") && !contentRule.startsWith("$.")) { //动态网页第一个js放到webView里执行 contentRule = contentRule.substring(1); String js = null; Matcher jsMatcher = JS_PATTERN.matcher(contentRule); if (jsMatcher.find()) { js = jsMatcher.group(); if (js.startsWith("")) { js = js.substring(4, js.lastIndexOf("<")); } else { js = js.substring(4); } } return getAjaxString(analyzeUrl, tag, js) .flatMap(response -> bookContent.analyzeBookContent(response, chapterBean, nextChapterBean, bookShelfBean, headerMap)); } else { return getResponseO(analyzeUrl) .flatMap(response -> setCookie(response, tag)) .flatMap(stringResponse -> checkLogin(stringResponse, chapterBean.getDurChapterUrl(), bookShelfBean.getBookInfoBean().getChapterUrl())) .flatMap(response -> bookContent.analyzeBookContent(response, chapterBean, nextChapterBean, bookShelfBean, headerMap)); } } catch (Exception e) { return Observable.error(new Throwable(String.format("url错误:%s", e.getLocalizedMessage()))); } } Observable> checkLogin(final Response stringResponse, String url, String baseUrl) { return Observable.create(emitter -> { String checkJs = bookSourceBean.getLoginCheckJs(); if (!TextUtils.isEmpty(checkJs)) { SimpleBindings bindings = new SimpleBindings(); bindings.put("source", bookSourceBean); bindings.put("url", url); bindings.put("java", this); bindings.put("result", stringResponse); bindings.put("baseUrl", baseUrl); @SuppressWarnings("unchecked") Response res = (Response) SCRIPT_ENGINE.eval(checkJs, bindings); emitter.onNext(res); return; } emitter.onNext(stringResponse); }); } public class NoSourceThrowable extends Throwable { NoSourceThrowable(String tag) { super(String.format("%s没有找到书源配置", tag)); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/impl/IDownloadTask.java ================================================ package com.kunfei.bookshelf.model.impl; import com.kunfei.bookshelf.bean.DownloadBookBean; import com.kunfei.bookshelf.bean.DownloadChapterBean; import io.reactivex.Scheduler; public interface IDownloadTask { int getId(); void startDownload(Scheduler scheduler); void stopDownload(); boolean isDownloading(); boolean isFinishing(); DownloadBookBean getDownloadBook(); void onDownloadPrepared(DownloadBookBean downloadBook); void onDownloadProgress(DownloadChapterBean chapterBean); void onDownloadChange(DownloadBookBean downloadBook); void onDownloadError(DownloadBookBean downloadBook); void onDownloadComplete(DownloadBookBean downloadBook); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/impl/IHttpGetApi.java ================================================ package com.kunfei.bookshelf.model.impl; import java.util.Map; import io.reactivex.Observable; import retrofit2.Response; import retrofit2.http.GET; import retrofit2.http.HeaderMap; import retrofit2.http.QueryMap; import retrofit2.http.Url; /** * Created by GKF on 2018/1/21. * get web content */ public interface IHttpGetApi { @GET Observable> get(@Url String url, @HeaderMap Map headers); @GET Observable> getMap(@Url String url, @QueryMap(encoded = true) Map queryMap, @HeaderMap Map headers); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/impl/IHttpPostApi.java ================================================ package com.kunfei.bookshelf.model.impl; import java.util.Map; import io.reactivex.Observable; import okhttp3.RequestBody; import retrofit2.Response; import retrofit2.http.Body; import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; import retrofit2.http.HeaderMap; import retrofit2.http.POST; import retrofit2.http.Url; /** * Created by GKF on 2018/1/29. * post */ public interface IHttpPostApi { @FormUrlEncoded @POST Observable> postMap(@Url String url, @FieldMap(encoded = true) Map fieldMap, @HeaderMap Map headers); @POST Observable> postJson(@Url String url, @Body RequestBody body, @HeaderMap Map headers); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/task/AnalyzeNextUrlTask.java ================================================ package com.kunfei.bookshelf.model.task; import com.kunfei.bookshelf.base.BaseModelImpl; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.WebChapterBean; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl; import com.kunfei.bookshelf.model.content.BookChapterList; import java.util.List; import java.util.Map; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class AnalyzeNextUrlTask { private WebChapterBean webChapterBean; private Callback callback; private BookShelfBean bookShelfBean; private Map headerMap; private BookChapterList bookChapterList; public AnalyzeNextUrlTask(BookChapterList bookChapterList, WebChapterBean webChapterBean, BookShelfBean bookShelfBean, Map headerMap) { this.bookChapterList = bookChapterList; this.webChapterBean = webChapterBean; this.bookShelfBean = bookShelfBean; this.headerMap = headerMap; } public AnalyzeNextUrlTask setCallback(Callback callback) { this.callback = callback; return this; } public void analyzeUrl(AnalyzeUrl analyzeUrl) { BaseModelImpl.getInstance().getResponseO(analyzeUrl) .flatMap(stringResponse -> bookChapterList.analyzeChapterList(stringResponse.body(), bookShelfBean, headerMap)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe(new MyObserver>() { @Override public void onSubscribe(Disposable d) { callback.addDisposable(d); } @Override public void onNext(List bookChapterBeans) { callback.analyzeFinish(webChapterBean, bookChapterBeans); } @Override public void onError(Throwable throwable) { callback.onError(throwable); } }); } public interface Callback { void addDisposable(Disposable disposable); void analyzeFinish(WebChapterBean bean, List bookChapterBeans); void onError(Throwable throwable); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/task/CheckSourceTask.java ================================================ package com.kunfei.bookshelf.model.task; import static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE; import android.text.TextUtils; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule; import com.kunfei.bookshelf.service.CheckSourceService; import java.util.List; import java.util.concurrent.TimeUnit; import javax.script.SimpleBindings; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.Observer; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; public class CheckSourceTask { private BookSourceBean sourceBean; private Scheduler scheduler; private CheckSourceService.CheckSourceListener checkSourceListener; public CheckSourceTask(BookSourceBean sourceBean, Scheduler scheduler, CheckSourceService.CheckSourceListener checkSourceListener) { this.sourceBean = sourceBean; this.scheduler = scheduler; this.checkSourceListener = checkSourceListener; } public void startCheck() { if (!TextUtils.isEmpty(sourceBean.getRuleSearchUrl())) { WebBookModel.getInstance().searchBook("我的", 1, sourceBean.getBookSourceUrl()) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .timeout(60, TimeUnit.SECONDS) .subscribe(new Observer>() { @Override public void onSubscribe(Disposable d) { checkSourceListener.compositeDisposableAdd(d); } @Override public void onNext(List searchBookBeans) { if (searchBookBeans.isEmpty()) { checkFind(); } else { sourceUnInvalid(); } } @Override public void onError(Throwable e) { checkFind(); } @Override public void onComplete() { } }); } else { checkFind(); } } private void checkFind() { if (!TextUtils.isEmpty(sourceBean.getRuleFindUrl())) { Observable.create((ObservableOnSubscribe) emitter -> { String[] kindA; if (!TextUtils.isEmpty(sourceBean.getRuleFindUrl())) { if (sourceBean.getRuleFindUrl().startsWith("")) { String jsStr = sourceBean.getRuleFindUrl().substring(4, sourceBean.getRuleFindUrl().lastIndexOf("<")); Object object = evalJS(jsStr, sourceBean.getBookSourceUrl(), sourceBean); kindA = object.toString().split("(&&|\n)+"); } else { kindA = sourceBean.getRuleFindUrl().split("(&&|\n)+"); } emitter.onNext(kindA[0].split("::")[1]); emitter.onComplete(); } }).flatMap(url -> WebBookModel.getInstance().findBook(url, 1, sourceBean.getBookSourceUrl())) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .timeout(60, TimeUnit.SECONDS) .subscribe(new Observer>() { @Override public void onSubscribe(Disposable d) { checkSourceListener.compositeDisposableAdd(d); } @Override public void onNext(List searchBookBeans) { if (searchBookBeans.isEmpty()) { sourceInvalid(); } else { sourceUnInvalid(); } } @Override public void onError(Throwable e) { sourceInvalid(); } @Override public void onComplete() { } }); } else { sourceInvalid(); } } private void sourceInvalid() { sourceBean.addGroup("失效"); sourceBean.setEnable(false); sourceBean.setSerialNumber(10000 + checkSourceListener.getCheckIndex()); DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(sourceBean); checkSourceListener.nextCheck(); } private void sourceUnInvalid() { if (sourceBean.containsGroup("失效")) { sourceBean.removeGroup("失效"); DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(sourceBean); } checkSourceListener.nextCheck(); } /** * 执行JS */ private Object evalJS(String jsStr, String baseUrl, BookSourceBean bookSourceBean) throws Exception { SimpleBindings bindings = new SimpleBindings(); bindings.put("java", new AnalyzeRule(null, bookSourceBean)); bindings.put("baseUrl", baseUrl); return SCRIPT_ENGINE.eval(jsStr, bindings); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/model/task/DownloadTaskImpl.java ================================================ package com.kunfei.bookshelf.model.task; import android.text.TextUtils; import com.hwangjr.rxbus.RxBus; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.DownloadBookBean; import com.kunfei.bookshelf.bean.DownloadChapterBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.model.impl.IDownloadTask; import java.util.ArrayList; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public abstract class DownloadTaskImpl implements IDownloadTask { private int id; private boolean isDownloading = false; private DownloadBookBean downloadBook; private List downloadChapters; private boolean isLocked = false; private CompositeDisposable disposables; protected DownloadTaskImpl(int id, DownloadBookBean downloadBook) { this.id = id; this.downloadBook = downloadBook; downloadChapters = new ArrayList<>(); disposables = new CompositeDisposable(); Observable.create((ObservableOnSubscribe) emitter -> { List chapterList = BookshelfHelp.getChapterList(downloadBook.getNoteUrl()); if (!chapterList.isEmpty()) { for (int i = downloadBook.getStart(); i <= downloadBook.getEnd(); i++) { DownloadChapterBean chapter = new DownloadChapterBean(); chapter.setBookName(downloadBook.getName()); chapter.setDurChapterIndex(chapterList.get(i).getDurChapterIndex()); chapter.setDurChapterName(chapterList.get(i).getDurChapterName()); chapter.setDurChapterUrl(chapterList.get(i).getDurChapterUrl()); chapter.setNoteUrl(chapterList.get(i).getNoteUrl()); chapter.setTag(chapterList.get(i).getTag()); if (!BookshelfHelp.isChapterCached(chapter.getBookName(), chapter.getTag(), chapter, false)) { downloadChapters.add(chapter); } } } downloadBook.setDownloadCount(downloadChapters.size()); emitter.onNext(downloadBook); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onSubscribe(Disposable d) { disposables.add(d); } @Override public void onNext(DownloadBookBean downloadBook) { if (downloadBook.isValid()) { onDownloadPrepared(downloadBook); whenProgress(downloadChapters.get(0)); } else { onDownloadComplete(downloadBook); } } @Override public void onError(Throwable e) { downloadBook.setValid(false); onDownloadError(downloadBook); } }); } @Override public int getId() { return id; } @Override public void startDownload(Scheduler scheduler) { if (isFinishing()) return; if (disposables.isDisposed()) { disposables = new CompositeDisposable(); } isDownloading = true; toDownload(scheduler); } @Override public void stopDownload() { if (!disposables.isDisposed()) { disposables.dispose(); } if (isDownloading) { isDownloading = false; onDownloadComplete(downloadBook); } if (!isFinishing()) { downloadChapters.clear(); } } @Override public boolean isDownloading() { return isDownloading; } @Override public boolean isFinishing() { return downloadChapters.isEmpty(); } @Override public DownloadBookBean getDownloadBook() { return downloadBook; } private synchronized void toDownload(Scheduler scheduler) { if (isFinishing()) { return; } if (!isLocked) { getDownloadingChapter() .subscribe(new MyObserver() { @Override public void onNext(DownloadChapterBean chapterBean) { if (chapterBean != null) { downloading(chapterBean, scheduler); } else { isLocked = true; } } @Override public void onError(Throwable e) { onDownloadError(downloadBook); } }); } } /** * @return 章节下载信息 */ private Observable getDownloadingChapter() { return Observable.create(emitter -> { DownloadChapterBean next = null; List temp = new ArrayList<>(downloadChapters); for (DownloadChapterBean data : temp) { boolean cached = BookshelfHelp.isChapterCached(data.getBookName(), data.getTag(), data, false); if (cached) { removeFromDownloadList(data); } else { next = data; break; } } emitter.onNext(next); }); } /** * 下载 */ private synchronized void downloading(DownloadChapterBean chapter, Scheduler scheduler) { whenProgress(chapter); BookShelfBean bookShelfBean = BookshelfHelp.getBook(chapter.getNoteUrl()); Observable.create((ObservableOnSubscribe) e -> { if (!BookshelfHelp.isChapterCached(chapter.getBookName(), chapter.getTag(), chapter, false)) { e.onNext(chapter); } else { e.onError(new Exception("cached")); } e.onComplete(); }) .flatMap(result -> { return WebBookModel.getInstance().getBookContent(bookShelfBean, chapter, null); }) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onSubscribe(Disposable d) { disposables.add(d); } @Override public void onNext(BookContentBean bookContentBean) { RxBus.get().post(RxBusTag.CHAPTER_CHANGE, bookContentBean); removeFromDownloadList(chapter); whenNext(scheduler, true); } @Override public void onError(Throwable e) { removeFromDownloadList(chapter); if (TextUtils.equals(e.getMessage(), "cached")) { whenNext(scheduler, false); } else { whenError(scheduler); } } }); } /** * 从下载列表移除 */ private synchronized void removeFromDownloadList(DownloadChapterBean chapterBean) { downloadChapters.remove(chapterBean); } private void whenNext(Scheduler scheduler, boolean success) { if (!isDownloading) { return; } if (success) { downloadBook.successCountAdd(); } if (isFinishing()) { stopDownload(); onDownloadComplete(downloadBook); } else { onDownloadChange(downloadBook); toDownload(scheduler); } } private void whenError(Scheduler scheduler) { if (!isDownloading) { return; } if (isFinishing()) { stopDownload(); if (downloadBook.getSuccessCount() == 0) { onDownloadError(downloadBook); } else { onDownloadComplete(downloadBook); } } else { toDownload(scheduler); } } private void whenProgress(DownloadChapterBean chapterBean) { if (!isDownloading) { return; } onDownloadProgress(chapterBean); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/BookDetailPresenter.java ================================================ package com.kunfei.bookshelf.presenter; import android.content.Intent; import android.text.TextUtils; import androidx.annotation.NonNull; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.OpenChapterBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.bean.TwoDataBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.ChangeSourceHelp; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.SavedSource; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.presenter.contract.BookDetailContract; import com.kunfei.bookshelf.utils.RxUtils; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; public class BookDetailPresenter extends BasePresenterImpl implements BookDetailContract.Presenter { public final static int FROM_BOOKSHELF = 1; public final static int FROM_SEARCH = 2; private int openFrom; private SearchBookBean searchBook; private BookShelfBean bookShelf; private List chapterBeanList; private Boolean inBookShelf = false; private final CompositeDisposable compositeDisposable = new CompositeDisposable(); private Disposable changeSourceDisposable; @Override public void initData(Intent intent) { openFrom = intent.getIntExtra("openFrom", FROM_BOOKSHELF); String key = intent.getStringExtra("data_key"); if (openFrom == FROM_BOOKSHELF) { bookShelf = (BookShelfBean) BitIntentDataManager.getInstance().getData(key); if (bookShelf == null) { String noteUrl = intent.getStringExtra("noteUrl"); if (!TextUtils.isEmpty(noteUrl)) { bookShelf = BookshelfHelp.getBook(noteUrl); } } if (bookShelf == null) { mView.finish(); return; } inBookShelf = true; searchBook = new SearchBookBean(); searchBook.setNoteUrl(bookShelf.getNoteUrl()); searchBook.setTag(bookShelf.getTag()); chapterBeanList = BookshelfHelp.getChapterList(bookShelf.getNoteUrl()); } else { initBookFormSearch((SearchBookBean) BitIntentDataManager.getInstance().getData(key)); } } @Override public void initBookFormSearch(SearchBookBean searchBookBean) { if (searchBookBean == null) { mView.finish(); return; } searchBook = searchBookBean; inBookShelf = BookshelfHelp.isInBookShelf(searchBookBean.getNoteUrl()); bookShelf = BookshelfHelp.getBookFromSearchBook(searchBookBean); } @Override public Boolean getInBookShelf() { return inBookShelf; } @Override public int getOpenFrom() { return openFrom; } @Override public SearchBookBean getSearchBook() { return searchBook; } @Override public BookShelfBean getBookShelf() { return bookShelf; } @Override public List getChapterList() { return chapterBeanList; } @Override public void getBookShelfInfo() { if (bookShelf == null) return; if (BookShelfBean.LOCAL_TAG.equals(bookShelf.getTag())) return; WebBookModel.getInstance().getBookInfo(bookShelf) .flatMap(bookShelfBean -> WebBookModel.getInstance().getChapterList(bookShelfBean)) .flatMap(chapterBeans -> saveBookToShelfO(bookShelf, chapterBeans)) .compose(RxUtils::toSimpleSingle) .subscribe(new MyObserver>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(@NonNull List bookChapterBeans) { chapterBeanList = bookChapterBeans; mView.updateView(); } @Override public void onError(Throwable e) { e.printStackTrace(); mView.toast(e.getLocalizedMessage()); mView.getBookShelfError(); } }); } /** * 保存数据 */ private Observable> saveBookToShelfO(BookShelfBean bookShelfBean, List chapterBeans) { return Observable.create(e -> { if (inBookShelf) { BookshelfHelp.saveBookToShelf(bookShelfBean); if (!chapterBeans.isEmpty()) { BookshelfHelp.delChapterList(bookShelfBean.getNoteUrl()); DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeans); } RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelf); } e.onNext(chapterBeans); e.onComplete(); }); } @Override public void addToBookShelf() { if (bookShelf != null) { Observable.create((ObservableOnSubscribe) e -> { BookshelfHelp.saveBookToShelf(bookShelf); searchBook.setIsCurrentSource(true); inBookShelf = true; e.onNext(true); e.onComplete(); }).compose(RxUtils::toSimpleSingle) .subscribe(new MyObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(@NonNull Boolean value) { if (value) { RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelf); mView.updateView(); } else { mView.toast("放入书架失败!"); } } @Override public void onError(Throwable e) { e.printStackTrace(); mView.toast("放入书架失败!"); } }); } } @Override public void removeFromBookShelf() { if (bookShelf != null) { Observable.create((ObservableOnSubscribe) e -> { BookshelfHelp.removeFromBookShelf(bookShelf); searchBook.setIsCurrentSource(false); inBookShelf = false; e.onNext(true); e.onComplete(); }).compose(RxUtils::toSimpleSingle) .subscribe(new MyObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(@NonNull Boolean value) { if (value) { RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf); mView.updateView(); } else { mView.toast("删除书籍失败!"); } } @Override public void onError(Throwable e) { e.printStackTrace(); mView.toast("删除书籍失败!"); } }); } } /** * 换源 */ @Override public void changeBookSource(SearchBookBean searchBookBean) { if (changeSourceDisposable != null && !changeSourceDisposable.isDisposed()) { changeSourceDisposable.dispose(); } searchBookBean.setName(bookShelf.getBookInfoBean().getName()); searchBookBean.setAuthor(bookShelf.getBookInfoBean().getAuthor()); ChangeSourceHelp.changeBookSource(searchBookBean, bookShelf) .subscribe(new MyObserver>>() { @Override public void onSubscribe(Disposable d) { super.onSubscribe(d); compositeDisposable.add(d); changeSourceDisposable = d; } @Override public void onNext(@NonNull TwoDataBean> value) { RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf); RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value); bookShelf = value.getData1(); chapterBeanList = value.getData2(); mView.updateView(); String tag = bookShelf.getTag(); try { long currentTime = System.currentTimeMillis(); String bookName = bookShelf.getBookInfoBean().getName(); BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(tag); if (SavedSource.Instance.getBookSource() != null && currentTime - SavedSource.Instance.getSaveTime() < 60000 && SavedSource.Instance.getBookName().equals(bookName)) SavedSource.Instance.getBookSource().increaseWeight(-450); BookSourceManager.saveBookSource(SavedSource.Instance.getBookSource()); SavedSource.Instance.setBookName(bookName); SavedSource.Instance.setSaveTime(currentTime); SavedSource.Instance.setBookSource(bookSourceBean); assert bookSourceBean != null; bookSourceBean.increaseWeightBySelection(); BookSourceManager.saveBookSource(bookSourceBean); } catch (Exception e) { e.printStackTrace(); } } @Override public void onError(Throwable e) { e.printStackTrace(); mView.updateView(); mView.toast(e.getMessage()); } }); } @Override public void attachView(@NonNull IView iView) { super.attachView(iView); RxBus.get().register(this); } @Override public void detachView() { RxBus.get().unregister(this); compositeDisposable.dispose(); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.HAD_ADD_BOOK), @Tag(RxBusTag.UPDATE_BOOK_PROGRESS)}) public void hadAddOrRemoveBook(BookShelfBean bookShelfBean) { bookShelf = bookShelfBean; mView.updateView(); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.SKIP_TO_CHAPTER)}) public void skipToChapter(OpenChapterBean openChapterBean) { bookShelf.setDurChapter(openChapterBean.getChapterIndex()); bookShelf.setDurChapterPage(openChapterBean.getPageIndex()); if (inBookShelf) { BookshelfHelp.saveBookToShelf(bookShelf); } mView.readBook(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/BookListPresenter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.presenter; import android.os.AsyncTask; import androidx.annotation.NonNull; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.DownloadBookBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.model.content.WebBook; import com.kunfei.bookshelf.presenter.contract.BookListContract; import com.kunfei.bookshelf.service.DownloadService; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.RxUtils; import java.util.ArrayList; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; public class BookListPresenter extends BasePresenterImpl implements BookListContract.Presenter { private int threadsNum = 6; private int refreshIndex; private List bookShelfBeans; private int group; private boolean hasUpdate = false; private final CompositeDisposable compositeDisposable = new CompositeDisposable(); @Override public void queryBookShelf(final Boolean needRefresh, final int group) { this.group = group; if (needRefresh) { hasUpdate = false; } Observable.create((ObservableOnSubscribe>) e -> { List bookShelfList; if (group == 0) { bookShelfList = BookshelfHelp.getAllBook(); } else { bookShelfList = BookshelfHelp.getBooksByGroup(group - 1); } e.onNext(bookShelfList == null ? new ArrayList<>() : bookShelfList); e.onComplete(); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver>() { @Override public void onNext(@NonNull List value) { bookShelfBeans = value; mView.refreshBookShelf(bookShelfBeans); if (needRefresh && NetworkUtils.isNetWorkAvailable()) { startRefreshBook(); } } @Override public void onError(Throwable e) { Timber.d(e); } }); } private void downloadAll(int downloadNum, boolean onlyNew) { if (bookShelfBeans == null || mView.getContext() == null) { return; } AsyncTask.execute(() -> { for (BookShelfBean bookShelfBean : new ArrayList<>(bookShelfBeans)) { if (!bookShelfBean.getTag().equals(BookShelfBean.LOCAL_TAG) && (!onlyNew || bookShelfBean.getHasUpdate())) { List chapterBeanList = BookshelfHelp.getChapterList(bookShelfBean.getNoteUrl()); if (chapterBeanList.size() >= bookShelfBean.getDurChapter()) { for (int start = bookShelfBean.getDurChapter(); start < chapterBeanList.size(); start++) { if (!chapterBeanList.get(start).getHasCache(bookShelfBean.getBookInfoBean())) { DownloadBookBean downloadBook = new DownloadBookBean(); downloadBook.setName(bookShelfBean.getBookInfoBean().getName()); downloadBook.setNoteUrl(bookShelfBean.getNoteUrl()); downloadBook.setCoverUrl(bookShelfBean.getBookInfoBean().getCoverUrl()); downloadBook.setStart(start); downloadBook.setEnd(downloadNum > 0 ? Math.min(chapterBeanList.size() - 1, start + downloadNum - 1) : chapterBeanList.size() - 1); downloadBook.setFinalDate(System.currentTimeMillis()); DownloadService.addDownload(mView.getContext(), downloadBook); break; } } } } } }); } private void startRefreshBook() { if (mView.getContext() != null) { threadsNum = mView.getPreferences().getInt(mView.getContext().getString(R.string.pk_threads_num), 6); if (bookShelfBeans != null && bookShelfBeans.size() > 0) { refreshIndex = -1; for (int i = 1; i <= threadsNum; i++) { refreshBookshelf(); } } } } private synchronized void refreshBookshelf() { refreshIndex++; if (refreshIndex < bookShelfBeans.size()) { BookShelfBean bookShelfBean = bookShelfBeans.get(refreshIndex); if (!bookShelfBean.getTag().equals(BookShelfBean.LOCAL_TAG) && bookShelfBean.getAllowUpdate() && bookShelfBean.getGroup() != 3) { int chapterNum = bookShelfBean.getChapterListSize(); bookShelfBean.setLoading(true); mView.refreshBook(bookShelfBean.getNoteUrl()); WebBookModel.getInstance().getChapterList(bookShelfBean) .flatMap(chapterBeanList -> saveBookToShelfO(bookShelfBean, chapterBeanList)) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer() { @Override public void onSubscribe(@NonNull Disposable d) { compositeDisposable.add(d); } @Override public void onNext(@NonNull BookShelfBean value) { if (value.getErrorMsg() != null) { mView.toast(value.getErrorMsg()); value.setErrorMsg(null); } bookShelfBean.setLoading(false); if (chapterNum < bookShelfBean.getChapterListSize()) hasUpdate = true; mView.refreshBook(bookShelfBean.getNoteUrl()); refreshBookshelf(); } @Override public void onError(@NonNull Throwable e) { if (!(e instanceof WebBook.NoSourceThrowable)) { bookShelfBean.setLoading(false); mView.refreshBook(bookShelfBean.getNoteUrl()); refreshBookshelf(); } } @Override public void onComplete() { } }); } else { refreshBookshelf(); } } else if (refreshIndex >= bookShelfBeans.size() + threadsNum - 1) { if (hasUpdate && mView.getPreferences().getBoolean(mView.getContext().getString(R.string.pk_auto_download), false)) { downloadAll(10, true); hasUpdate = false; } queryBookShelf(false, group); } } /** * 保存数据 */ private Observable saveBookToShelfO(BookShelfBean bookShelfBean, List chapterBeanList) { return Observable.create(e -> { if (!chapterBeanList.isEmpty()) { BookshelfHelp.delChapterList(bookShelfBean.getNoteUrl()); BookshelfHelp.saveBookToShelf(bookShelfBean); DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeanList); } e.onNext(bookShelfBean); e.onComplete(); }); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public void attachView(@NonNull IView iView) { super.attachView(iView); RxBus.get().register(this); } @Override public void detachView() { RxBus.get().unregister(this); compositeDisposable.dispose(); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.HAD_ADD_BOOK), @Tag(RxBusTag.HAD_REMOVE_BOOK), @Tag(RxBusTag.UPDATE_BOOK_PROGRESS)}) public void hadAddOrRemoveBook(BookShelfBean bookShelfBean) { queryBookShelf(false, group); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.UPDATE_GROUP)}) public void updateGroup(Integer group) { this.group = group; mView.updateGroup(group); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.REFRESH_BOOK_LIST)}) public void reFlashBookList(Boolean needRefresh) { queryBookShelf(needRefresh, group); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.DOWNLOAD_ALL)}) public void downloadAll(Integer downloadNum) { downloadAll(downloadNum, false); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/BookSourcePresenter.java ================================================ package com.kunfei.bookshelf.presenter; import android.annotation.SuppressLint; import android.graphics.Color; import android.os.AsyncTask; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; import com.google.android.material.snackbar.Snackbar; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.DocumentHelper; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.presenter.contract.BookSourceContract; import com.kunfei.bookshelf.service.CheckSourceService; import java.io.File; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import static android.app.Activity.RESULT_OK; import static android.text.TextUtils.isEmpty; /** * Created by GKF on 2017/12/18. * 书源管理 */ public class BookSourcePresenter extends BasePresenterImpl implements BookSourceContract.Presenter { private BookSourceBean delBookSource; private Snackbar progressSnackBar; @Override public void saveData(BookSourceBean bookSourceBean) { AsyncTask.execute(() -> DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean)); } @Override public void saveData(List bookSourceBeans) { AsyncTask.execute(() -> { if (mView.getSort() == 0) { for (int i = 1; i <= bookSourceBeans.size(); i++) { bookSourceBeans.get(i - 1).setSerialNumber(i); } } DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(bookSourceBeans); }); } @Override public void delData(BookSourceBean bookSourceBean) { this.delBookSource = bookSourceBean; Observable.create((ObservableOnSubscribe) e -> { DbHelper.getDaoSession().getBookSourceBeanDao().delete(bookSourceBean); e.onNext(true); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.getSnackBar(delBookSource.getBookSourceName() + "已删除", Snackbar.LENGTH_LONG) .setAction("恢复", view -> restoreBookSource(delBookSource)) .setActionTextColor(Color.WHITE) .show(); } @Override public void onError(Throwable e) { mView.toast("删除失败"); mView.refreshBookSource(); } }); } @Override public void delData(List bookSourceBeans) { Observable.create((ObservableOnSubscribe) e -> { for (BookSourceBean sourceBean : bookSourceBeans) { DbHelper.getDaoSession().getBookSourceBeanDao().delete(sourceBean); } e.onNext(true); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.toast("删除成功"); mView.refreshBookSource(); mView.setResult(RESULT_OK); } @Override public void onError(Throwable e) { mView.toast("删除失败"); } }); } private void restoreBookSource(BookSourceBean bookSourceBean) { Observable.create((ObservableOnSubscribe) e -> { BookSourceManager.addBookSource(bookSourceBean); e.onNext(true); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.refreshBookSource(); mView.setResult(RESULT_OK); } @Override public void onError(Throwable e) { e.printStackTrace(); } }); } @Override public void importBookSource(String text) { mView.showSnackBar("正在导入书源", Snackbar.LENGTH_INDEFINITE); Observable> observable = BookSourceManager.importSource(text); if (observable != null) { observable.subscribe(getImportObserver()); } else { mView.showSnackBar("格式不对", Snackbar.LENGTH_SHORT); } } @Override public void importBookSourceLocal(String path) { if (TextUtils.isEmpty(path)) { mView.toast(R.string.read_file_error); return; } String json; DocumentFile file; try { file = DocumentFile.fromFile(new File(path)); } catch (Exception e) { mView.toast(path + "无法打开!"); return; } json = DocumentHelper.readString(file); if (!isEmpty(json)) { mView.showSnackBar("正在导入书源", Snackbar.LENGTH_INDEFINITE); importBookSource(json); } else { mView.toast(R.string.read_file_error); } } private MyObserver> getImportObserver() { return new MyObserver>() { @SuppressLint("DefaultLocale") @Override public void onNext(List bookSourceBeans) { if (bookSourceBeans.size() > 0) { mView.refreshBookSource(); mView.showSnackBar(String.format("导入成功%d个书源", bookSourceBeans.size()), Snackbar.LENGTH_SHORT); mView.setResult(RESULT_OK); } else { mView.showSnackBar("格式不对", Snackbar.LENGTH_SHORT); } } @Override public void onError(Throwable e) { mView.showSnackBar(e.getMessage(), Snackbar.LENGTH_SHORT); } }; } @Override public void checkBookSource(List sourceBeans) { CheckSourceService.start(mView.getContext(), sourceBeans); } @Override public void checkFindSource(List sourceBeans) { String TAG_FIND_SOUECE="发现"; Observable.create((ObservableOnSubscribe) e -> { for (BookSourceBean sourceBean : sourceBeans) { String rule=sourceBean.getRuleFindUrl(); if(rule==null) sourceBean.removeGroup(TAG_FIND_SOUECE); else if(rule.trim().length()<1){ sourceBean.removeGroup(TAG_FIND_SOUECE); sourceBean.setRuleFindUrl(null); }else{ sourceBean.addGroup(TAG_FIND_SOUECE); } DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(sourceBean); } e.onNext(true); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.toast(TAG_FIND_SOUECE+"标签验校完成"); mView.refreshBookSource(); mView.setResult(RESULT_OK); } @Override public void onError(Throwable e) { mView.toast("验校失败"); } }); } ///////////////////////////////////////////////// @Override public void attachView(@NonNull IView iView) { super.attachView(iView); RxBus.get().register(this); } @Override public void detachView() { RxBus.get().unregister(this); } /////////////////////RxBus//////////////////////// @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.CHECK_SOURCE_STATE)}) public void upCheckSourceState(String msg) { mView.refreshBookSource(); if (progressSnackBar == null) { progressSnackBar = mView.getSnackBar(msg, Snackbar.LENGTH_INDEFINITE); progressSnackBar.setActionTextColor(Color.WHITE); progressSnackBar.setAction(mView.getContext().getString(R.string.cancel), view -> CheckSourceService.stop(mView.getContext())); } else { progressSnackBar.setText(msg); } if (!progressSnackBar.isShown()) { progressSnackBar.show(); } } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.CHECK_SOURCE_FINISH)}) public void checkSourceFinish(String msg) { mView.showSnackBar(msg, Snackbar.LENGTH_SHORT); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/ChoiceBookPresenter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.presenter; import android.content.Intent; import androidx.annotation.NonNull; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.presenter.contract.ChoiceBookContract; import java.util.ArrayList; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class ChoiceBookPresenter extends BasePresenterImpl implements ChoiceBookContract.Presenter { private final CompositeDisposable compositeDisposable = new CompositeDisposable(); private final String tag; private final String url; private final String title; private int page = 1; private long startThisSearchTime; public ChoiceBookPresenter(final Intent intent) { url = intent.getStringExtra("url"); title = intent.getStringExtra("title"); tag = intent.getStringExtra("tag"); Observable.create((ObservableOnSubscribe>) e -> { List temp = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder().list(); if (temp == null) temp = new ArrayList<>(); e.onNext(temp); }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer>() { @Override public void onSubscribe(@NonNull Disposable d) { compositeDisposable.add(d); } @Override public void onNext(@NonNull List value) { initPage(); toSearchBooks(null); mView.startRefreshAnim(); } @Override public void onError(@NonNull Throwable e) { e.printStackTrace(); } @Override public void onComplete() { } }); } @Override public int getPage() { return page; } @Override public void initPage() { this.page = 1; this.startThisSearchTime = System.currentTimeMillis(); } @Override public void toSearchBooks(String key) { final long tempTime = startThisSearchTime; searchBook(tempTime); } private void searchBook(final long searchTime) { WebBookModel.getInstance().findBook(url, page, tag) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer>() { @Override public void onSubscribe(@NonNull Disposable d) { compositeDisposable.add(d); } @Override public void onNext(@NonNull List value) { if (searchTime == startThisSearchTime) { if (page == 1) { mView.refreshSearchBook(value); mView.refreshFinish(value.size() <= 0); } else { mView.loadMoreSearchBook(value); } page++; } } @Override public void onError(@NonNull Throwable e) { e.printStackTrace(); mView.searchBookError(e.getMessage()); } @Override public void onComplete() { } }); } @Override public String getTitle() { return title; } //////////////////////////////////////////////////////////////////////////////////////////////////// @Override public void attachView(@NonNull IView iView) { super.attachView(iView); RxBus.get().register(this); } @Override public void detachView() { RxBus.get().unregister(this); compositeDisposable.dispose(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/FindBookPresenter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.presenter; import static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE; import android.util.Pair; import android.widget.Toast; import androidx.annotation.NonNull; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.FindKindBean; import com.kunfei.bookshelf.bean.FindKindGroupBean; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule; import com.kunfei.bookshelf.presenter.contract.FindBookContract; import com.kunfei.bookshelf.utils.ACache; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData; import java.util.ArrayList; import java.util.List; import javax.script.SimpleBindings; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.SingleOnSubscribe; import io.reactivex.disposables.Disposable; public class FindBookPresenter extends BasePresenterImpl implements FindBookContract.Presenter { private Disposable disposable; private AnalyzeRule analyzeRule; private String findError = "发现规则语法错误"; @SuppressWarnings("unchecked") @Override public void initData() { if (disposable != null) return; ACache aCache = ACache.get(mView.getContext(), "findCache"); Single.create((SingleOnSubscribe>) e -> { List group = new ArrayList<>(); boolean showAllFind = MApplication.getConfigPreferences().getBoolean("showAllFind", true); List sourceBeans = new ArrayList<>(showAllFind ? BookSourceManager.getAllBookSourceBySerialNumber() : BookSourceManager.getSelectedBookSourceBySerialNumber()); for (BookSourceBean sourceBean : sourceBeans) { Pair> pair = sourceBean.getFindList(); if (pair != null) { group.add(new RecyclerViewData(pair.first, pair.second, false)); } } e.onSuccess(group); }) .compose(RxUtils::toSimpleSingle) .subscribe(new SingleObserver>() { @Override public void onSubscribe(Disposable d) { disposable = d; } @Override public void onSuccess(List recyclerViewData) { mView.upData(recyclerViewData); disposable.dispose(); disposable = null; } @Override public void onError(Throwable e) { e.printStackTrace(); Toast.makeText(mView.getContext(), e.getMessage(), Toast.LENGTH_SHORT).show(); disposable.dispose(); disposable = null; } }); } /** * 执行JS */ private Object evalJS(String jsStr, String baseUrl, BookSourceBean bookSourceBean) throws Exception { SimpleBindings bindings = new SimpleBindings(); bindings.put("java", new AnalyzeRule(null, bookSourceBean)); bindings.put("baseUrl", baseUrl); return SCRIPT_ENGINE.eval(jsStr, bindings); } @Override public void attachView(@NonNull IView iView) { super.attachView(iView); } @Override public void detachView() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/ImportBookPresenter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.presenter; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.bookshelf.bean.LocBookShelfBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.model.ImportBookModel; import com.kunfei.bookshelf.presenter.contract.ImportBookContract; import com.kunfei.bookshelf.utils.RxUtils; import java.io.File; import java.util.List; import io.reactivex.Observable; import io.reactivex.Observer; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; public class ImportBookPresenter extends BasePresenterImpl implements ImportBookContract.Presenter { private CompositeDisposable compositeDisposable = new CompositeDisposable(); @Override public void importBooks(List books) { Observable.fromIterable(books) .flatMap(file -> ImportBookModel.getInstance().importBook(file)) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(LocBookShelfBean value) { if (value.getNew()) { RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value.getBookShelfBean()); } } @Override public void onError(Throwable e) { e.printStackTrace(); mView.addError(e.getMessage()); } @Override public void onComplete() { mView.addSuccess(); } }); } @Override public void detachView() { compositeDisposable.dispose(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/MainPresenter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.presenter; import android.annotation.SuppressLint; import android.text.TextUtils; import androidx.annotation.NonNull; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.dao.BookSourceBeanDao; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.presenter.contract.MainContract; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.StringUtils; import java.util.List; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; public class MainPresenter extends BasePresenterImpl implements MainContract.Presenter { /** * @param bookUrls 如果不包含书源,一行为一本小说的地址。如果包含书源,只解析为一本数,以免url#{{书源}}中书源包含换行 */ @Override public void addBookUrl(String bookUrls) { bookUrls = bookUrls.trim(); if (TextUtils.isEmpty(bookUrls)) return; String[] urls; if (bookUrls.matches("[^\n]+#\\{[\\s\\S]+")) { urls = new String[]{bookUrls}; } else { urls = bookUrls.split("\\n"); } Observable.fromArray(urls) .flatMap(this::addBookUrlO) .compose(RxUtils::toSimpleSingle) .subscribe(new MyObserver() { @Override public void onNext(@NonNull BookShelfBean bookShelfBean) { getBook(bookShelfBean); } @Override public void onError(Throwable e) { mView.toast(e.getMessage()); } }); } private Observable addBookUrlO(String bookUrl) { return Observable.create(e -> { if (StringUtils.isTrimEmpty(bookUrl)) { e.onComplete(); return; } String source = ""; String url = bookUrl; if (url.replaceAll("(\\s|\n)*", "").matches("^.*(#\\{).*")) { String[] string = bookUrl.split("#\\{", 2); source = StringUtils.unCompressJson(string[1]); if (StringUtils.isJsonType(source)) url = string[0]; else source = ""; } BookInfoBean temp = DbHelper.getDaoSession().getBookInfoBeanDao().load(url); if (temp != null) { e.onError(new Throwable("已在书架中")); return; } else { String baseUrl = StringUtils.getBaseUrl(url); BookSourceBean bookSourceBean = DbHelper.getDaoSession().getBookSourceBeanDao().load(baseUrl); // RuleBookUrlPattern推定 考虑有书源规则不完善,需要排除RuleBookUrlPatternt填写.*匹配全部url的情况 if (bookSourceBean == null) { List sourceBeans = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.RuleBookUrlPattern.isNotNull() , BookSourceBeanDao.Properties.RuleBookUrlPattern.notEq("") , BookSourceBeanDao.Properties.RuleBookUrlPattern.notEq(".*") ).list(); for (BookSourceBean sourceBean : sourceBeans) { if (url.matches(sourceBean.getRuleBookUrlPattern())) { bookSourceBean = sourceBean; break; } } } //BookSourceUrl推定 考虑有书源规则不完善,没有填写RuleBookUrlPattern的情况(但是通常会填写bookSourceUrl),因此需要做补充 if (bookSourceBean == null) { String siteUrl = url.replaceFirst("^(http://|https://)?(m\\.|www\\.|web\\.)?", "").replaceFirst("/.*$", ""); List sourceBeans = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.BookSourceUrl.like("%" + siteUrl + "%")).list(); for (BookSourceBean sourceBean : sourceBeans) { //由于RuleBookUrlPattern推定排除了RuleBookUrlPattern为空或者匹配所有字符的情况,因此需要做过杀推定 if (sourceBean.getRuleBookUrlPattern().equals(null)) { bookSourceBean = sourceBean; break; } else if (sourceBean.getRuleBookUrlPattern().replaceAll("\\s", "").length() == 0) { bookSourceBean = sourceBean; break; } if (url.matches(sourceBean.getRuleBookUrlPattern())) { bookSourceBean = sourceBean; break; } } } BookShelfBean bookShelfBean = new BookShelfBean(); bookShelfBean.setNoteUrl(url); if (bookSourceBean != null) { bookShelfBean.setTag(bookSourceBean.getBookSourceUrl()); bookShelfBean.setDurChapter(0); bookShelfBean.setGroup(mView.getGroup() % 4); bookShelfBean.setDurChapterPage(0); bookShelfBean.setFinalDate(System.currentTimeMillis()); e.onNext(bookShelfBean); } else { if (source.length() > 10) { Observable> observable = BookSourceManager.importSource(source); if (observable != null) { observable.subscribe(new MyObserver>() { @SuppressLint("DefaultLocale") @Override public void onNext(@NonNull List bookSourceBeans) { if (bookSourceBeans.size() == 1) { BookSourceBean bean = (bookSourceBeans.get(0)); // BookShelfBean bookShelfBean = new BookShelfBean(); bookShelfBean.setTag(bean.getBookSourceUrl()); // bookShelfBean.setNoteUrl(url); bookShelfBean.setDurChapter(0); bookShelfBean.setGroup(mView.getGroup() % 4); bookShelfBean.setDurChapterPage(0); bookShelfBean.setFinalDate(System.currentTimeMillis()); // e.onNext(bookShelfBean); getBook(bookShelfBean); } else { e.onError(new Throwable("未导入内嵌的书源-" + bookSourceBeans.size())); } } /* @Override public void onError(Throwable e) { mView.toast(e.getLocalizedMessage()); }*/ }); } else { e.onError(new Throwable("未找到内嵌的书源")); } } e.onError(new Throwable("未找到对应书源")); return; } } e.onComplete(); }); } private void getBook(BookShelfBean bookShelfBean) { WebBookModel.getInstance() .getBookInfo(bookShelfBean) .flatMap(bookShelfBean1 -> WebBookModel.getInstance().getChapterList(bookShelfBean1)) .flatMap(chapterBeanList -> saveBookToShelfO(bookShelfBean, chapterBeanList)) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(@NonNull BookShelfBean value) { if (value.getBookInfoBean().getChapterUrl() == null) { mView.toast("添加书籍失败"); } else { //成功 //发送RxBus RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelfBean); mView.toast("添加书籍成功"); } } @Override public void onError(Throwable e) { mView.toast("添加书籍失败" + e.getMessage()); } }); } /** * 保存数据 */ private Observable saveBookToShelfO(BookShelfBean bookShelfBean, List chapterBeanList) { return Observable.create(e -> { BookshelfHelp.saveBookToShelf(bookShelfBean); DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeanList); e.onNext(bookShelfBean); e.onComplete(); }); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public void attachView(@NonNull IView iView) { super.attachView(iView); RxBus.get().register(this); } @Override public void detachView() { RxBus.get().unregister(this); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.IMMERSION_CHANGE)}) public void initImmersionBar(Boolean immersion) { mView.initImmersionBar(); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.RECREATE)}) public void recreate(Boolean recreate) { mView.recreate(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/ReadBookPresenter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.presenter; import static android.text.TextUtils.isEmpty; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.provider.MediaStore; import androidx.annotation.NonNull; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.BookmarkBean; import com.kunfei.bookshelf.bean.DownloadBookBean; import com.kunfei.bookshelf.bean.LocBookShelfBean; import com.kunfei.bookshelf.bean.OpenChapterBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.bean.TwoDataBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.ChangeSourceHelp; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.ImportBookModel; import com.kunfei.bookshelf.model.SavedSource; import com.kunfei.bookshelf.presenter.contract.ReadBookContract; import com.kunfei.bookshelf.service.DownloadService; import com.kunfei.bookshelf.service.ReadAloudService; import java.io.File; import java.util.ArrayList; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class ReadBookPresenter extends BasePresenterImpl implements ReadBookContract.Presenter { private final static int OPEN_FROM_OTHER = 0; public final static int OPEN_FROM_APP = 1; private BookShelfBean bookShelf; private ChangeSourceHelp changeSourceHelp; private List chapterBeanList = new ArrayList<>(); private Disposable changeSourceDisposable; @Override public void initData(Activity activity) { Intent intent = activity.getIntent(); int open_from = intent.getData() != null ? OPEN_FROM_OTHER : OPEN_FROM_APP; open_from = intent.getIntExtra("openFrom", open_from); mView.setAdd(intent.getBooleanExtra("inBookshelf", true)); if (open_from == OPEN_FROM_APP) { loadBook(intent); } else { mView.openBookFromOther(); mView.upMenu(); } } @Override public void loadBook(Intent intent) { Observable.create((ObservableOnSubscribe) e -> { if (bookShelf == null) { String bookKey = intent.getStringExtra("bookKey"); if (!isEmpty(bookKey)) { bookShelf = (BookShelfBean) BitIntentDataManager.getInstance().getData(bookKey); } } if (bookShelf == null && !isEmpty(mView.getNoteUrl())) { bookShelf = BookshelfHelp.getBook(mView.getNoteUrl()); } if (bookShelf == null) { List beans = BookshelfHelp.getAllBook(); if (beans != null && beans.size() > 0) { bookShelf = beans.get(0); } } if (bookShelf != null && chapterBeanList.isEmpty()) { chapterBeanList = BookshelfHelp.getChapterList(bookShelf.getNoteUrl()); } if (bookShelf == null) { e.onError(new Exception("没有书籍")); } else { e.onNext(bookShelf); e.onComplete(); } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(@NonNull BookShelfBean bookShelfBean) { if (isEmpty(bookShelf.getBookInfoBean().getName())) { mView.finish(); } else { mView.startLoadingBook(); mView.upMenu(); } } @Override public void onError(Throwable e) { mView.finish(); } }); } /** * 禁用当前书源 */ public void disableDurBookSource() { try { BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(bookShelf.getTag()); if (bookSourceBean != null) { bookSourceBean.addGroup("禁用"); DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean); mView.toast("已禁用" + bookSourceBean.getBookSourceName()); } } catch (Exception ignored) { } } @Override public BookChapterBean getDurChapter() { if (chapterBeanList.size() == 0) { return null; } if (chapterBeanList.size() > bookShelf.getDurChapter()) { return chapterBeanList.get(bookShelf.getDurChapter()); } return chapterBeanList.get(chapterBeanList.size() - 1); } @Override public void saveBook() { if (bookShelf != null) { AsyncTask.execute(() -> BookshelfHelp.saveBookToShelf(bookShelf)); } } @Override public void saveProgress() { if (bookShelf != null) { AsyncTask.execute(() -> { bookShelf.setFinalDate(System.currentTimeMillis()); bookShelf.setHasUpdate(false); DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelf); RxBus.get().post(RxBusTag.UPDATE_BOOK_PROGRESS, bookShelf); }); } } /** * APP外部打开 */ @Override public void openBookFromOther(Activity activity) { Uri uri = activity.getIntent().getData(); getRealFilePath(activity, uri) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new MyObserver() { @Override public void onNext(@NonNull String value) { ImportBookModel.getInstance().importBook(new File(value)) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new MyObserver() { @Override public void onNext(@NonNull LocBookShelfBean value) { if (value.getNew()) RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value); bookShelf = value.getBookShelfBean(); mView.setAdd(BookshelfHelp.isInBookShelf(bookShelf.getNoteUrl())); mView.startLoadingBook(); } @Override public void onError(Throwable e) { e.printStackTrace(); mView.toast("文本打开失败!"); } }); } @Override public void onError(Throwable e) { e.printStackTrace(); mView.toast("文本打开失败!"); } }); } /** * 下载 */ @Override public void addDownload(int start, int end) { addToShelf(() -> { DownloadBookBean downloadBook = new DownloadBookBean(); downloadBook.setName(bookShelf.getBookInfoBean().getName()); downloadBook.setNoteUrl(bookShelf.getNoteUrl()); downloadBook.setCoverUrl(bookShelf.getBookInfoBean().getCoverUrl()); downloadBook.setStart(start); downloadBook.setEnd(end); downloadBook.setFinalDate(System.currentTimeMillis()); DownloadService.addDownload(mView.getContext(), downloadBook); }); } /** * 换源 */ @Override public void changeBookSource(SearchBookBean searchBook) { if (changeSourceDisposable != null && !changeSourceDisposable.isDisposed()) { changeSourceDisposable.dispose(); } searchBook.setName(bookShelf.getBookInfoBean().getName()); searchBook.setAuthor(bookShelf.getBookInfoBean().getAuthor()); ChangeSourceHelp.changeBookSource(searchBook, bookShelf) .subscribe(new MyObserver>>() { @Override public void onSubscribe(Disposable d) { super.onSubscribe(d); changeSourceDisposable = d; } @Override public void onNext(@NonNull TwoDataBean> value) { RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf); RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value); bookShelf = value.getData1(); chapterBeanList = value.getData2(); mView.changeSourceFinish(bookShelf); String tag = bookShelf.getTag(); try { long currentTime = System.currentTimeMillis(); String bookName = bookShelf.getBookInfoBean().getName(); BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(tag); if (SavedSource.Instance.getBookSource() != null && currentTime - SavedSource.Instance.getSaveTime() < 60000 && SavedSource.Instance.getBookName().equals(bookName)) SavedSource.Instance.getBookSource().increaseWeight(-450); BookSourceManager.saveBookSource(SavedSource.Instance.getBookSource()); SavedSource.Instance.setBookName(bookName); SavedSource.Instance.setSaveTime(currentTime); SavedSource.Instance.setBookSource(bookSourceBean); assert bookSourceBean != null; bookSourceBean.increaseWeightBySelection(); BookSourceManager.saveBookSource(bookSourceBean); } catch (Exception e) { e.printStackTrace(); } } @Override public void onError(Throwable e) { mView.toast(e.getMessage()); mView.changeSourceFinish(null); } }); } @Override public void autoChangeSource() { if (changeSourceHelp == null) { changeSourceHelp = new ChangeSourceHelp(); } changeSourceHelp.autoChange(bookShelf, new ChangeSourceHelp.ChangeSourceListener() { @Override public void finish(BookShelfBean bookShelfBean, List chapterBeanList) { if (!chapterBeanList.isEmpty()) { RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf); RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelfBean); bookShelf = bookShelfBean; ReadBookPresenter.this.chapterBeanList = chapterBeanList; mView.changeSourceFinish(bookShelf); } else { mView.changeSourceFinish(null); } } @Override public void error(Throwable throwable) { mView.toast(throwable.getMessage()); mView.changeSourceFinish(null); } }); } @Override public void saveBookmark(BookmarkBean bookmarkBean) { Observable.create((ObservableOnSubscribe) e -> { BookshelfHelp.saveBookmark(bookmarkBean); e.onNext(bookmarkBean); e.onComplete(); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(); } @Override public void delBookmark(BookmarkBean bookmarkBean) { Observable.create((ObservableOnSubscribe) e -> { BookshelfHelp.delBookmark(bookmarkBean); e.onNext(bookmarkBean); e.onComplete(); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(); } @Override public BookShelfBean getBookShelf() { return bookShelf; } @Override public List getChapterList() { return chapterBeanList; } @Override public void setChapterList(List chapterList) { this.chapterBeanList = chapterList; AsyncTask.execute(() -> DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterList)); } @Override public void addToShelf(final OnAddListener addListener) { if (bookShelf != null) { AsyncTask.execute(() -> { BookshelfHelp.saveBookToShelf(bookShelf); RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelf); mView.setAdd(true); if (addListener != null) { addListener.addSuccess(); } }); } } @Override public void removeFromShelf() { if (bookShelf != null) { Observable.create((ObservableOnSubscribe) e -> { BookshelfHelp.removeFromBookShelf(bookShelf); e.onNext(true); e.onComplete(); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(@NonNull Boolean aBoolean) { RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf); mView.setAdd(true); mView.finish(); } @Override public void onError(Throwable e) { } }); } } private Observable getRealFilePath(final Context context, final Uri uri) { return Observable.create(e -> { String data = ""; if (null != uri) { final String scheme = uri.getScheme(); if (scheme == null) data = uri.getPath(); else if (ContentResolver.SCHEME_FILE.equals(scheme)) { data = uri.getPath(); } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null); if (null != cursor) { if (cursor.moveToFirst()) { int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); if (index > -1) { data = cursor.getString(index); } } cursor.close(); } if ((data == null || data.length() <= 0) && uri.getPath() != null && uri.getPath().contains("/storage/emulated/")) { data = uri.getPath().substring(uri.getPath().indexOf("/storage/emulated/")); } } } e.onNext(data == null ? "" : data); e.onComplete(); }); } @Override public BookSourceBean getBookSource() { if (bookShelf != null) { return BookSourceManager.getBookSourceByUrl(bookShelf.getTag()); } return null; } @Override public void attachView(@NonNull IView iView) { super.attachView(iView); RxBus.get().register(this); } ///////////////////////////////////////////////// @Override public void detachView() { if (changeSourceHelp != null) { changeSourceHelp.stopSearch(); } RxBus.get().unregister(this); } /////////////////////RxBus//////////////////////// @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.MEDIA_BUTTON)}) public void onMediaButton(String command) { if (bookShelf != null) { mView.onMediaButton(command); } } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.UPDATE_READ)}) public void updateRead(Boolean recreate) { mView.refresh(recreate); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.ALOUD_STATE)}) public void upAloudState(ReadAloudService.Status state) { mView.upAloudState(state); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.ALOUD_TIMER)}) public void upAloudTimer(String timer) { mView.upAloudTimer(timer); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.SKIP_TO_CHAPTER)}) public void skipToChapter(OpenChapterBean openChapterBean) { mView.skipToChapter(openChapterBean.getChapterIndex(), openChapterBean.getPageIndex()); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.OPEN_BOOK_MARK)}) public void openBookmark(BookmarkBean bookmarkBean) { mView.showBookmark(bookmarkBean); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.READ_ALOUD_START)}) public void readAloudStart(Integer start) { mView.readAloudStart(start); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.READ_ALOUD_NUMBER)}) public void readAloudLength(Integer start) { mView.readAloudLength(start); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.RECREATE)}) public void recreate(Boolean recreate) { mView.recreate(); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.AUDIO_SIZE)}) public void upAudioSize(Integer audioSize) { mView.upAudioSize(audioSize); BookChapterBean bean = chapterBeanList.get(bookShelf.getDurChapter()); bean.setEnd(Long.valueOf(audioSize)); DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplace(bean); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.AUDIO_DUR)}) public void upAudioDur(Integer audioDur) { mView.upAudioDur(audioDur); } public interface OnAddListener { void addSuccess(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/ReplaceRulePresenter.java ================================================ package com.kunfei.bookshelf.presenter; import android.graphics.Color; import androidx.documentfile.provider.DocumentFile; import com.google.android.material.snackbar.Snackbar; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import com.kunfei.bookshelf.help.DocumentHelper; import com.kunfei.bookshelf.model.ReplaceRuleManager; import com.kunfei.bookshelf.presenter.contract.ReplaceRuleContract; import java.io.File; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import static android.text.TextUtils.isEmpty; /** * Created by GKF on 2017/12/18. * 书源管理 */ public class ReplaceRulePresenter extends BasePresenterImpl implements ReplaceRuleContract.Presenter { @Override public void detachView() { RxBus.get().unregister(this); } @Override public void saveData(List replaceRuleBeans) { Observable.create((ObservableOnSubscribe) e -> { int i = 0; for (ReplaceRuleBean replaceRuleBean : replaceRuleBeans) { i++; replaceRuleBean.setSerialNumber(i + 1); } ReplaceRuleManager.addDataS(replaceRuleBeans); e.onNext(true); e.onComplete(); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(); } @Override public void delData(ReplaceRuleBean replaceRuleBean) { Observable.create((ObservableOnSubscribe) e -> { ReplaceRuleManager.delData(replaceRuleBean); e.onNext(true); e.onComplete(); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean replaceRuleBeans) { mView.refresh(); mView.getSnackBar(replaceRuleBean.getReplaceSummary() + "已删除", Snackbar.LENGTH_LONG) .setAction("恢复", view -> restoreData(replaceRuleBean)) .setActionTextColor(Color.WHITE) .show(); } @Override public void onError(Throwable e) { } }); } @Override public void delData(List replaceRuleBeans) { Observable.create((ObservableOnSubscribe) e -> { ReplaceRuleManager.delDataS(replaceRuleBeans); e.onNext(true); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.toast("删除成功"); mView.refresh(); } @Override public void onError(Throwable e) { mView.toast("删除失败"); } }); } private void restoreData(ReplaceRuleBean replaceRuleBean) { ReplaceRuleManager.saveData(replaceRuleBean) .subscribe(new MySingleObserver() { @Override public void onSuccess(Boolean aBoolean) { mView.refresh(); } }); } @Override public void importDataSLocal(String path) { String json; DocumentFile file = DocumentFile.fromFile(new File(path)); json = DocumentHelper.readString(file); if (!isEmpty(json)) { importDataS(json); } else { mView.toast("文件读取失败"); } } @Override public void importDataS(String text) { Observable observable = ReplaceRuleManager.importReplaceRule(text); if (observable != null) { observable.subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.refresh(); mView.toast("导入成功"); } @Override public void onError(Throwable e) { mView.toast("格式不对"); } }); } else { mView.toast("导入失败"); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/SearchBookPresenter.java ================================================ package com.kunfei.bookshelf.presenter; import android.text.TextUtils; import androidx.annotation.NonNull; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.bean.SearchHistoryBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.dao.SearchHistoryBeanDao; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.SearchBookModel; import com.kunfei.bookshelf.presenter.contract.SearchBookContract; import java.util.ArrayList; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; public class SearchBookPresenter extends BasePresenterImpl implements SearchBookContract.Presenter { private static final int BOOK = 2; private long startThisSearchTime; private String durSearchKey; private List bookShelfS = new ArrayList<>(); //用来比对搜索的书籍是否已经添加进书架 private SearchBookModel searchBookModel; public SearchBookPresenter() { Observable.create((ObservableOnSubscribe>) e -> { List booAll = BookshelfHelp.getAllBook(); e.onNext(booAll == null ? new ArrayList<>() : booAll); e.onComplete(); }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver>() { @Override public void onNext(List value) { bookShelfS.addAll(value); } @Override public void onError(Throwable e) { e.printStackTrace(); } }); //搜索监听 SearchBookModel.OnSearchListener onSearchListener = new SearchBookModel.OnSearchListener() { @Override public void refreshSearchBook() { mView.refreshSearchBook(); } @Override public void refreshFinish(Boolean value) { mView.refreshFinish(value); } @Override public void loadMoreFinish(Boolean value) { mView.loadMoreFinish(value); } @Override public void loadMoreSearchBook(List value) { mView.loadMoreSearchBook(value); } @Override public void searchBookError(Throwable throwable) { mView.searchBookError(throwable); } @Override public int getItemCount() { return mView.getSearchBookAdapter().getICount(); } }; //搜索引擎初始化 if (MApplication.SEARCH_GROUP != null) { List sourceBeanList = BookSourceManager.getEnableSourceByGroup(MApplication.SEARCH_GROUP); if (sourceBeanList.size() > 0) { searchBookModel = new SearchBookModel(onSearchListener, sourceBeanList); } else { searchBookModel = new SearchBookModel(onSearchListener); } } else { searchBookModel = new SearchBookModel(onSearchListener); } } /** * 插入搜索历史 */ public void insertSearchHistory() { final int type = SearchBookPresenter.BOOK; final String content = mView.getEdtContent().getText().toString().trim(); Observable.create((ObservableOnSubscribe) e -> { List data = DbHelper.getDaoSession().getSearchHistoryBeanDao() .queryBuilder() .where(SearchHistoryBeanDao.Properties.Type.eq(type), SearchHistoryBeanDao.Properties.Content.eq(content)) .limit(1) .build().list(); SearchHistoryBean searchHistoryBean; if (null != data && data.size() > 0) { searchHistoryBean = data.get(0); searchHistoryBean.setDate(System.currentTimeMillis()); DbHelper.getDaoSession().getSearchHistoryBeanDao().update(searchHistoryBean); } else { searchHistoryBean = new SearchHistoryBean(type, content, System.currentTimeMillis()); DbHelper.getDaoSession().getSearchHistoryBeanDao().insert(searchHistoryBean); } e.onNext(searchHistoryBean); }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(SearchHistoryBean value) { mView.insertSearchHistorySuccess(value); } @Override public void onError(Throwable e) { e.printStackTrace(); } }); } @Override public void cleanSearchHistory() { final String content = mView.getEdtContent().getText().toString().trim(); Observable.create((ObservableOnSubscribe) e -> { int a = DbHelper.getDb().delete(SearchHistoryBeanDao.TABLENAME, SearchHistoryBeanDao.Properties.Type.columnName + "=? and " + SearchHistoryBeanDao.Properties.Content.columnName + " like ?", new String[]{String.valueOf(SearchBookPresenter.BOOK), "%" + content + "%"}); e.onNext(a); }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Integer value) { if (value > 0) { mView.querySearchHistorySuccess(null); } } @Override public void onError(Throwable e) { e.printStackTrace(); } }); } @Override public void cleanSearchHistory(SearchHistoryBean searchHistoryBean) { Observable.create((ObservableOnSubscribe) e -> { DbHelper.getDaoSession().getSearchHistoryBeanDao().delete(searchHistoryBean); e.onNext(true); e.onComplete(); }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean value) { if (value) { querySearchHistory(mView.getEdtContent().getText().toString().trim()); } } @Override public void onError(Throwable e) { e.printStackTrace(); } }); } @Override public void querySearchHistory(String content) { Observable.create((ObservableOnSubscribe>) e -> { List data = DbHelper.getDaoSession().getSearchHistoryBeanDao() .queryBuilder() .where(SearchHistoryBeanDao.Properties.Type.eq(SearchBookPresenter.BOOK), SearchHistoryBeanDao.Properties.Content.like("%" + content + "%")) .orderDesc(SearchHistoryBeanDao.Properties.Date) .limit(50) .build().list(); e.onNext(data); }).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver>() { @Override public void onNext(List value) { if (null != value) mView.querySearchHistorySuccess(value); } @Override public void onError(Throwable e) { } }); } @Override public int getPage() { return searchBookModel.getPage(); } @Override public void initPage() { searchBookModel.setPage(0); } /** * 搜索 */ @Override public void toSearchBooks(String key, Boolean fromError) { if (key != null) { durSearchKey = key; startThisSearchTime = System.currentTimeMillis(); searchBookModel.setSearchTime(startThisSearchTime); searchBookModel.searchReNew(); } searchBookModel.search(durSearchKey, startThisSearchTime, bookShelfS, fromError); } @Override public void initSearchEngineS(String group) { if (TextUtils.isEmpty(group)) { searchBookModel.initSearchEngineS(BookSourceManager.getSelectedBookSource()); } else { searchBookModel.initSearchEngineS(BookSourceManager.getEnableSourceByGroup(group)); } } /** * 停止搜索 */ @Override public void stopSearch() { searchBookModel.stopSearch(); } @Override public void attachView(@NonNull IView iView) { super.attachView(iView); RxBus.get().register(this); } @Override public void detachView() { RxBus.get().unregister(this); searchBookModel.onDestroy(); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.SEARCH_BOOK)}) public void searchBook(String searchKey) { mView.searchBook(searchKey); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/SourceEditPresenter.java ================================================ package com.kunfei.bookshelf.presenter; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.text.TextUtils; import androidx.annotation.NonNull; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.bean.BookSource3Bean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.presenter.contract.SourceEditContract; import com.kunfei.bookshelf.utils.RxUtils; import java.util.List; import java.util.Objects; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; /** * Created by GKF on 2018/1/28. * 编辑书源 */ public class SourceEditPresenter extends BasePresenterImpl implements SourceEditContract.Presenter { @Override public Observable saveSource(BookSourceBean bookSource, BookSourceBean bookSourceOld) { return Observable.create((ObservableOnSubscribe) e -> { if (!TextUtils.isEmpty(bookSourceOld.getBookSourceUrl()) && !Objects.equals(bookSource.getBookSourceUrl(), bookSourceOld.getBookSourceUrl())) { DbHelper.getDaoSession().getBookSourceBeanDao().delete(bookSourceOld); } BookSourceManager.addBookSource(bookSource); e.onNext(true); }).compose(RxUtils::toSimpleSingle); } @Override public void copySource(String bookSource) { ClipboardManager clipboard = (ClipboardManager) mView.getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = ClipData.newPlainText(null, bookSource); if (clipboard != null) { clipboard.setPrimaryClip(clipData); } } @Override public void pasteSource() { ClipboardManager clipboard = (ClipboardManager) mView.getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = clipboard != null ? clipboard.getPrimaryClip() : null; if (clipData != null && clipData.getItemCount() > 0) { setText(String.valueOf(clipData.getItemAt(0).getText())); } } @Override public void setText(String bookSourceStr) { try { if (bookSourceStr.trim().length() > 5) { mView.setText( mathcSourceBean(bookSourceStr.trim()) ); } else { mView.toast("似乎不是书源内容"); // 理论上这里已经没用了 // Gson gson = new Gson(); // BookSourceBean bookSourceBean = gson.fromJson(bookSourceStr, BookSourceBean.class); // mView.setText(bookSourceBean); } } catch (Exception e) { mView.toast("数据格式不对"); e.printStackTrace(); } } private BookSourceBean mathcSourceBean(String str) { Gson gson = new Gson(); BookSource3Bean bookSource3Bean=new BookSource3Bean(); BookSourceBean bookSource2Bean=new BookSourceBean(); int r2 = 0, r3 = 0; try { if (str.charAt(0) == '[' && str.charAt(str.length() - 1) == ']') { List list = gson.fromJson(str, new TypeToken>() { }.getType()); bookSource3Bean = list.get(0); } else { bookSource3Bean = gson.fromJson(str, BookSource3Bean.class); } r3 = gson.toJson(bookSource3Bean).length(); } catch (Exception e) { e.printStackTrace(); } try { if (str.charAt(0) == '[' && str.charAt(str.length() - 1) == ']') { List list = gson.fromJson(str, new TypeToken>() { }.getType()); bookSource2Bean = list.get(0); } else { bookSource2Bean = gson.fromJson(str, BookSourceBean.class); } r2 = gson.toJson(bookSource2Bean).length(); // r2 r3的计算在调用searchUrl2RuleSearchUrl() 等高级转换方法之前,是简化算法的粗糙的做法 if (r2 > r3) return bookSource2Bean; } catch (Exception e) { e.printStackTrace(); } if (r3 > 0) { mView.toast("导入了阅读3.0书源。如有Bug请及时上报"); return bookSource3Bean.addGroupTag("阅读3.0书源").toBookSourceBean(); } return bookSource2Bean; } @Override public void attachView(@NonNull IView iView) { super.attachView(iView); } @Override public void detachView() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/TxtChapterRulePresenter.java ================================================ package com.kunfei.bookshelf.presenter; import android.graphics.Color; import androidx.documentfile.provider.DocumentFile; import com.google.android.material.snackbar.Snackbar; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BasePresenterImpl; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.TxtChapterRuleBean; import com.kunfei.bookshelf.help.DocumentHelper; import com.kunfei.bookshelf.model.ReplaceRuleManager; import com.kunfei.bookshelf.model.TxtChapterRuleManager; import com.kunfei.bookshelf.presenter.contract.TxtChapterRuleContract; import java.io.File; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import static android.text.TextUtils.isEmpty; public class TxtChapterRulePresenter extends BasePresenterImpl implements TxtChapterRuleContract.Presenter { @Override public void detachView() { RxBus.get().unregister(this); } @Override public void saveData(List txtChapterRuleBeans) { Observable.create((ObservableOnSubscribe) e -> { int i = 0; for (TxtChapterRuleBean ruleBean : txtChapterRuleBeans) { i++; ruleBean.setSerialNumber(i + 1); } DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplaceInTx(txtChapterRuleBeans); e.onNext(true); e.onComplete(); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(); } @Override public void delData(TxtChapterRuleBean txtChapterRuleBean) { Observable.create((ObservableOnSubscribe) e -> { TxtChapterRuleManager.del(txtChapterRuleBean); e.onNext(true); e.onComplete(); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean replaceRuleBeans) { mView.refresh(); mView.getSnackBar(txtChapterRuleBean.getName() + "已删除", Snackbar.LENGTH_LONG) .setAction("恢复", view -> restoreData(txtChapterRuleBean)) .setActionTextColor(Color.WHITE) .show(); } @Override public void onError(Throwable e) { } }); } @Override public void delData(List txtChapterRuleBeans) { Observable.create((ObservableOnSubscribe) e -> { TxtChapterRuleManager.del(txtChapterRuleBeans); e.onNext(true); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.toast("删除成功"); mView.refresh(); } @Override public void onError(Throwable e) { mView.toast("删除失败"); } }); } private void restoreData(TxtChapterRuleBean txtChapterRuleBean) { TxtChapterRuleManager.save(txtChapterRuleBean); mView.refresh(); } @Override public void importDataSLocal(String path) { String json; DocumentFile file = DocumentFile.fromFile(new File(path)); json = DocumentHelper.readString(file); if (!isEmpty(json)) { importDataS(json); } else { mView.toast("文件读取失败"); } } @Override public void importDataS(String text) { Observable observable = ReplaceRuleManager.importReplaceRule(text); if (observable != null) { observable.subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { mView.refresh(); mView.toast("导入成功"); } @Override public void onError(Throwable e) { mView.toast("格式不对"); } }); } else { mView.toast("导入失败"); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookDetailContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import android.content.Intent; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.SearchBookBean; import java.util.List; public interface BookDetailContract { interface Presenter extends IPresenter { void initData(Intent intent); int getOpenFrom(); SearchBookBean getSearchBook(); BookShelfBean getBookShelf(); List getChapterList(); Boolean getInBookShelf(); void initBookFormSearch(SearchBookBean searchBookBean); void getBookShelfInfo(); void addToBookShelf(); void removeFromBookShelf(); void changeBookSource(SearchBookBean searchBookBean); } interface View extends IView { /** * 更新书籍详情UI */ void updateView(); /** * 数据获取失败 */ void getBookShelfError(); void readBook(); void finish(); void toast(String msg); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookListContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import android.content.SharedPreferences; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.BookShelfBean; import java.util.List; public interface BookListContract { interface View extends IView { /** * 刷新书架书籍小说信息 更新UI * * @param bookShelfBeanList 书架 */ void refreshBookShelf(List bookShelfBeanList); void refreshBook(String noteUrl); SharedPreferences getPreferences(); /** * 更新Group */ void updateGroup(Integer group); } interface Presenter extends IPresenter { void queryBookShelf(Boolean needRefresh, int group); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookSourceContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.google.android.material.snackbar.Snackbar; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.BookSourceBean; import java.util.List; public interface BookSourceContract { interface Presenter extends IPresenter { void saveData(BookSourceBean bookSourceBean); void saveData(List bookSourceBeans); void delData(BookSourceBean bookSourceBean); void delData(List bookSourceBeans); void importBookSource(String url); void importBookSourceLocal(String path); void checkBookSource(List sourceBeans); void checkFindSource(List sourceBeans); } interface View extends IView { void refreshBookSource(); Snackbar getSnackBar(String msg, int length); void showSnackBar(String msg, int length); void setResult(int resultCode); int getSort(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/ChoiceBookContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.SearchBookBean; import java.util.List; public interface ChoiceBookContract { interface Presenter extends IPresenter { int getPage(); void initPage(); void toSearchBooks(String key); String getTitle(); } interface View extends IView { void refreshSearchBook(List books); void loadMoreSearchBook(List books); void refreshFinish(Boolean isAll); void loadMoreFinish(Boolean isAll); void searchBookError(String msg); void addBookShelfFailed(String massage); void startRefreshAnim(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/FindBookContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData; import java.util.List; public interface FindBookContract { interface Presenter extends IPresenter { void initData(); } interface View extends IView { void upData(List group); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/ImportBookContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import java.io.File; import java.util.List; public interface ImportBookContract { interface Presenter extends IPresenter { void importBooks(List books); } interface View extends IView { /** * 添加成功 */ void addSuccess(); /** * 添加失败 */ void addError(String msg); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/MainContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; public interface MainContract { interface View extends IView { void initImmersionBar(); /** * 取消弹出框 */ void dismissHUD(); /** * 恢复数据 */ void onRestore(String msg); void recreate(); void toast(String msg); void toast(int strId); int getGroup(); } interface Presenter extends IPresenter { void addBookUrl(String bookUrl); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/ReadBookContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import android.app.Activity; import android.content.Intent; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.BookmarkBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.presenter.ReadBookPresenter; import com.kunfei.bookshelf.service.ReadAloudService; import java.util.List; public interface ReadBookContract { interface View extends IView { String getNoteUrl(); Boolean getAdd(); void setAdd(Boolean isAdd); void changeSourceFinish(BookShelfBean book); void startLoadingBook(); void upMenu(); void openBookFromOther(); void showBookmark(BookmarkBean bookmarkBean); void skipToChapter(int chapterIndex, int pageIndex); void onMediaButton(String cmd); void upAloudState(ReadAloudService.Status state); void upAloudTimer(String timer); void readAloudStart(int start); void readAloudLength(int readAloudLength); void refresh(boolean recreate); void finish(); void recreate(); void upAudioSize(int audioSize); void upAudioDur(int audioDur); } interface Presenter extends IPresenter { void loadBook(Intent intent); BookShelfBean getBookShelf(); List getChapterList(); BookChapterBean getDurChapter(); void setChapterList(List chapterList); void saveBook(); void saveProgress(); void addToShelf(final ReadBookPresenter.OnAddListener Listener); void removeFromShelf(); void initData(Activity activity); void openBookFromOther(Activity activity); void addDownload(int start, int end); void changeBookSource(SearchBookBean searchBookBean); void autoChangeSource(); void saveBookmark(BookmarkBean bookmarkBean); void delBookmark(BookmarkBean bookmarkBean); void disableDurBookSource(); BookSourceBean getBookSource(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/ReplaceRuleContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.google.android.material.snackbar.Snackbar; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import java.util.List; public interface ReplaceRuleContract { interface Presenter extends IPresenter { void saveData(List replaceRuleBeans); void delData(ReplaceRuleBean replaceRuleBean); void delData(List replaceRuleBeans); void importDataSLocal(String uri); void importDataS(String text); } interface View extends IView { void refresh(); Snackbar getSnackBar(String msg, int length); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/SearchBookContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import android.widget.EditText; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.bean.SearchHistoryBean; import com.kunfei.bookshelf.view.adapter.SearchBookAdapter; import java.util.List; public interface SearchBookContract { interface Presenter extends IPresenter { void insertSearchHistory(); void querySearchHistory(String content); void cleanSearchHistory(); void cleanSearchHistory(SearchHistoryBean searchHistoryBean); int getPage(); void initPage(); void toSearchBooks(String key, Boolean fromError); void stopSearch(); void initSearchEngineS(String group); } interface View extends IView { void searchBook(String searchKey); /** * 成功 新增查询记录 */ void insertSearchHistorySuccess(SearchHistoryBean searchHistoryBean); /** * 成功搜索 搜索记录 */ void querySearchHistorySuccess(List datas); /** * 首次查询成功 更新UI */ void refreshSearchBook(); /** * 加载更多书籍成功 更新UI */ void loadMoreSearchBook(List books); /** * 刷新成功 */ void refreshFinish(Boolean isAll); /** * 加载成功 */ void loadMoreFinish(Boolean isAll); /** * 搜索失败 */ void searchBookError(Throwable throwable); /** * 获取搜索内容EditText */ EditText getEdtContent(); /** * @return SearchBookAdapter */ SearchBookAdapter getSearchBookAdapter(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/SourceEditContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.BookSourceBean; import io.reactivex.Observable; public interface SourceEditContract { interface Presenter extends IPresenter { Observable saveSource(BookSourceBean bookSource, BookSourceBean bookSourceOld); void copySource(String bookSource); void pasteSource(); void setText(String bookSourceStr); } interface View extends IView { void setText(BookSourceBean bookSourceBean); String getBookSourceStr(boolean hasFind); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/presenter/contract/TxtChapterRuleContract.java ================================================ package com.kunfei.bookshelf.presenter.contract; import com.google.android.material.snackbar.Snackbar; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.kunfei.bookshelf.bean.TxtChapterRuleBean; import java.util.List; public interface TxtChapterRuleContract { interface Presenter extends IPresenter { void saveData(List txtChapterRuleBeans); void delData(TxtChapterRuleBean txtChapterRuleBean); void delData(List txtChapterRuleBeans); void importDataSLocal(String uri); void importDataS(String text); } interface View extends IView { void refresh(); Snackbar getSnackBar(String msg, int length); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/service/CheckSourceService.java ================================================ package com.kunfei.bookshelf.service; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.IBinder; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.model.task.CheckSourceTask; import com.kunfei.bookshelf.view.activity.BookSourceActivity; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.reactivex.Scheduler; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService; import static com.kunfei.bookshelf.constant.AppConstant.ActionStartService; public class CheckSourceService extends Service { private static final int notificationId = 3333; private static final String ActionOpenActivity = "openActivity"; private List bookSourceBeanList; private int threadsNum; private int checkIndex; private CompositeDisposable compositeDisposable; private ExecutorService executorService; private Scheduler scheduler; private CheckSourceListener checkSourceListener; /** * 启动服务 */ public static void start(Context context, List sourceBeans) { if (sourceBeans.isEmpty()) return; String key = String.valueOf(System.currentTimeMillis()); BitIntentDataManager.getInstance().putData(key, sourceBeans); Intent intent = new Intent(context, CheckSourceService.class); intent.putExtra("data_key", key); intent.setAction(ActionStartService); context.startService(intent); } /** * 停止服务 */ public static void stop(Context context) { Intent intent = new Intent(context, CheckSourceService.class); intent.setAction(ActionDoneService); context.startService(intent); } @Override public void onCreate() { super.onCreate(); SharedPreferences preference = MApplication.getConfigPreferences(); checkSourceListener = new CheckSourceListener() { @Override public void nextCheck() { CheckSourceService.this.nextCheck(); } @Override public void compositeDisposableAdd(Disposable disposable) { compositeDisposable.add(disposable); } @Override public int getCheckIndex() { return checkIndex; } }; threadsNum = preference.getInt(this.getString(R.string.pk_threads_num), 6); executorService = Executors.newFixedThreadPool(threadsNum); scheduler = Schedulers.from(executorService); compositeDisposable = new CompositeDisposable(); updateNotification(0, "正在加载"); } @SuppressWarnings("unchecked") @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); if (action != null) { switch (action) { case ActionDoneService: doneService(); break; case ActionStartService: String key = intent.getStringExtra("data_key"); bookSourceBeanList = (List) BitIntentDataManager.getInstance().getData(key); startCheck(); } } } return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); executorService.shutdown(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private void doneService() { RxBus.get().post(RxBusTag.CHECK_SOURCE_FINISH, "校验完成"); compositeDisposable.dispose(); stopSelf(); } /** * 更新通知 */ private void updateNotification(int state, String msg) { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdReadAloud) .setSmallIcon(R.drawable.ic_network_check) .setOngoing(true) .setContentTitle(getString(R.string.check_book_source)) .setContentText(msg) .setContentIntent(getActivityPendingIntent()); builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getThisServicePendingIntent()); if (bookSourceBeanList != null) { builder.setProgress(bookSourceBeanList.size(), state, false); } builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); Notification notification = builder.build(); startForeground(notificationId, notification); } private PendingIntent getActivityPendingIntent() { Intent intent = new Intent(this, BookSourceActivity.class); intent.setAction(CheckSourceService.ActionOpenActivity); return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent getThisServicePendingIntent() { Intent intent = new Intent(this, this.getClass()); intent.setAction(ActionDoneService); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } public void startCheck() { if (bookSourceBeanList != null && bookSourceBeanList.size() > 0) { RxBus.get().post(RxBusTag.CHECK_SOURCE_STATE, "开始效验"); checkIndex = -1; for (int i = 1; i <= threadsNum; i++) { nextCheck(); } } } private synchronized void nextCheck() { checkIndex++; if (checkIndex > threadsNum) { String msg = String.format(getString(R.string.progress_show), checkIndex - threadsNum, bookSourceBeanList.size()); RxBus.get().post(RxBusTag.CHECK_SOURCE_STATE, msg); updateNotification(checkIndex - threadsNum, msg); } if (checkIndex < bookSourceBeanList.size()) { CheckSourceTask checkSource = new CheckSourceTask(bookSourceBeanList.get(checkIndex), scheduler, checkSourceListener); checkSource.startCheck(); } else { if (checkIndex >= bookSourceBeanList.size() + threadsNum - 1) { doneService(); } } } public interface CheckSourceListener { void nextCheck(); void compositeDisposableAdd(Disposable disposable); int getCheckIndex(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/service/DownloadService.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.service; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.text.TextUtils; import android.util.SparseArray; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.DownloadBookBean; import com.kunfei.bookshelf.bean.DownloadChapterBean; import com.kunfei.bookshelf.model.impl.IDownloadTask; import com.kunfei.bookshelf.model.task.DownloadTaskImpl; import com.kunfei.bookshelf.view.activity.DownloadActivity; import java.util.ArrayList; import java.util.Locale; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.reactivex.Scheduler; import io.reactivex.schedulers.Schedulers; public class DownloadService extends Service { public static final String cancelAction = "cancelAction"; public static final String addDownloadAction = "addDownload"; public static final String removeDownloadAction = "removeDownloadAction"; public static final String progressDownloadAction = "progressDownloadAction"; public static final String obtainDownloadListAction = "obtainDownloadListAction"; public static final String finishDownloadAction = "finishDownloadAction"; private int notificationId = 19901122; private int downloadTaskId = 0; private long currentTime; public static boolean isRunning = false; private ExecutorService executor; private Scheduler scheduler; private int threadsNum; private Handler handler = new Handler(Looper.getMainLooper()); private SparseArray downloadTasks = new SparseArray<>(); @Override public void onCreate() { super.onCreate(); isRunning = true; //创建 Notification.Builder 对象 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdDownload) .setSmallIcon(R.drawable.ic_download) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setOngoing(false) .setContentTitle(getString(R.string.download_offline_t)) .setContentText(getString(R.string.download_offline_s)); //发送通知 Notification notification = builder.build(); startForeground(notificationId, notification); SharedPreferences preferences = getSharedPreferences("CONFIG", 0); threadsNum = preferences.getInt(this.getString(R.string.pk_threads_num), 4); executor = Executors.newFixedThreadPool(threadsNum); scheduler = Schedulers.from(executor); } @Override public void onDestroy() { cancelDownload(); isRunning = false; executor.shutdown(); stopForeground(true); super.onDestroy(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); if (action == null) { finishSelf(); } else { switch (action) { case addDownloadAction: DownloadBookBean downloadBook = intent.getParcelableExtra("downloadBook"); if (downloadBook != null) { addDownload(downloadBook); } break; case removeDownloadAction: String noteUrl = intent.getStringExtra("noteUrl"); removeDownload(noteUrl); break; case cancelAction: finishSelf(); break; case obtainDownloadListAction: refreshDownloadList(); break; } } } return super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private synchronized void addDownload(DownloadBookBean downloadBook) { if (checkDownloadTaskExist(downloadBook)) { return; } downloadTaskId++; new DownloadTaskImpl(downloadTaskId, downloadBook) { @Override public void onDownloadPrepared(DownloadBookBean downloadBook) { if (canStartNextTask()) { startDownload(scheduler); } downloadTasks.put(getId(), this); sendUpDownloadBook(addDownloadAction, downloadBook); } @Override public void onDownloadProgress(DownloadChapterBean chapterBean) { isProgress(chapterBean); } @Override public void onDownloadChange(DownloadBookBean downloadBook) { sendUpDownloadBook(progressDownloadAction, downloadBook); } @Override public void onDownloadError(DownloadBookBean downloadBook) { if (downloadTasks.indexOfValue(this) >= 0) { downloadTasks.remove(getId()); } toast(String.format(Locale.getDefault(), "%s:下载失败", downloadBook.getName())); startNextTaskAfterRemove(downloadBook); } @Override public void onDownloadComplete(DownloadBookBean downloadBook) { if (downloadTasks.indexOfValue(this) >= 0) { downloadTasks.remove(getId()); } startNextTaskAfterRemove(downloadBook); } }; } private void cancelDownload() { for (int i = downloadTasks.size() - 1; i >= 0; i--) { IDownloadTask downloadTask = downloadTasks.valueAt(i); downloadTask.stopDownload(); } } private void removeDownload(String noteUrl) { if (noteUrl == null) { return; } for (int i = downloadTasks.size() - 1; i >= 0; i--) { IDownloadTask downloadTask = downloadTasks.valueAt(i); DownloadBookBean downloadBook = downloadTask.getDownloadBook(); if (downloadBook != null && TextUtils.equals(noteUrl, downloadBook.getNoteUrl())) { downloadTask.stopDownload(); break; } } } private void refreshDownloadList() { ArrayList downloadBookBeans = new ArrayList<>(); for (int i = downloadTasks.size() - 1; i >= 0; i--) { IDownloadTask downloadTask = downloadTasks.valueAt(i); DownloadBookBean downloadBook = downloadTask.getDownloadBook(); if (downloadBook != null) { downloadBookBeans.add(downloadBook); } } if (!downloadBookBeans.isEmpty()) { sendUpDownloadBooks(downloadBookBeans); } } private void startNextTaskAfterRemove(DownloadBookBean downloadBook) { sendUpDownloadBook(removeDownloadAction, downloadBook); handler.postDelayed(() -> { if (downloadTasks.size() == 0) { finishSelf(); } else { startNextTask(); } }, 1000); } private void startNextTask() { if (!canStartNextTask()) { return; } for (int i = 0; i < downloadTasks.size(); i++) { IDownloadTask downloadTask = downloadTasks.valueAt(i); if (!downloadTask.isDownloading()) { downloadTask.startDownload(scheduler); break; } } } private boolean canStartNextTask() { int downloading = 0; for (int i = downloadTasks.size() - 1; i >= 0; i--) { IDownloadTask downloadTask = downloadTasks.valueAt(i); if (downloadTask.isDownloading()) { downloading += 1; } } return downloading < threadsNum; } private synchronized boolean checkDownloadTaskExist(DownloadBookBean downloadBook) { for (int i = downloadTasks.size() - 1; i >= 0; i--) { IDownloadTask downloadTask = downloadTasks.valueAt(i); if (Objects.equals(downloadTask.getDownloadBook(), downloadBook)) { return true; } } return false; } private void sendUpDownloadBook(String action, DownloadBookBean downloadBook) { Intent intent = new Intent(action); intent.putExtra("downloadBook", downloadBook); sendBroadcast(intent); } private void sendUpDownloadBooks(ArrayList downloadBooks) { Intent intent = new Intent(obtainDownloadListAction); intent.putParcelableArrayListExtra("downloadBooks", downloadBooks); sendBroadcast(intent); } private void toast(String msg) { Toast.makeText(DownloadService.this, msg, Toast.LENGTH_SHORT).show(); } private PendingIntent getChancelPendingIntent() { Intent intent = new Intent(this, DownloadService.class); intent.setAction(DownloadService.cancelAction); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private synchronized void isProgress(DownloadChapterBean downloadChapterBean) { if (!isRunning) { return; } if (System.currentTimeMillis() - currentTime < 1000) {//更新太快无法取消 return; } currentTime = System.currentTimeMillis(); Intent mainIntent = new Intent(this, DownloadActivity.class); PendingIntent mainPendingIntent = PendingIntent.getActivity(this, 0, mainIntent, PendingIntent.FLAG_UPDATE_CURRENT); //创建 Notification.Builder 对象 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdDownload) .setSmallIcon(R.drawable.ic_download) //通知栏大图标 .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) //点击通知后自动清除 .setAutoCancel(true) .setContentTitle("正在下载:" + downloadChapterBean.getBookName()) .setContentText(downloadChapterBean.getDurChapterName() == null ? " " : downloadChapterBean.getDurChapterName()) .setContentIntent(mainPendingIntent); builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getChancelPendingIntent()); //发送通知 startForeground(notificationId, builder.build()); } private void finishSelf() { sendBroadcast(new Intent(finishDownloadAction)); stopSelf(); } public static void addDownload(Context context, DownloadBookBean downloadBook) { if (context == null || downloadBook == null) { return; } Intent intent = new Intent(context, DownloadService.class); intent.setAction(addDownloadAction); intent.putExtra("downloadBook", downloadBook); context.startService(intent); } public static void removeDownload(Context context, String noteUrl) { if (noteUrl == null || !isRunning) { return; } Intent intent = new Intent(context, DownloadService.class); intent.setAction(removeDownloadAction); intent.putExtra("noteUrl", noteUrl); context.startService(intent); } public static void cancelDownload(Context context) { if (!isRunning) { return; } Intent intent = new Intent(context, DownloadService.class); intent.setAction(cancelAction); context.startService(intent); } public static void obtainDownloadList(Context context) { if (!isRunning) { return; } Intent intent = new Intent(context, DownloadService.class); intent.setAction(obtainDownloadListAction); context.startService(intent); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/service/MediaButtonIntentReceiver.java ================================================ package com.kunfei.bookshelf.service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.view.KeyEvent; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.AppActivityManager; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.presenter.ReadBookPresenter; import com.kunfei.bookshelf.view.activity.ReadBookActivity; /** * Created by GKF on 2018/1/6. * 监听耳机键 */ public class MediaButtonIntentReceiver extends BroadcastReceiver { public static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); public static boolean handleIntent(final Context context, final Intent intent) { final String intentAction = intent.getAction(); if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) { final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); if (event == null) { return false; } final int keycode = event.getKeyCode(); final int action = event.getAction(); String command = null; switch (keycode) { case KeyEvent.KEYCODE_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: command = ReadAloudService.ActionMediaPlay; break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: command = ReadAloudService.ActionMediaPrev; break; case KeyEvent.KEYCODE_MEDIA_NEXT: command = ReadAloudService.ActionMediaNext; break; default: break; } if (command != null) { if (action == KeyEvent.ACTION_DOWN) { readAloud(context, command); return true; } } } return false; } private static void readAloud(final Context context, String command) { if (!AppActivityManager.getInstance().isExist(ReadBookActivity.class)) { Intent intent = new Intent(context, ReadBookActivity.class); intent.putExtra("openFrom", ReadBookPresenter.OPEN_FROM_APP); intent.putExtra("readAloud", true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); } } else { RxBus.get().post(RxBusTag.MEDIA_BUTTON, command); } } @Override public void onReceive(final Context context, final Intent intent) { if (handleIntent(context, intent) && isOrderedBroadcast()) { abortBroadcast(); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/service/ReadAloudService.java ================================================ package com.kunfei.bookshelf.service; import static android.text.TextUtils.isEmpty; import static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.media.AudioAttributes; import android.media.AudioFocusRequest; import android.media.AudioManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.hwangjr.rxbus.RxBus; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.ExoPlayerHelper; import com.kunfei.bookshelf.help.MediaManager; import com.kunfei.bookshelf.view.activity.ReadBookActivity; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; /** * Created by GKF on 2018/1/2. * 朗读服务 */ public class ReadAloudService extends Service implements Player.Listener { private static final String TAG = ReadAloudService.class.getSimpleName(); public static final String ActionMediaPlay = "mediaBtnPlay"; public static final String ActionMediaPrev = "mediaBtnPrev"; public static final String ActionMediaNext = "mediaBtnNext"; public static final String ActionNewReadAloud = "newReadAloud"; public static final String ActionPauseService = "pauseService"; public static final String ActionResumeService = "resumeService"; private static final String ActionReadActivity = "readActivity"; private static final String ActionSetTimer = "updateTimer"; private static final String ActionSetProgress = "setProgress"; private static final String ActionUITimerStop = "UITimerStop"; private static final String ActionUITimerRemaining = "UITimerRemaining"; private static final int notificationId = 3222; public static final int maxTimeMinute = 360; private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SEEK_TO; public static Boolean running = false; private TextToSpeech textToSpeech; private TextToSpeech textToSpeech_ui; private HashMap mParams; private Boolean ttsInitSuccess = false; private Boolean speak = true; private Boolean pause = false; private final List contentList = new ArrayList<>(); private int nowSpeak; private static int timeMinute = 0; private boolean timerEnable = false; private AudioManager audioManager; private MediaSessionCompat mediaSessionCompat; private AudioFocusChangeListener audioFocusChangeListener; private AudioFocusRequest mFocusRequest; private BroadcastReceiver broadcastReceiver; private SharedPreferences preference; private int speechRate; private String title; private String text; private boolean fadeTts; private final Handler handler = new Handler(); private final Handler mainHandler = new Handler(Looper.getMainLooper()); private Runnable dsRunnable; private Runnable mpRunnable; private MediaManager mediaManager; private int readAloudNumber; private boolean isAudio; private SimpleExoPlayer player; private String audioUrl; private Long progress; /** * 朗读 */ public static void play(Context context, Boolean aloudButton, String content, String title, String text, boolean isAudio, long progress) { Intent readAloudIntent = new Intent(context, ReadAloudService.class); readAloudIntent.setAction(ActionNewReadAloud); readAloudIntent.putExtra("aloudButton", aloudButton); readAloudIntent.putExtra("content", content); readAloudIntent.putExtra("title", title); readAloudIntent.putExtra("text", text); readAloudIntent.putExtra("isAudio", isAudio); readAloudIntent.putExtra("progress", progress); context.startService(readAloudIntent); } /** * @param context 停止 */ public static void stop(Context context) { if (running) { Intent intent = new Intent(context, ReadAloudService.class); intent.setAction(ActionDoneService); context.startService(intent); } } /** * @param context 暂停 */ public static void pause(Context context) { if (running) { Intent intent = new Intent(context, ReadAloudService.class); intent.setAction(ActionPauseService); context.startService(intent); } } /** * @param context 继续 */ public static void resume(Context context) { if (running) { Intent intent = new Intent(context, ReadAloudService.class); intent.setAction(ActionResumeService); context.startService(intent); } } public static void setTimer(Context context, int minute) { if (running) { Intent intent = new Intent(context, ReadAloudService.class); intent.setAction(ActionSetTimer); intent.putExtra("minute", minute); context.startService(intent); } } public static void setProgress(Context context, long progress) { if (running) { Intent intent = new Intent(context, ReadAloudService.class); intent.setAction(ActionSetProgress); intent.putExtra("progress", progress); context.startService(intent); } } public static void tts_ui_timer_stop(Context context) { if (running) { Intent intent = new Intent(context, ReadAloudService.class); intent.setAction(ActionUITimerStop); context.startService(intent); } } public static void tts_ui_timer_remaining(Context context) { if (running) { Intent intent = new Intent(context, ReadAloudService.class); intent.setAction(ActionUITimerRemaining); context.startService(intent); } } @Override public void onCreate() { super.onCreate(); running = true; preference = MApplication.getConfigPreferences(); audioFocusChangeListener = new AudioFocusChangeListener(); audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mediaManager = MediaManager.getInstance(); mediaManager.setStream(TextToSpeech.Engine.DEFAULT_STREAM); fadeTts = preference.getBoolean("fadeTTS", false); dsRunnable = this::doDs; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { initFocusRequest(); } initMediaSession(); initBroadcastReceiver(); mediaSessionCompat.setActive(true); updateMediaSessionPlaybackState(); updateNotification(); mpRunnable = new Runnable() { @Override public void run() { if (player != null) { RxBus.get().post(RxBusTag.AUDIO_DUR, (int) player.getCurrentPosition()); } handler.removeCallbacks(this); handler.postDelayed(this, 1000); } }; } @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); clearTTS(); stopSelf(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); if (action != null) { String sText; switch (action) { case ActionDoneService: stopSelf(); break; case ActionPauseService: pauseReadAloud(true); break; case ActionResumeService: resumeReadAloud(); break; case ActionSetTimer: updateTimer(intent.getIntExtra("minute", 10)); break; case ActionNewReadAloud: newReadAloud(intent.getStringExtra("content"), intent.getBooleanExtra("aloudButton", false), intent.getStringExtra("title"), intent.getStringExtra("text"), intent.getBooleanExtra("isAudio", false), intent.getLongExtra("progress", 0L)); break; case ActionSetProgress: if (player != null) { progress = intent.getLongExtra("progress", 0); player.seekTo(progress); } break; case ActionUITimerStop: sText = getString(R.string.read_aloud_timerstop); textToSpeech_ui.speak(sText, TextToSpeech.QUEUE_FLUSH, mParams); break; case ActionUITimerRemaining: if (timeMinute > 0 && timeMinute <= maxTimeMinute) { if (timeMinute <= 60) { sText = getString(R.string.read_aloud_timerremaining, timeMinute); } else { int hours = timeMinute / 60; int minutes = timeMinute % 60; sText = getString(R.string.read_aloud_timerremaininglong, hours, minutes); } } else { sText = getString(R.string.read_aloud_timerstop); } pauseReadAloud(false); textToSpeech_ui.speak(sText, TextToSpeech.QUEUE_FLUSH, mParams); resumeReadAloud(); break; } } } return super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private void initTTS() { if (textToSpeech == null) textToSpeech = new TextToSpeech(this, new TTSListener()); if (textToSpeech_ui == null) textToSpeech_ui = new TextToSpeech(this, new TTSUIListener()); if (mParams == null) { mParams = new HashMap<>(); mParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM, "3"); } } private void initMediaPlayer() { if (player != null) return; player = new SimpleExoPlayer.Builder(this).build(); player.addListener(this); } @Override public void onPlaybackStateChanged(int playbackState) { switch (playbackState) { case Player.STATE_IDLE: //播放器没有可播放的媒体。 break; case Player.STATE_BUFFERING: //播放器无法立即从当前位置开始播放。这种状态通常需要加载更多数据时发生。 break; case Player.STATE_READY: // 播放器可以立即从当前位置开始播放。如果{@link#getPlayWhenReady()}为true,否则暂停。 if (player.getCurrentPosition() != progress) { player.seekTo(progress); } if (player.getPlayWhenReady()) { speak = true; RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PLAY); } else { speak = false; RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PAUSE); } RxBus.get().post(RxBusTag.AUDIO_SIZE, (int) player.getDuration()); RxBus.get().post(RxBusTag.AUDIO_DUR, (int) player.getCurrentPosition()); handler.postDelayed(mpRunnable, 1000); break; case Player.STATE_ENDED: //播放器完成了播放 handler.removeCallbacks(mpRunnable); RxBus.get().post(RxBusTag.ALOUD_STATE, Status.NEXT); break; } } @Override public void onPlayerError(@NonNull PlaybackException error) { error.printStackTrace(); mainHandler.post(() -> Toast.makeText(ReadAloudService.this, "播放出错:" + error.getLocalizedMessage(), Toast.LENGTH_LONG).show()); pauseReadAloud(true); player.release(); } private void newReadAloud(String content, Boolean aloudButton, String title, String text, boolean isAudio, Long progress) { if (TextUtils.isEmpty(content)) { stopSelf(); return; } this.text = text; this.title = title; this.progress = progress; this.isAudio = isAudio; nowSpeak = 0; readAloudNumber = 0; contentList.clear(); if (isAudio) { initMediaPlayer(); audioUrl = content; } else { initTTS(); String[] splitSpeech = content.split("\n"); for (String aSplitSpeech : splitSpeech) { if (!isEmpty(aSplitSpeech)) { contentList.add(aSplitSpeech); } } } if (aloudButton || speak) { speak = false; pause = false; playTTS(); } } public void playTTS() { updateNotification(); if (isAudio) { try { Uri uri = Uri.parse(audioUrl); MediaSource mediaSource = ExoPlayerHelper.INSTANCE.createMediaSource(uri, null); player.setMediaSource(mediaSource); player.setPlayWhenReady(true); player.prepare(); } catch (Exception e) { e.printStackTrace(); } } else { if (fadeTts) { AsyncTask.execute(() -> mediaManager.fadeInVolume()); handler.postDelayed(this::playTTSN, 200); } else { playTTSN(); } } } public void playTTSN() { if (contentList.size() < 1) { RxBus.get().post(RxBusTag.ALOUD_STATE, Status.NEXT); return; } if (ttsInitSuccess && !speak && requestFocus()) { speak = !speak; RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PLAY); updateNotification(); initSpeechRate(); HashMap map = new HashMap<>(); map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "content"); for (int i = nowSpeak; i < contentList.size(); i++) { if (i == 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_FLUSH, null, "content"); } else { textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_FLUSH, map); } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_ADD, null, "content"); } else { textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_ADD, map); } } } } } public void toTTSSetting() { //跳转到文字转语音设置界面 try { Intent intent = new Intent(); intent.setAction("com.android.settings.TTS_SETTINGS"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } catch (Exception ignored) { } } private void initSpeechRate() { if (speechRate != preference.getInt("speechRate", 10) && !preference.getBoolean("speechRateFollowSys", true)) { speechRate = preference.getInt("speechRate", 10); float speechRateF = (float) speechRate / 10; textToSpeech.setSpeechRate(speechRateF); } } /** * @param pause true 暂停, false 失去焦点 */ private void pauseReadAloud(Boolean pause) { this.pause = pause; speak = false; updateNotification(); updateMediaSessionPlaybackState(); if (isAudio) { if (player != null && player.isPlaying()) player.pause(); } else { if (fadeTts) { AsyncTask.execute(() -> mediaManager.fadeOutVolume()); handler.postDelayed(() -> textToSpeech.stop(), 300); } else { textToSpeech.stop(); } } RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PAUSE); } /** * 恢复朗读 */ private void resumeReadAloud() { updateTimer(0); pause = false; updateNotification(); if (isAudio) { if (player != null && !player.isPlaying()) player.play(); } else { playTTS(); } RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PLAY); } private void updateTimer(int minute) { if (10 == minute) { if (timeMinute < 30) { timeMinute = timeMinute + minute; } else if (timeMinute < 120) { timeMinute = timeMinute + 15; } else if (timeMinute < 180) { timeMinute = timeMinute + 30; } else { timeMinute = timeMinute + 60; } } else { timeMinute = timeMinute + minute; } if (timeMinute > maxTimeMinute) { timerEnable = false; handler.removeCallbacks(dsRunnable); timeMinute = 0; updateNotification(); } else if (timeMinute <= 0) { if (timerEnable) { handler.removeCallbacks(dsRunnable); stopSelf(); } } else { timerEnable = true; updateNotification(); handler.removeCallbacks(dsRunnable); handler.postDelayed(dsRunnable, 60000); } } private void doDs() { if (!pause) { setTimer(this, -1); } } private PendingIntent getReadBookActivityPendingIntent() { Intent intent = new Intent(this, ReadBookActivity.class); intent.setAction(ReadAloudService.ActionReadActivity); return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent getThisServicePendingIntent(String actionStr) { Intent intent = new Intent(this, this.getClass()); intent.setAction(actionStr); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } /** * 更新通知 */ private void updateNotification() { if (text == null) text = getString(R.string.read_aloud_s); String nTitle; if (pause) { nTitle = getString(R.string.read_aloud_pause); } else if (timeMinute > 0 && timeMinute <= maxTimeMinute) { if (timeMinute <= 60) { nTitle = getString(R.string.read_aloud_timer, timeMinute); } else { int hours = timeMinute / 60; int minutes = timeMinute % 60; nTitle = getString(R.string.read_aloud_timerlong, hours, minutes); } } else { nTitle = getString(R.string.read_aloud_t); } nTitle += ": " + title; RxBus.get().post(RxBusTag.ALOUD_TIMER, nTitle); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdReadAloud) .setSmallIcon(R.drawable.ic_volume_up) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_read_book)) .setOngoing(true) .setContentTitle(nTitle) .setContentText(text) .setContentIntent(getReadBookActivityPendingIntent()); if (pause) { builder.addAction(R.drawable.ic_play_24dp, getString(R.string.resume), getThisServicePendingIntent(ActionResumeService)); } else { builder.addAction(R.drawable.ic_pause_24dp, getString(R.string.pause), getThisServicePendingIntent(ActionPauseService)); } builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.stop), getThisServicePendingIntent(ActionDoneService)); builder.addAction(R.drawable.ic_time_add_24dp, getString(R.string.set_timer), getThisServicePendingIntent(ActionSetTimer)); builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSessionCompat.getSessionToken()) .setShowActionsInCompactView(0, 1, 2)); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); Notification notification = builder.build(); startForeground(notificationId, notification); } @Override public void onDestroy() { running = false; super.onDestroy(); stopForeground(true); handler.removeCallbacks(dsRunnable); RxBus.get().post(RxBusTag.ALOUD_STATE, Status.STOP); unRegisterMediaButton(); unregisterReceiver(broadcastReceiver); clearTTS(); } private void clearTTS() { if (player != null) { player.release(); player = null; } if (textToSpeech != null) { if (fadeTts) { AsyncTask.execute(() -> mediaManager.fadeOutVolume()); } textToSpeech.stop(); textToSpeech.shutdown(); textToSpeech = null; } if (textToSpeech_ui != null) { textToSpeech_ui.stop(); textToSpeech_ui.shutdown(); textToSpeech_ui = null; } } private void unRegisterMediaButton() { if (mediaSessionCompat != null) { mediaSessionCompat.setCallback(null); mediaSessionCompat.setActive(false); mediaSessionCompat.release(); } audioManager.abandonAudioFocus(audioFocusChangeListener); } /** * @return 音频焦点 */ private boolean requestFocus() { if (!isAudio) { MediaManager.playSilentSound(this); } int request; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { request = audioManager.requestAudioFocus(mFocusRequest); } else { request = audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); } return (request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); } @RequiresApi(api = Build.VERSION_CODES.O) private void initFocusRequest() { AudioAttributes mPlaybackAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); mFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(mPlaybackAttributes) .setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener(audioFocusChangeListener) .build(); } /** * 初始化MediaSession */ private void initMediaSession() { ComponentName mComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setComponent(mComponent); PendingIntent mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_CANCEL_CURRENT); mediaSessionCompat = new MediaSessionCompat(this, TAG, mComponent, mediaButtonReceiverPendingIntent); mediaSessionCompat.setCallback(new MediaSessionCompat.Callback() { @Override public boolean onMediaButtonEvent(Intent mediaButtonEvent) { return MediaButtonIntentReceiver.handleIntent(ReadAloudService.this, mediaButtonEvent); } }); mediaSessionCompat.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); } private void initBroadcastReceiver() { broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(action)) { pauseReadAloud(true); } } }; IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(broadcastReceiver, intentFilter); } private void updateMediaSessionPlaybackState() { mediaSessionCompat.setPlaybackState( new PlaybackStateCompat.Builder() .setActions(MEDIA_SESSION_ACTIONS) .setState(speak ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, nowSpeak, 1) .build()); } private final class TTSListener implements TextToSpeech.OnInitListener { @Override public void onInit(int i) { if (i == TextToSpeech.SUCCESS) { textToSpeech.setLanguage(Locale.CHINA); textToSpeech.setOnUtteranceProgressListener(new ttsUtteranceListener()); ttsInitSuccess = true; playTTS(); } else { mainHandler.post(() -> Toast.makeText(ReadAloudService.this, getString(R.string.tts_init_failed), Toast.LENGTH_SHORT).show()); ReadAloudService.this.stopSelf(); } } } private final class TTSUIListener implements TextToSpeech.OnInitListener { @Override public void onInit(int i) { if (i == TextToSpeech.SUCCESS) { textToSpeech_ui.setLanguage(Locale.CHINA); } } } /** * 朗读监听 */ private class ttsUtteranceListener extends UtteranceProgressListener { @Override public void onStart(String s) { updateMediaSessionPlaybackState(); RxBus.get().post(RxBusTag.READ_ALOUD_START, readAloudNumber + 1); RxBus.get().post(RxBusTag.READ_ALOUD_NUMBER, readAloudNumber + 1); } @Override public void onDone(String s) { readAloudNumber = readAloudNumber + contentList.get(nowSpeak).length() + 1; nowSpeak = nowSpeak + 1; if (nowSpeak >= contentList.size()) { RxBus.get().post(RxBusTag.ALOUD_STATE, Status.NEXT); } } @Override public void onError(String s) { pauseReadAloud(true); RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PAUSE); } @Override public void onRangeStart(String utteranceId, int start, int end, int frame) { super.onRangeStart(utteranceId, start, end, frame); RxBus.get().post(RxBusTag.READ_ALOUD_NUMBER, readAloudNumber + start); } } class AudioFocusChangeListener implements AudioManager.OnAudioFocusChangeListener { @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: // 重新获得焦点, 可做恢复播放,恢复后台音量的操作 if (!pause) { resumeReadAloud(); } break; case AudioManager.AUDIOFOCUS_LOSS: // 永久丢失焦点除非重新主动获取,这种情况是被其他播放器抢去了焦点, 为避免与其他播放器混音,可将音乐暂停 break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // 暂时丢失焦点,这种情况是被其他应用申请了短暂的焦点,可压低后台音量 if (!pause) { pauseReadAloud(false); } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音), break; } } } public enum Status { PLAY, STOP, PAUSE, NEXT } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/service/ShareService.java ================================================ package com.kunfei.bookshelf.service; import android.app.Activity; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.text.TextUtils; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.web.ShareServer; import java.io.IOException; import java.net.InetAddress; import java.util.List; import static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService; import static com.kunfei.bookshelf.constant.AppConstant.ActionStartService; public class ShareService extends Service { private static boolean isRunning = false; private ShareServer shareServer; private List bookSourceBeans; public static void startThis(Activity activity, List bookSourceBeans) { String key = String.valueOf(System.currentTimeMillis()); BitIntentDataManager.getInstance().putData(key, bookSourceBeans); Intent intent = new Intent(activity, ShareService.class); intent.putExtra("data_key", key); intent.setAction(ActionStartService); activity.startService(intent); } public static void upServer(Activity activity) { if (isRunning) { Intent intent = new Intent(activity, ShareService.class); intent.setAction(ActionStartService); activity.startService(intent); } } public static void stopThis(Context context) { if (isRunning) { Intent intent = new Intent(context, ShareService.class); intent.setAction(ActionDoneService); context.startService(intent); } } @Override public void onCreate() { super.onCreate(); updateNotification("正在启动分享"); new Handler(Looper.getMainLooper()) .post(() -> Toast.makeText(this, "正在启动分享\n具体信息查看通知栏", Toast.LENGTH_SHORT).show()); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); if (action != null) { switch (action) { case ActionStartService: upServer(intent); break; case ActionDoneService: stopSelf(); break; } } } return super.onStartCommand(intent, flags, startId); } @SuppressWarnings("unchecked") private void upServer(Intent intent) { String key = intent.getStringExtra("data_key"); if (!TextUtils.isEmpty(key)) { bookSourceBeans = (List) BitIntentDataManager.getInstance().getData(key); } if (shareServer != null && shareServer.isAlive()) { shareServer.stop(); } shareServer = new ShareServer(65501, () -> bookSourceBeans); InetAddress inetAddress = NetworkUtils.getLocalIPAddress(); if (inetAddress != null) { try { shareServer.start(); isRunning = true; updateNotification(String.format("分享地址:%s", inetAddress.getHostAddress())); } catch (IOException e) { stopSelf(); } } else { stopSelf(); } } @Override public void onDestroy() { super.onDestroy(); isRunning = false; if (shareServer != null && shareServer.isAlive()) { shareServer.stop(); } } private PendingIntent getThisServicePendingIntent() { Intent intent = new Intent(this, this.getClass()); intent.setAction(ActionDoneService); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } /** * 更新通知 */ private void updateNotification(String content) { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdWeb) .setSmallIcon(R.drawable.ic_share) .setOngoing(true) .setContentTitle(getString(R.string.wifi_share)) .setContentText(content); builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getThisServicePendingIntent()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); Notification notification = builder.build(); int notificationId = 1122; startForeground(notificationId, notification); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/service/WebService.java ================================================ package com.kunfei.bookshelf.service; import android.app.Activity; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.web.HttpServer; import com.kunfei.bookshelf.web.WebSocketServer; import java.io.IOException; import java.net.InetAddress; import static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService; import static com.kunfei.bookshelf.constant.AppConstant.ActionStartService; public class WebService extends Service { private static boolean sIsRunning = false; private HttpServer httpServer; private WebSocketServer webSocketServer; /** * Start the web service, return true if the service can be started normally, false if it is started. * * @param context Indicates component context. * @return true if the service can be started normally, false if it is started. */ public static boolean startThis(Context context) { if (sIsRunning) { return false; } else { Intent intent = new Intent(context, WebService.class); intent.setAction(ActionStartService); context.startService(intent); return true; } } public static void upHttpServer(Activity activity) { if (sIsRunning) { Intent intent = new Intent(activity, WebService.class); intent.setAction(ActionStartService); activity.startService(intent); } } @Override public void onCreate() { super.onCreate(); updateNotification(getString(R.string.web_service_starting_hint_short)); new Handler(Looper.getMainLooper()) .post(() -> Toast.makeText(this, R.string.web_service_starting_hint_long, Toast.LENGTH_SHORT).show()); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); if (action != null) { switch (action) { case ActionStartService: upServer(); break; case ActionDoneService: stopSelf(); break; } } } return super.onStartCommand(intent, flags, startId); } private void upServer() { if (httpServer != null && httpServer.isAlive()) { httpServer.stop(); } if (webSocketServer != null && webSocketServer.isAlive()) { webSocketServer.stop(); } int port = getPort(); httpServer = new HttpServer(port); webSocketServer = new WebSocketServer(port + 1); InetAddress inetAddress = NetworkUtils.getLocalIPAddress(); if (inetAddress != null) { try { httpServer.start(); webSocketServer.start(1000 * 30); // 通信超时设置 sIsRunning = true; updateNotification(getString(R.string.http_ip, inetAddress.getHostAddress(), port)); } catch (IOException e) { stopSelf(); } } else { stopSelf(); } } @Override public void onDestroy() { super.onDestroy(); sIsRunning = false; if (httpServer != null && httpServer.isAlive()) { httpServer.stop(); } if (webSocketServer != null && webSocketServer.isAlive()) { webSocketServer.stop(); } } private PendingIntent getThisServicePendingIntent() { Intent intent = new Intent(this, this.getClass()); intent.setAction(ActionDoneService); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private int getPort() { int port = MApplication.getConfigPreferences().getInt("webPort", 1122); if (port > 65530 || port < 1024) { port = 1122; } return port; } /** * 更新通知 */ private void updateNotification(String content) { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdWeb) .setSmallIcon(R.drawable.ic_web_service_noti) .setOngoing(true) .setContentTitle(getString(R.string.web_service)) .setContentText(content); builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getThisServicePendingIntent()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); Notification notification = builder.build(); int notificationId = 1122; startForeground(notificationId, notification); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ACache.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.utils; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.PixelFormat; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.RandomAccessFile; import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import timber.log.Timber; /** * 本地缓存 */ @SuppressWarnings({"unused", "ResultOfMethodCallIgnored", "WeakerAccess"}) public class ACache { public static final int TIME_HOUR = 60 * 60; public static final int TIME_DAY = TIME_HOUR * 24; private static final int MAX_SIZE = 1000 * 1000 * 50; // 50 mb private static final int MAX_COUNT = Integer.MAX_VALUE; // 不限制存放数据的数量 private static Map mInstanceMap = new HashMap<>(); private ACacheManager mCache; private ACache(File cacheDir, long max_size, int max_count) { try { if (!cacheDir.exists() && !cacheDir.mkdirs()) { Timber.tag("ACache").i("can't make dirs in %s", cacheDir.getAbsolutePath()); } mCache = new ACacheManager(cacheDir, max_size, max_count); } catch (Exception ignored) { } } public static ACache get(Context ctx) { return get(ctx, "ACache"); } public static ACache get(Context ctx, String cacheName) { File f = new File(ctx.getFilesDir(), cacheName); return get(f, MAX_SIZE, MAX_COUNT); } public static ACache get(File cacheDir) { return get(cacheDir, MAX_SIZE, MAX_COUNT); } public static ACache get(Context ctx, long max_zise, int max_count) { try { File f = new File(ctx.getFilesDir(), "ACache"); return get(f, max_zise, max_count); } catch (Exception ignored) { } return null; } public static ACache get(File cacheDir, long max_zise, int max_count) { try { ACache manager = mInstanceMap.get(cacheDir.getAbsoluteFile() + myPid()); if (manager == null) { manager = new ACache(cacheDir, max_zise, max_count); mInstanceMap.put(cacheDir.getAbsolutePath() + myPid(), manager); } return manager; } catch (Exception ignored) { } return null; } private static String myPid() { return "_" + android.os.Process.myPid(); } // ======================================= // ============ String数据 读写 ============== // ======================================= /** * 保存 String数据 到 缓存中 * * @param key 保存的key * @param value 保存的String数据 */ public void put(String key, String value) { try { File file = mCache.newFile(key); BufferedWriter out = null; try { out = new BufferedWriter(new FileWriter(file), 1024); out.write(value); } catch (IOException ignored) { } finally { if (out != null) { try { out.flush(); out.close(); } catch (IOException ignored) { } } mCache.put(file); } } catch (Exception ignored) { } } /** * 保存 String数据 到 缓存中 * * @param key 保存的key * @param value 保存的String数据 * @param saveTime 保存的时间,单位:秒 */ public void put(String key, String value, int saveTime) { put(key, Utils.newStringWithDateInfo(saveTime, value)); } /** * 读取 String数据 * * @return String 数据 */ public String getAsString(String key) { File file = mCache.get(key); if (!file.exists()) return null; boolean removeFile = false; try (BufferedReader in = new BufferedReader(new FileReader(file))) { StringBuilder readString = new StringBuilder(); String currentLine; while ((currentLine = in.readLine()) != null) { readString.append(currentLine); } if (!Utils.isDue(readString.toString())) { return Utils.clearDateInfo(readString.toString()); } else { removeFile = true; return null; } } catch (IOException e) { return null; } finally { if (removeFile) remove(key); } } // ======================================= // ========== JSONObject 数据 读写 ========= // ======================================= /** * 保存 JSONObject数据 到 缓存中 * * @param key 保存的key * @param value 保存的JSON数据 */ public void put(String key, JSONObject value) { put(key, value.toString()); } /** * 保存 JSONObject数据 到 缓存中 * * @param key 保存的key * @param value 保存的JSONObject数据 * @param saveTime 保存的时间,单位:秒 */ public void put(String key, JSONObject value, int saveTime) { put(key, value.toString(), saveTime); } /** * 读取JSONObject数据 * * @return JSONObject数据 */ public JSONObject getAsJSONObject(String key) { String JSONString = getAsString(key); try { return new JSONObject(JSONString); } catch (Exception e) { return null; } } // ======================================= // ============ JSONArray 数据 读写 ============= // ======================================= /** * 保存 JSONArray数据 到 缓存中 * * @param key 保存的key * @param value 保存的JSONArray数据 */ public void put(String key, JSONArray value) { put(key, value.toString()); } /** * 保存 JSONArray数据 到 缓存中 * * @param key 保存的key * @param value 保存的JSONArray数据 * @param saveTime 保存的时间,单位:秒 */ public void put(String key, JSONArray value, int saveTime) { put(key, value.toString(), saveTime); } /** * 读取JSONArray数据 * * @return JSONArray数据 */ public JSONArray getAsJSONArray(String key) { String JSONString = getAsString(key); try { return new JSONArray(JSONString); } catch (Exception e) { return null; } } // ======================================= // ============== byte 数据 读写 ============= // ======================================= /** * 保存 byte数据 到 缓存中 * * @param key 保存的key * @param value 保存的数据 */ public void put(String key, byte[] value) { File file = mCache.newFile(key); FileOutputStream out = null; try { out = new FileOutputStream(file); out.write(value); } catch (Exception ignored) { } finally { if (out != null) { try { out.flush(); out.close(); } catch (IOException ignored) { } } mCache.put(file); } } /** * 保存 byte数据 到 缓存中 * * @param key 保存的key * @param value 保存的数据 * @param saveTime 保存的时间,单位:秒 */ public void put(String key, byte[] value, int saveTime) { put(key, Utils.newByteArrayWithDateInfo(saveTime, value)); } /** * 获取 byte 数据 * * @return byte 数据 */ public byte[] getAsBinary(String key) { RandomAccessFile RAFile = null; boolean removeFile = false; try { File file = mCache.get(key); if (!file.exists()) return null; RAFile = new RandomAccessFile(file, "r"); byte[] byteArray = new byte[(int) RAFile.length()]; RAFile.read(byteArray); if (!Utils.isDue(byteArray)) { return Utils.clearDateInfo(byteArray); } else { removeFile = true; return null; } } catch (Exception e) { e.printStackTrace(); return null; } finally { if (RAFile != null) { try { RAFile.close(); } catch (IOException e) { e.printStackTrace(); } } if (removeFile) remove(key); } } // ======================================= // ============= 序列化 数据 读写 =============== // ======================================= /** * 保存 Serializable数据 到 缓存中 * * @param key 保存的key * @param value 保存的value */ public void put(String key, Serializable value) { put(key, value, -1); } /** * 保存 Serializable数据到 缓存中 * * @param key 保存的key * @param value 保存的value * @param saveTime 保存的时间,单位:秒 */ public void put(String key, Serializable value, int saveTime) { ByteArrayOutputStream baos; baos = new ByteArrayOutputStream(); try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(value); byte[] data = baos.toByteArray(); if (saveTime != -1) { put(key, data, saveTime); } else { put(key, data); } } catch (Exception e) { e.printStackTrace(); } } /** * 读取 Serializable数据 * * @return Serializable 数据 */ public Object getAsObject(String key) { byte[] data = getAsBinary(key); if (data != null) { ByteArrayInputStream bais = null; ObjectInputStream ois = null; try { bais = new ByteArrayInputStream(data); ois = new ObjectInputStream(bais); return ois.readObject(); } catch (Exception e) { e.printStackTrace(); return null; } finally { try { if (bais != null) bais.close(); } catch (IOException e) { e.printStackTrace(); } try { if (ois != null) ois.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } // ======================================= // ============== bitmap 数据 读写 ============= // ======================================= /** * 保存 bitmap 到 缓存中 * * @param key 保存的key * @param value 保存的bitmap数据 */ public void put(String key, Bitmap value) { put(key, Utils.Bitmap2Bytes(value)); } /** * 保存 bitmap 到 缓存中 * * @param key 保存的key * @param value 保存的 bitmap 数据 * @param saveTime 保存的时间,单位:秒 */ public void put(String key, Bitmap value, int saveTime) { put(key, Utils.Bitmap2Bytes(value), saveTime); } /** * 读取 bitmap 数据 * * @return bitmap 数据 */ public Bitmap getAsBitmap(String key) { if (getAsBinary(key) == null) { return null; } return Utils.Bytes2Bimap(getAsBinary(key)); } // ======================================= // ============= drawable 数据 读写 ============= // ======================================= /** * 保存 drawable 到 缓存中 * * @param key 保存的key * @param value 保存的drawable数据 */ public void put(String key, Drawable value) { put(key, Utils.drawable2Bitmap(value)); } /** * 保存 drawable 到 缓存中 * * @param key 保存的key * @param value 保存的 drawable 数据 * @param saveTime 保存的时间,单位:秒 */ public void put(String key, Drawable value, int saveTime) { put(key, Utils.drawable2Bitmap(value), saveTime); } /** * 读取 Drawable 数据 * * @return Drawable 数据 */ public Drawable getAsDrawable(String key) { if (getAsBinary(key) == null) { return null; } return Utils.bitmap2Drawable(Utils.Bytes2Bimap(getAsBinary(key))); } /** * 获取缓存文件 * * @return value 缓存的文件 */ public File file(String key) { try { File f = mCache.newFile(key); if (f.exists()) { return f; } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 移除某个key * * @return 是否移除成功 */ public boolean remove(String key) { return mCache.remove(key); } /** * 清除所有数据 */ public void clear() { mCache.clear(); } /** * @author 杨福海(michael) www.yangfuhai.com * @version 1.0 * @title 时间计算工具类 */ private static class Utils { private static final char mSeparator = ' '; /** * 判断缓存的String数据是否到期 * * @return true:到期了 false:还没有到期 */ private static boolean isDue(String str) { return isDue(str.getBytes()); } /** * 判断缓存的byte数据是否到期 * * @return true:到期了 false:还没有到期 */ private static boolean isDue(byte[] data) { try { String[] strs = getDateInfoFromDate(data); if (strs != null && strs.length == 2) { String saveTimeStr = strs[0]; while (saveTimeStr.startsWith("0")) { saveTimeStr = saveTimeStr .substring(1); } long saveTime = Long.valueOf(saveTimeStr); long deleteAfter = Long.valueOf(strs[1]); if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) { return true; } } } catch (Exception e) { e.printStackTrace(); } return false; } private static String newStringWithDateInfo(int second, String strInfo) { return createDateInfo(second) + strInfo; } private static byte[] newByteArrayWithDateInfo(int second, byte[] data2) { byte[] data1 = createDateInfo(second).getBytes(); byte[] retdata = new byte[data1.length + data2.length]; System.arraycopy(data1, 0, retdata, 0, data1.length); System.arraycopy(data2, 0, retdata, data1.length, data2.length); return retdata; } private static String clearDateInfo(String strInfo) { if (strInfo != null && hasDateInfo(strInfo.getBytes())) { strInfo = strInfo.substring(strInfo.indexOf(mSeparator) + 1); } return strInfo; } private static byte[] clearDateInfo(byte[] data) { if (hasDateInfo(data)) { return copyOfRange(data, indexOf(data, mSeparator) + 1, data.length); } return data; } private static boolean hasDateInfo(byte[] data) { return data != null && data.length > 15 && data[13] == '-' && indexOf(data, mSeparator) > 14; } private static String[] getDateInfoFromDate(byte[] data) { if (hasDateInfo(data)) { String saveDate = new String(copyOfRange(data, 0, 13)); String deleteAfter = new String(copyOfRange(data, 14, indexOf(data, mSeparator))); return new String[]{saveDate, deleteAfter}; } return null; } private static int indexOf(byte[] data, char c) { for (int i = 0; i < data.length; i++) { if (data[i] == c) { return i; } } return -1; } private static byte[] copyOfRange(byte[] original, int from, int to) { int newLength = to - from; if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); byte[] copy = new byte[newLength]; System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); return copy; } private static String createDateInfo(int second) { StringBuilder currentTime = new StringBuilder(System.currentTimeMillis() + ""); while (currentTime.length() < 13) { currentTime.insert(0, "0"); } return currentTime + "-" + second + mSeparator; } /* * Bitmap → byte[] */ private static byte[] Bitmap2Bytes(Bitmap bm) { if (bm == null) { return null; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); bm.compress(Bitmap.CompressFormat.PNG, 100, baos); return baos.toByteArray(); } /* * byte[] → Bitmap */ private static Bitmap Bytes2Bimap(byte[] b) { if (b.length == 0) { return null; } return BitmapFactory.decodeByteArray(b, 0, b.length); } /* * Drawable → Bitmap */ private static Bitmap drawable2Bitmap(Drawable drawable) { if (drawable == null) { return null; } // 取 drawable 的长宽 int w = drawable.getIntrinsicWidth(); int h = drawable.getIntrinsicHeight(); // 取 drawable 的颜色格式 Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; // 建立对应 bitmap Bitmap bitmap = Bitmap.createBitmap(w, h, config); // 建立对应 bitmap 的画布 Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, w, h); // 把 drawable 内容画到画布中 drawable.draw(canvas); return bitmap; } /* * Bitmap → Drawable */ @SuppressWarnings("deprecation") private static Drawable bitmap2Drawable(Bitmap bm) { if (bm == null) { return null; } return new BitmapDrawable(bm); } } /** * @author 杨福海(michael) www.yangfuhai.com * @version 1.0 * @title 缓存管理器 */ public class ACacheManager { private final AtomicLong cacheSize; private final AtomicInteger cacheCount; private final long sizeLimit; private final int countLimit; private final Map lastUsageDates = Collections .synchronizedMap(new HashMap<>()); protected File cacheDir; private ACacheManager(File cacheDir, long sizeLimit, int countLimit) { this.cacheDir = cacheDir; this.sizeLimit = sizeLimit; this.countLimit = countLimit; cacheSize = new AtomicLong(); cacheCount = new AtomicInteger(); calculateCacheSizeAndCacheCount(); } /** * 计算 cacheSize和cacheCount */ private void calculateCacheSizeAndCacheCount() { new Thread(() -> { try { int size = 0; int count = 0; File[] cachedFiles = cacheDir.listFiles(); if (cachedFiles != null) { for (File cachedFile : cachedFiles) { size += calculateSize(cachedFile); count += 1; lastUsageDates.put(cachedFile, cachedFile.lastModified()); } cacheSize.set(size); cacheCount.set(count); } } catch (Exception e) { e.printStackTrace(); } }).start(); } private void put(File file) { try { int curCacheCount = cacheCount.get(); while (curCacheCount + 1 > countLimit) { long freedSize = removeNext(); cacheSize.addAndGet(-freedSize); curCacheCount = cacheCount.addAndGet(-1); } cacheCount.addAndGet(1); long valueSize = calculateSize(file); long curCacheSize = cacheSize.get(); while (curCacheSize + valueSize > sizeLimit) { long freedSize = removeNext(); curCacheSize = cacheSize.addAndGet(-freedSize); } cacheSize.addAndGet(valueSize); long currentTime = System.currentTimeMillis(); file.setLastModified(currentTime); lastUsageDates.put(file, currentTime); } catch (Exception e) { e.printStackTrace(); } } private File get(String key) { File file = newFile(key); long currentTime = System.currentTimeMillis(); file.setLastModified(currentTime); lastUsageDates.put(file, currentTime); return file; } private File newFile(String key) { return new File(cacheDir, key.hashCode() + ""); } private boolean remove(String key) { File image = get(key); return image.delete(); } private void clear() { try { lastUsageDates.clear(); cacheSize.set(0); File[] files = cacheDir.listFiles(); if (files != null) { for (File f : files) { f.delete(); } } } catch (Exception e) { e.printStackTrace(); } } /** * 移除旧的文件 */ private long removeNext() { try { if (lastUsageDates.isEmpty()) { return 0; } Long oldestUsage = null; File mostLongUsedFile = null; Set> entries = lastUsageDates.entrySet(); synchronized (lastUsageDates) { for (Entry entry : entries) { if (mostLongUsedFile == null) { mostLongUsedFile = entry.getKey(); oldestUsage = entry.getValue(); } else { Long lastValueUsage = entry.getValue(); if (lastValueUsage < oldestUsage) { oldestUsage = lastValueUsage; mostLongUsedFile = entry.getKey(); } } } } long fileSize = 0; if (mostLongUsedFile != null) { fileSize = calculateSize(mostLongUsedFile); if (mostLongUsedFile.delete()) { lastUsageDates.remove(mostLongUsedFile); } } return fileSize; } catch (Exception e) { e.printStackTrace(); return 0; } } private long calculateSize(File file) { return file.length(); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ActivityExtensions.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils import android.app.Activity import android.graphics.Color import android.os.Build import android.os.Bundle import android.util.DisplayMetrics import android.view.* import android.widget.FrameLayout import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.DialogFragment import com.kunfei.bookshelf.R inline fun AppCompatActivity.showDialogFragment( arguments: Bundle.() -> Unit = {} ) { val dialog = T::class.java.newInstance() val bundle = Bundle() bundle.apply(arguments) dialog.arguments = bundle dialog.show(supportFragmentManager, T::class.simpleName) } fun AppCompatActivity.showDialogFragment(dialogFragment: DialogFragment) { dialogFragment.show(supportFragmentManager, dialogFragment::class.simpleName) } val Activity.windowSize: DisplayMetrics get() { val displayMetrics = DisplayMetrics() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics val insets = windowMetrics.windowInsets .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) displayMetrics.widthPixels = windowMetrics.bounds.width() - insets.left - insets.right displayMetrics.heightPixels = windowMetrics.bounds.height() - insets.top - insets.bottom } else { @Suppress("DEPRECATION") windowManager.defaultDisplay.getMetrics(displayMetrics) } return displayMetrics } fun Activity.fullScreen() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(true) } @Suppress("DEPRECATION") window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE @Suppress("DEPRECATION") window.clearFlags( WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION ) window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) } /** * 设置状态栏颜色 */ fun Activity.setStatusBarColorAuto( @ColorInt color: Int, isTransparent: Boolean, fullScreen: Boolean ) { val isLightBar = ColorUtils.isColorLight(color) if (fullScreen) { if (isTransparent) { window.statusBarColor = Color.TRANSPARENT } else { window.statusBarColor = getCompatColor(R.color.status_bar_bag) } } else { window.statusBarColor = color } setLightStatusBar(isLightBar) } fun Activity.setLightStatusBar(isLightBar: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.insetsController?.let { if (isLightBar) { it.setSystemBarsAppearance( WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS ) } else { it.setSystemBarsAppearance( 0, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS ) } } } @Suppress("DEPRECATION") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val decorView = window.decorView val systemUiVisibility = decorView.systemUiVisibility if (isLightBar) { decorView.systemUiVisibility = systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } else { decorView.systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() } } } /** * 设置导航栏颜色 */ fun Activity.setNavigationBarColorAuto(@ColorInt color: Int) { val isLightBor = ColorUtils.isColorLight(color) window.navigationBarColor = color if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.insetsController?.let { if (isLightBor) { it.setSystemBarsAppearance( WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS ) } else { it.setSystemBarsAppearance( 0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS ) } } } @Suppress("DEPRECATION") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val decorView = window.decorView var systemUiVisibility = decorView.systemUiVisibility systemUiVisibility = if (isLightBor) { systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR } else { systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() } decorView.systemUiVisibility = systemUiVisibility } } /////以下方法需要在View完全被绘制出来之后调用,否则判断不了,在比如 onWindowFocusChanged()方法中可以得到正确的结果///// /** * 返回NavigationBar */ val Activity.navigationBar: View? get() { val viewGroup = (window.decorView as? ViewGroup) ?: return null for (i in 0 until viewGroup.childCount) { val child = viewGroup.getChildAt(i) val childId = child.id if (childId != View.NO_ID && resources.getResourceEntryName(childId) == "navigationBarBackground" ) { return child } } return null } /** * 返回NavigationBar是否存在 */ val Activity.isNavigationBarExist: Boolean get() = navigationBar != null /** * 返回NavigationBar高度 */ val Activity.navigationBarHeight: Int get() { if (isNavigationBarExist) { val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") return resources.getDimensionPixelSize(resourceId) } return 0 } /** * 返回navigationBar位置 */ val Activity.navigationBarGravity: Int get() { val gravity = (navigationBar?.layoutParams as? FrameLayout.LayoutParams)?.gravity return gravity ?: Gravity.BOTTOM } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/BatteryUtil.java ================================================ package com.kunfei.bookshelf.utils; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryManager; public class BatteryUtil { public static int getLevel(Context context) { IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus = context.registerReceiver(null, iFilter); return batteryStatus != null ? batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) : -1; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/BitmapUtil.java ================================================ package com.kunfei.bookshelf.utils; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.net.Uri; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RenderScript; import android.renderscript.ScriptIntrinsicBlur; import android.util.Log; import com.kunfei.bookshelf.MApplication; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @SuppressWarnings({"unused", "WeakerAccess"}) public class BitmapUtil { private static final String TAG = "BitmapUtil"; public static Bitmap getFitSampleBitmap(String file_path, int width, int height) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(file_path, options); options.inSampleSize = getFitInSampleSize(width, height, options); options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(file_path, options); } public static int getFitInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) { int inSampleSize = 1; if (options.outWidth > reqWidth || options.outHeight > reqHeight) { int widthRatio = Math.round((float) options.outWidth / (float) reqWidth); int heightRatio = Math.round((float) options.outHeight / (float) reqHeight); inSampleSize = Math.min(widthRatio, heightRatio); } return inSampleSize; } /** * 通过资源id转化成Bitmap */ public static Bitmap ReadBitmapById(Context context, int resId) { BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inPreferredConfig = Config.ARGB_8888; opt.inPurgeable = true; opt.inInputShareable = true; InputStream is = context.getResources().openRawResource(resId); return BitmapFactory.decodeStream(is, null, opt); } /** * 缩放Bitmap满屏 */ public static Bitmap getBitmap(Bitmap bitmap, int screenWidth, int screenHeight) { int w = bitmap.getWidth(); int h = bitmap.getHeight(); Matrix matrix = new Matrix(); float scale = (float) screenWidth / w; float scale2 = (float) screenHeight / h; // scale = scale < scale2 ? scale : scale2; matrix.postScale(scale, scale); Bitmap bmp = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true); if (!bitmap.equals(bmp) && !bitmap.isRecycled()) { bitmap.recycle(); bitmap = null; } return bmp;// Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true); } /** * 按最大边按一定大小缩放图片 */ public static Bitmap scaleImage(byte[] buffer, float size) { // 获取原图宽度 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inPurgeable = true; options.inInputShareable = true; Bitmap bm; // 计算缩放比例 float reSize = options.outWidth / size; if (options.outWidth < options.outHeight) { reSize = options.outHeight / size; } // 如果是小图则放大 if (reSize <= 1) { int newWidth = 0; int newHeight = 0; if (options.outWidth > options.outHeight) { newWidth = (int) size; newHeight = options.outHeight * (int) size / options.outWidth; } else { newHeight = (int) size; newWidth = options.outWidth * (int) size / options.outHeight; } bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length); bm = scaleImage(bm, newWidth, newHeight); if (bm == null) { Log.e(TAG, "convertToThumb, decode fail:" + null); return null; } return bm; } // 缩放 options.inJustDecodeBounds = false; options.inSampleSize = (int) reSize; bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); if (bm == null) { Log.e(TAG, "convertToThumb, decode fail:" + null); return null; } return bm; } /** * 检查图片是否超过一定值,是则缩小 */ public static Bitmap convertToThumb(byte[] buffer, float size) { // 获取原图宽度 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inPurgeable = true; options.inInputShareable = true; Bitmap bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); // 计算缩放比例 float reSize = options.outWidth / size; if (options.outWidth > options.outHeight) { reSize = options.outHeight / size; } if (reSize <= 0) { reSize = 1; } Log.d(TAG, "convertToThumb, reSize:" + reSize); // 缩放 options.inJustDecodeBounds = false; options.inSampleSize = (int) reSize; if (bm != null && !bm.isRecycled()) { bm.recycle(); bm = null; Log.e(TAG, "convertToThumb, recyle"); } bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); if (bm == null) { Log.e(TAG, "convertToThumb, decode fail:" + null); return null; } return bm; } /** * Bitmap --> byte[] */ private static byte[] readBitmap(Bitmap bmp) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); bmp.compress(Bitmap.CompressFormat.JPEG, 60, stream); try { stream.flush(); stream.close(); } catch (IOException e) { e.printStackTrace(); } return stream.toByteArray(); } /** * Bitmap --> byte[] */ public static byte[] readBitmapFromBuffer(byte[] buffer, float size) { return readBitmap(convertToThumb(buffer, size)); } /** * 以屏幕宽度为基准,显示图片 */ public static Bitmap decodeStream(Context context, Intent data, float size) { Bitmap image = null; try { Uri dataUri = data.getData(); // 获取原图宽度 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inPurgeable = true; options.inInputShareable = true; BitmapFactory.decodeStream(context.getContentResolver() .openInputStream(dataUri), null, options); // 计算缩放比例 float reSize = (int) (options.outWidth / size); if (reSize <= 0) { reSize = 1; } Log.d(TAG, "old-w:" + options.outWidth + ", llyt-w:" + size + ", resize:" + reSize); // 缩放 options.inJustDecodeBounds = false; options.inSampleSize = (int) reSize; image = BitmapFactory.decodeStream(context.getContentResolver() .openInputStream(dataUri), null, options); } catch (Exception e) { e.printStackTrace(); } return image; } /** * 按新的宽高缩放图片 */ public static Bitmap scaleImage(Bitmap bm, int newWidth, int newHeight) { if (bm == null) { return null; } int width = bm.getWidth(); int height = bm.getHeight(); float scaleWidth = ((float) newWidth) / width; float scaleHeight = ((float) newHeight) / height; Matrix matrix = new Matrix(); matrix.postScale(scaleWidth, scaleHeight); Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true); if (!bm.isRecycled()) { bm.recycle(); } return newbm; } /** * 设置固定的宽度,高度随之变化,使图片不会变形 */ public static Bitmap fitBitmap(Bitmap target, int newWidth) { int width = target.getWidth(); int height = target.getHeight(); Matrix matrix = new Matrix(); float scaleWidth = ((float) newWidth) / width; // float scaleHeight = ((float)newHeight) / height; matrix.postScale(scaleWidth, scaleWidth); // Bitmap result = Bitmap.createBitmap(target,0,0,width,height, // matrix,true); Bitmap bmp = Bitmap.createBitmap(target, 0, 0, width, height, matrix, true); if (!target.equals(bmp) && !target.isRecycled()) { target.recycle(); } return bmp;// Bitmap.createBitmap(target, 0, 0, width, height, matrix, // true); } /** * 根据指定的宽高平铺图像 */ public static Bitmap createRepeater(int width, int heigth, Bitmap src) { int countWidth = (width + src.getWidth() - 1) / src.getWidth(); int countHeight = (heigth + src.getHeight() - 1) / src.getHeight(); Bitmap bitmap = Bitmap.createBitmap(width, heigth, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); for (int i = 0; i < countHeight; ++i) { for (int idx = 0; idx < countWidth; ++idx) { canvas.drawBitmap(src, idx * src.getWidth(), i * src.getHeight(), null); } } return bitmap; } /** * 图片的质量压缩方法 */ public static Bitmap compressImage(Bitmap image) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, stream);// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 int options = 100; while (stream.toByteArray().length / 1024 > 100) { // 循环判断如果压缩后图片是否大于100kb,大于继续压缩 stream.reset();// 重置stream即清空stream image.compress(Bitmap.CompressFormat.JPEG, options, stream);// 这里压缩options%,把压缩后的数据存放到baos中 options -= 10;// 每次都减少10 } ByteArrayInputStream isBm = new ByteArrayInputStream(stream.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中 Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片 try { stream.close(); } catch (IOException e) { e.printStackTrace(); } try { isBm.close(); } catch (IOException e) { e.printStackTrace(); } if (!image.isRecycled()) { image.recycle(); } return bitmap; } /** * 图片按比例大小压缩方法(根据Bitmap图片压缩) */ public static Bitmap getImage(Bitmap image) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, stream); if (stream.toByteArray().length / 1024 > 1024) {// 判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出 stream.reset();// 重置stream即清空stream image.compress(Bitmap.CompressFormat.JPEG, 50, stream);// 这里压缩50%,把压缩后的数据存放到baos中 } ByteArrayInputStream isBm; BitmapFactory.Options newOpts = new BitmapFactory.Options(); // 开始读入图片,此时把options.inJustDecodeBounds 设回true了 newOpts.inJustDecodeBounds = true; Bitmap bitmap; newOpts.inJustDecodeBounds = false; int w = newOpts.outWidth; int h = newOpts.outHeight; // 现在主流手机比较多是800*480分辨率,所以高和宽我们设置为 float hh = 800f;// 这里设置高度为800f float ww = 480f;// 这里设置宽度为480f // 缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可 int be = 1;// be=1表示不缩放 if (w > h && w > ww) {// 如果宽度大的话根据宽度固定大小缩放 be = (int) (newOpts.outWidth / ww); } else if (w < h && h > hh) {// 如果高度高的话根据宽度固定大小缩放 be = (int) (newOpts.outHeight / hh); } if (be <= 0) be = 1; newOpts.inSampleSize = be;// 设置缩放比例 // 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了 isBm = new ByteArrayInputStream(stream.toByteArray()); bitmap = BitmapFactory.decodeStream(isBm, null, newOpts); try { isBm.close(); } catch (IOException e) { e.printStackTrace(); } if (!image.isRecycled()) { image.recycle(); } assert bitmap != null; return compressImage(bitmap);// 压缩好比例大小后再进行质量压缩 } /** * 通过资源id转化成Bitmap 全屏显示 */ public static Bitmap ReadBitmapById(Context context, int drawableId, int screenWidth, int screenHight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Config.ARGB_8888; options.inInputShareable = true; options.inPurgeable = true; InputStream stream = context.getResources().openRawResource(drawableId); Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options); assert bitmap != null; return getBitmap(bitmap, screenWidth, screenHight); } /** * 高斯模糊 */ public static Bitmap stackBlur(Bitmap srcBitmap) { if (srcBitmap == null) return null; RenderScript rs = RenderScript.create(MApplication.getInstance()); Bitmap blurredBitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true); //分配用于渲染脚本的内存 Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED); Allocation output = Allocation.createTyped(rs, input.getType()); //加载我们想要使用的特定脚本的实例。 ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); script.setInput(input); //设置模糊半径 script.setRadius(8); //启动 ScriptIntrinsicBlur script.forEach(output); //将输出复制到模糊的位图 output.copyTo(blurredBitmap); return blurredBitmap; } public static Bitmap addBitmap(Bitmap first, Bitmap second,int spacer_mid,int spacer_width,int spacer_top,int spacer_bottom) { int width = Math.max(first.getWidth(),second.getWidth())+spacer_width*2; int height = first.getHeight() + second.getHeight() + spacer_mid+spacer_bottom+spacer_top; Bitmap result = Bitmap.createBitmap(width, height, Config.ARGB_8888); Canvas canvas = new Canvas(result); canvas.drawColor(Color.WHITE); canvas.drawBitmap(first, (width-first.getWidth())/2, spacer_top, null); canvas.drawBitmap(second, (width-second.getWidth())/2,first.getHeight()+spacer_top+spacer_mid, null); return result; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ColorUtils.kt ================================================ package com.kunfei.bookshelf.utils import android.graphics.Color import androidx.annotation.ColorInt import androidx.annotation.FloatRange import java.util.* import kotlin.math.* @Suppress("unused") object ColorUtils { fun intToString(intColor: Int): String { return String.format("#%06X", 0xFFFFFF and intColor) } @JvmStatic fun stripAlpha(@ColorInt color: Int): Int { return -0x1000000 or color } @JvmStatic @ColorInt fun shiftColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 2.0) by: Float): Int { if (by == 1f) return color val alpha = Color.alpha(color) val hsv = FloatArray(3) Color.colorToHSV(color, hsv) hsv[2] *= by // value component return (alpha shl 24) + (0x00ffffff and Color.HSVToColor(hsv)) } @JvmStatic @ColorInt fun darkenColor(@ColorInt color: Int): Int { return shiftColor(color, 0.9f) } @ColorInt fun lightenColor(@ColorInt color: Int): Int { return shiftColor(color, 1.1f) } @JvmStatic fun isColorLight(@ColorInt color: Int): Boolean { val darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255 return darkness < 0.4 } @ColorInt fun invertColor(@ColorInt color: Int): Int { val r = 255 - Color.red(color) val g = 255 - Color.green(color) val b = 255 - Color.blue(color) return Color.argb(Color.alpha(color), r, g, b) } @JvmStatic @ColorInt fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int { val alpha = (Color.alpha(color) * factor).roundToInt() val red = Color.red(color) val green = Color.green(color) val blue = Color.blue(color) return Color.argb(alpha, red, green, blue) } @ColorInt fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int { val a = min(255, max(0, (alpha * 255).toInt())) shl 24 val rgb = 0x00ffffff and baseColor return a + rgb } /** * Taken from CollapsingToolbarLayout's CollapsingTextHelper class. */ fun blendColors(color1: Int, color2: Int, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Int { val inverseRatio = 1f - ratio val a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio val r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio val g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio val b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt()) } /** * 按条件的到随机颜色 * * @param alpha 透明 * @param lower 下边界 * @param upper 上边界 * @return 颜色值 */ fun getRandomColor(alpha: Int, lower: Int, upper: Int): Int { return RandomColor(alpha, lower, upper).color } /** * @return 获取随机色 */ fun getRandomColor(): Int { return RandomColor(255, 80, 200).color } /** * 随机颜色 */ class RandomColor(alpha: Int, lower: Int, upper: Int) { private var alpha: Int = 0 private var lower: Int = 0 private var upper: Int = 0 //随机数是前闭 后开 val color: Int get() { val red = getLower() + Random().nextInt(getUpper() - getLower() + 1) val green = getLower() + Random().nextInt(getUpper() - getLower() + 1) val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1) return Color.argb(getAlpha(), red, green, blue) } init { require(upper > lower) { "must be lower < upper" } setAlpha(alpha) setLower(lower) setUpper(upper) } private fun getAlpha(): Int { return alpha } private fun setAlpha(alpha: Int) { var alpha1 = alpha if (alpha1 > 255) alpha1 = 255 if (alpha1 < 0) alpha1 = 0 this.alpha = alpha1 } private fun getLower(): Int { return lower } private fun setLower(lower: Int) { var lower1 = lower if (lower1 < 0) lower1 = 0 this.lower = lower1 } private fun getUpper(): Int { return upper } private fun setUpper(upper: Int) { var upper1 = upper if (upper1 > 255) upper1 = 255 this.upper = upper1 } } fun argb(R: Int, G: Int, B: Int): Int { return argb(Byte.MAX_VALUE.toInt(), R, G, B) } fun argb(A: Int, R: Int, G: Int, B: Int): Int { val colorByteArr = byteArrayOf(A.toByte(), R.toByte(), G.toByte(), B.toByte()) return byteArrToInt(colorByteArr) } fun rgb(argb: Int): IntArray { return intArrayOf(argb shr 16 and 0xFF, argb shr 8 and 0xFF, argb and 0xFF) } fun byteArrToInt(colorByteArr: ByteArray): Int { return ((colorByteArr[0].toInt() shl 24) + (colorByteArr[1].toInt() and 0xFF shl 16) + (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF)) } fun rgb2lab(R: Int, G: Int, B: Int): IntArray { val x: Float val y: Float val z: Float val fx: Float val fy: Float val fz: Float val xr: Float val yr: Float val zr: Float val eps = 216f / 24389f val k = 24389f / 27f val xr1 = 0.964221f // reference white D50 val yr1 = 1.0f val zr1 = 0.825211f // RGB to XYZ var r: Float = R / 255f //R 0..1 var g: Float = G / 255f //G 0..1 var b: Float = B / 255f //B 0..1 // assuming sRGB (D65) r = if (r <= 0.04045) r / 12 else ((r + 0.055) / 1.055).pow(2.4).toFloat() g = if (g <= 0.04045) g / 12 else ((g + 0.055) / 1.055).pow(2.4).toFloat() b = if (b <= 0.04045) b / 12 else ((b + 0.055) / 1.055).pow(2.4).toFloat() x = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b // XYZ to Lab xr = x / xr1 yr = y / yr1 zr = z / zr1 fx = if (xr > eps) xr.toDouble().pow(1 / 3.0) .toFloat() else ((k * xr + 16.0) / 116.0).toFloat() fy = if (yr > eps) yr.toDouble().pow(1 / 3.0) .toFloat() else ((k * yr + 16.0) / 116.0).toFloat() fz = if (zr > eps) zr.toDouble().pow(1 / 3.0) .toFloat() else ((k * zr + 16.0) / 116).toFloat() val ls: Float = 116 * fy - 16 val `as`: Float = 500 * (fx - fy) val bs: Float = 200 * (fy - fz) val lab = IntArray(3) lab[0] = (2.55 * ls + .5).toInt() lab[1] = (`as` + .5).toInt() lab[2] = (bs + .5).toInt() return lab } /** * Computes the difference between two RGB colors by converting them to the L*a*b scale and * comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76} */ fun getColorDifference(a: Int, b: Int): Double { val r1: Int = Color.red(a) val g1: Int = Color.green(a) val b1: Int = Color.blue(a) val r2: Int = Color.red(b) val g2: Int = Color.green(b) val b2: Int = Color.blue(b) val lab1 = rgb2lab(r1, g1, b1) val lab2 = rgb2lab(r2, g2, b2) return sqrt( (lab2[0] - lab1[0].toDouble()) .pow(2.0) + (lab2[1] - lab1[1].toDouble()) .pow(2.0) + (lab2[2] - lab1[2].toDouble()) .pow(2.0) ) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ContextExtensions.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils import android.annotation.SuppressLint import android.app.Activity import android.app.PendingIntent import android.app.PendingIntent.* import android.app.Service import android.content.* import android.content.pm.PackageManager import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.drawable.Drawable import android.net.Uri import android.os.BatteryManager import android.provider.Settings import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.preference.PreferenceManager import com.kunfei.bookshelf.R import java.io.File inline fun Context.startActivity(configIntent: Intent.() -> Unit = {}) { val intent = Intent(this, A::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.apply(configIntent) startActivity(intent) } inline fun Context.startService(configIntent: Intent.() -> Unit = {}) { startService(Intent(this, T::class.java).apply(configIntent)) } inline fun Context.stopService() { stopService(Intent(this, T::class.java)) } @SuppressLint("UnspecifiedImmutableFlag") inline fun Context.servicePendingIntent( action: String, configIntent: Intent.() -> Unit = {} ): PendingIntent? { val intent = Intent(this, T::class.java) intent.action = action configIntent.invoke(intent) return getService(this, 0, intent, FLAG_UPDATE_CURRENT) } @SuppressLint("UnspecifiedImmutableFlag") inline fun Context.activityPendingIntent( action: String, configIntent: Intent.() -> Unit = {} ): PendingIntent? { val intent = Intent(this, T::class.java) intent.action = action configIntent.invoke(intent) return getActivity(this, 0, intent, FLAG_UPDATE_CURRENT) } @SuppressLint("UnspecifiedImmutableFlag") inline fun Context.broadcastPendingIntent( action: String, configIntent: Intent.() -> Unit = {} ): PendingIntent? { val intent = Intent(this, T::class.java) intent.action = action configIntent.invoke(intent) return getBroadcast(this, 0, intent, FLAG_CANCEL_CURRENT) } val Context.defaultSharedPreferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(this) fun Context.getPrefBoolean(key: String, defValue: Boolean = false) = defaultSharedPreferences.getBoolean(key, defValue) fun Context.putPrefBoolean(key: String, value: Boolean = false) = defaultSharedPreferences.edit { putBoolean(key, value) } fun Context.getPrefInt(key: String, defValue: Int = 0) = defaultSharedPreferences.getInt(key, defValue) fun Context.putPrefInt(key: String, value: Int) = defaultSharedPreferences.edit { putInt(key, value) } fun Context.getPrefLong(key: String, defValue: Long = 0L) = defaultSharedPreferences.getLong(key, defValue) fun Context.putPrefLong(key: String, value: Long) = defaultSharedPreferences.edit { putLong(key, value) } fun Context.getPrefString(key: String, defValue: String? = null) = defaultSharedPreferences.getString(key, defValue) fun Context.putPrefString(key: String, value: String?) = defaultSharedPreferences.edit { putString(key, value) } fun Context.getPrefStringSet( key: String, defValue: MutableSet? = null ): MutableSet? = defaultSharedPreferences.getStringSet(key, defValue) fun Context.putPrefStringSet(key: String, value: MutableSet) = defaultSharedPreferences.edit { putStringSet(key, value) } fun Context.removePref(key: String) = defaultSharedPreferences.edit { remove(key) } fun Context.getCompatColor(@ColorRes id: Int): Int = ContextCompat.getColor(this, id) fun Context.getCompatDrawable(@DrawableRes id: Int): Drawable? = ContextCompat.getDrawable(this, id) fun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = ContextCompat.getColorStateList(this, id) /** * 系统息屏时间 */ val Context.sysScreenOffTime: Int get() { var screenOffTime = 0 try { screenOffTime = Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT) } catch (e: Exception) { e.printStackTrace() } return screenOffTime } val Context.statusBarHeight: Int get() { val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") return resources.getDimensionPixelSize(resourceId) } val Context.navigationBarHeight: Int get() { val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") return resources.getDimensionPixelSize(resourceId) } fun Context.share(text: String, title: String = getString(R.string.share)) { kotlin.runCatching { val intent = Intent(Intent.ACTION_SEND) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.putExtra(Intent.EXTRA_SUBJECT, title) intent.putExtra(Intent.EXTRA_TEXT, text) intent.type = "text/plain" startActivity(Intent.createChooser(intent, title)) } } fun Context.sendToClip(text: String) { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager val clipData = ClipData.newPlainText(null, text) clipboard?.let { clipboard.setPrimaryClip(clipData) longToastOnUi(R.string.copy_complete) } } fun Context.getClipText(): String? { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? clipboard?.primaryClip?.let { if (it.itemCount > 0) { return it.getItemAt(0).text.toString().trim() } } return null } fun Context.sendMail(mail: String) { try { val intent = Intent(Intent.ACTION_SENDTO) intent.data = Uri.parse("mailto:$mail") intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } catch (e: Exception) { toastOnUi(e.localizedMessage ?: "Error") } } /** * 系统是否暗色主题 */ fun Context.sysIsDarkMode(): Boolean { val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK return mode == Configuration.UI_MODE_NIGHT_YES } /** * 获取电量 */ val Context.sysBattery: Int get() { val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) val batteryStatus = registerReceiver(null, iFilter) return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 } val Context.externalFiles: File get() = this.getExternalFilesDir(null) ?: this.filesDir val Context.externalCache: File get() = this.externalCacheDir ?: this.cacheDir fun Context.openUrl(url: String) { openUrl(Uri.parse(url)) } fun Context.openUrl(uri: Uri) { val intent = Intent(Intent.ACTION_VIEW) intent.data = uri intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) if (intent.resolveActivity(packageManager) != null) { try { startActivity(intent) } catch (e: Exception) { toastOnUi(e.localizedMessage ?: "open url error") } } else { try { startActivity(Intent.createChooser(intent, "请选择浏览器")) } catch (e: Exception) { toastOnUi(e.localizedMessage ?: "open url error") } } } val Context.channel: String get() { try { val pm = packageManager val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA) return appInfo.metaData.getString("channel") ?: "" } catch (e: Exception) { e.printStackTrace() } return "" } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ConvertUtils.kt ================================================ package com.kunfei.bookshelf.utils import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import java.io.BufferedReader import java.io.InputStream import java.io.InputStreamReader import java.text.DecimalFormat /** * 数据类型转换、单位转换 * * @author 李玉江[QQ:1023694760] * @since 2014-4-18 */ @Suppress("MemberVisibilityCanBePrivate") object ConvertUtils { const val GB: Long = 1073741824 const val MB: Long = 1048576 const val KB: Long = 1024 fun toInt(obj: Any): Int { return kotlin.runCatching { Integer.parseInt(obj.toString()) }.getOrDefault(-1) } fun toInt(bytes: ByteArray): Int { var result = 0 var byte: Byte for (i in bytes.indices) { byte = bytes[i] result += (byte.toInt() and 0xFF).shl(8 * i) } return result } fun toFloat(obj: Any): Float { return kotlin.runCatching { java.lang.Float.parseFloat(obj.toString()) }.getOrDefault(-1f) } fun toString(objects: Array, tag: String): String { val sb = StringBuilder() for (`object` in objects) { sb.append(`object`) sb.append(tag) } return sb.toString() } @JvmOverloads fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? { var bitmap: Bitmap? = null if (bytes.isNotEmpty()) { kotlin.runCatching { val options = BitmapFactory.Options() // 设置让解码器以最佳方式解码 options.inPreferredConfig = null if (width > 0 && height > 0) { options.outWidth = width options.outHeight = height } bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) bitmap!!.density = 96// 96 dpi } } return bitmap } private fun toDrawable(bitmap: Bitmap?): Drawable? { return if (bitmap == null) null else BitmapDrawable(Resources.getSystem(), bitmap) } fun toDrawable(bytes: ByteArray): Drawable? { return toDrawable(toBitmap(bytes)) } fun toFileSizeString(fileSize: Long): String { val df = DecimalFormat("0.00") val fileSizeString: String = when { fileSize < KB -> fileSize.toString() + "B" fileSize < MB -> df.format(fileSize.toDouble() / KB) + "K" fileSize < GB -> df.format(fileSize.toDouble() / MB) + "M" else -> df.format(fileSize.toDouble() / GB) + "G" } return fileSizeString } @JvmOverloads fun toString(`is`: InputStream, charset: String = "utf-8"): String { val sb = StringBuilder() kotlin.runCatching { val reader = BufferedReader(InputStreamReader(`is`, charset)) while (true) { val line = reader.readLine() if (line == null) { break } else { sb.append(line).append("\n") } } reader.close() `is`.close() } return sb.toString() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/DensityUtil.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.utils; import android.app.Activity; import android.content.Context; import android.graphics.Point; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Display; import android.view.WindowManager; import java.lang.reflect.Method; @SuppressWarnings({"unused", "WeakerAccess"}) public class DensityUtil { /** * dp转px */ public static int dp2px(Context context, float dpVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics()); } /** * sp转px */ public static int sp2px(Context context, float spVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, context.getResources().getDisplayMetrics()); } /** * px转dp */ public static float px2dp(Context context, float pxVal) { final float scale = context.getResources().getDisplayMetrics().density; return (pxVal / scale); } /** * px转sp */ public static float px2sp(Context context, float pxVal) { return (pxVal / context.getResources().getDisplayMetrics().scaledDensity); } public static Point getDisplayPoint(Context context) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = windowManager.getDefaultDisplay(); DisplayMetrics displayMetrics = new DisplayMetrics(); @SuppressWarnings("rawtypes") Class c; try { c = Class.forName("android.view.Display"); @SuppressWarnings("unchecked") Method method = c.getMethod("getRealMetrics", DisplayMetrics.class); method.invoke(display, displayMetrics); return new Point(displayMetrics.widthPixels, displayMetrics.heightPixels); } catch (Exception e) { e.printStackTrace(); } DisplayMetrics dm = new DisplayMetrics(); ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(dm); return new Point(dm.widthPixels, dm.heightPixels); } public static int getWindowWidth(Context context) { WindowManager wm = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); return wm.getDefaultDisplay().getWidth(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/DialogExtensions.kt ================================================ package com.kunfei.bookshelf.utils import android.view.WindowManager import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.kunfei.bookshelf.utils.theme.ThemeStore import com.kunfei.bookshelf.utils.theme.filletBackground fun AlertDialog.applyTint(): AlertDialog { window?.setBackgroundDrawable(context.filletBackground) val colorStateList = Selector.colorBuild() .setDefaultColor(ThemeStore.accentColor(context)) .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(context))) .create() if (getButton(AlertDialog.BUTTON_NEGATIVE) != null) { getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList) } if (getButton(AlertDialog.BUTTON_POSITIVE) != null) { getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList) } if (getButton(AlertDialog.BUTTON_NEUTRAL) != null) { getButton(AlertDialog.BUTTON_NEUTRAL).setTextColor(colorStateList) } return this } fun AlertDialog.requestInputMethod() { window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) } fun DialogFragment.setLayout(widthMix: Float, heightMix: Float) { val dm = requireActivity().windowSize dialog?.window?.setLayout( (dm.widthPixels * widthMix).toInt(), (dm.heightPixels * heightMix).toInt() ) } fun DialogFragment.setLayout(width: Int, heightMix: Float) { val dm = requireActivity().windowSize dialog?.window?.setLayout( width, (dm.heightPixels * heightMix).toInt() ) } fun DialogFragment.setLayout(widthMix: Float, height: Int) { val dm = requireActivity().windowSize dialog?.window?.setLayout( (dm.widthPixels * widthMix).toInt(), height ) } fun DialogFragment.setLayout(width: Int, height: Int) { dialog?.window?.setLayout(width, height) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/DocumentExtensions.kt ================================================ package com.kunfei.bookshelf.utils import android.content.Context import android.database.Cursor import android.net.Uri import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile import splitties.init.appCtx import java.io.File import java.io.IOException import java.nio.charset.Charset import java.util.* @Suppress("MemberVisibilityCanBePrivate") object DocumentUtils { fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean { val parent = getDirDocument(root, *subDirs) ?: return false return parent.findFile(fileName)?.exists() ?: false } fun delete(root: DocumentFile, fileName: String, vararg subDirs: String) { val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs) parent?.findFile(fileName)?.delete() } fun createFileIfNotExist( root: DocumentFile, fileName: String, mimeType: String = "", vararg subDirs: String ): DocumentFile? { val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs) return parent?.createFile(mimeType, fileName) } fun createFolderIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? { var parent: DocumentFile? = root for (subDirName in subDirs) { val subDir = parent?.findFile(subDirName) ?: parent?.createDirectory(subDirName) parent = subDir } return parent } fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? { var parent = root for (subDirName in subDirs) { val subDir = parent.findFile(subDirName) parent = subDir ?: return null } return parent } @JvmStatic @Throws(Exception::class) fun writeText( context: Context, data: String, fileUri: Uri, charset: Charset = Charsets.UTF_8 ): Boolean { return writeBytes(context, data.toByteArray(charset), fileUri) } @JvmStatic @Throws(Exception::class) fun writeBytes(context: Context, data: ByteArray, fileUri: Uri): Boolean { context.contentResolver.openOutputStream(fileUri)?.let { it.write(data) it.close() return true } return false } @JvmStatic @Throws(Exception::class) fun readText(context: Context, uri: Uri): String { return String(readBytes(context, uri)) } @JvmStatic @Throws(Exception::class) fun readBytes(context: Context, uri: Uri): ByteArray { context.contentResolver.openInputStream(uri)?.let { val len: Int = it.available() val buffer = ByteArray(len) it.read(buffer) it.close() return buffer } ?: throw IOException("打开文件失败\n${uri}") } @Throws(Exception::class) fun listFiles(uri: Uri, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList { if (!uri.isContentScheme()) { return listFiles(uri.path!!, filter) } val childrenUri = DocumentsContract .buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri)) val docList = arrayListOf() var cursor: Cursor? = null try { cursor = appCtx.contentResolver.query( childrenUri, arrayOf( DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_SIZE, DocumentsContract.Document.COLUMN_MIME_TYPE ), null, null, DocumentsContract.Document.COLUMN_DISPLAY_NAME ) cursor?.let { val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID) val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME) val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE) val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE) val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) if (cursor.moveToFirst()) { do { val item = FileDoc( name = cursor.getString(nci), isDir = cursor.getString(mci) == DocumentsContract.Document.MIME_TYPE_DIR, size = cursor.getLong(sci), date = Date(cursor.getLong(dci)), uri = DocumentsContract .buildDocumentUriUsingTree(uri, cursor.getString(ici)) ) if (filter == null || filter.invoke(item)) { docList.add(item) } } while (cursor.moveToNext()) } } } finally { cursor?.close() } return docList } @Throws(Exception::class) fun listFiles(path: String, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList { val docList = arrayListOf() val file = File(path) file.listFiles()?.forEach { val item = FileDoc( it.name, it.isDirectory, it.length(), Date(it.lastModified()), Uri.fromFile(it) ) if (filter == null || filter.invoke(item)) { docList.add(item) } } return docList } } data class FileDoc( val name: String, val isDir: Boolean, val size: Long, val date: Date, val uri: Uri ) { override fun toString(): String { return if (uri.isContentScheme()) uri.toString() else uri.path!! } val isContentScheme get() = uri.isContentScheme() fun readBytes(): ByteArray { return uri.readBytes(appCtx) } companion object { fun fromDocumentFile(doc: DocumentFile): FileDoc { return FileDoc( name = doc.name ?: "", isDir = doc.isDirectory, size = doc.length(), date = Date(doc.lastModified()), uri = doc.uri ) } fun fromFile(file: File): FileDoc { return FileDoc( name = file.name, isDir = file.isDirectory, size = file.length(), date = Date(file.lastModified()), uri = Uri.fromFile(file) ) } } } @Throws(Exception::class) fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) { DocumentUtils.writeText(context, data, this.uri, charset) } @Throws(Exception::class) fun DocumentFile.writeBytes(context: Context, data: ByteArray) { DocumentUtils.writeBytes(context, data, this.uri) } @Throws(Exception::class) fun DocumentFile.readText(context: Context): String { return DocumentUtils.readText(context, this.uri) } @Throws(Exception::class) fun DocumentFile.readBytes(context: Context): ByteArray { return DocumentUtils.readBytes(context, this.uri) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/DocumentUtil.java ================================================ package com.kunfei.bookshelf.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("_"); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/DrawableUtil.kt ================================================ package com.kunfei.bookshelf.utils import android.content.res.ColorStateList import android.graphics.PorterDuff import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.TransitionDrawable import androidx.annotation.ColorInt import androidx.core.graphics.drawable.DrawableCompat /** * @author Karim Abou Zeid (kabouzeid) */ object DrawableUtil { fun createTransitionDrawable( @ColorInt startColor: Int, @ColorInt endColor: Int ): TransitionDrawable { return createTransitionDrawable(ColorDrawable(startColor), ColorDrawable(endColor)) } @JvmStatic fun createTransitionDrawable(start: Drawable?, end: Drawable?): TransitionDrawable { val drawables = arrayOfNulls(2) drawables[0] = start drawables[1] = end return TransitionDrawable(drawables) } } fun Drawable.setTintList( tint: ColorStateList, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP ) { val wrappedDrawable = DrawableCompat.wrap(this) wrappedDrawable.mutate() DrawableCompat.setTintMode(wrappedDrawable, tintMode) DrawableCompat.setTintList(wrappedDrawable, tint) } fun Drawable.setTint( @ColorInt tint: Int, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP ) { val wrappedDrawable = DrawableCompat.wrap(this) wrappedDrawable.mutate() DrawableCompat.setTintMode(wrappedDrawable, tintMode) DrawableCompat.setTint(wrappedDrawable, tint) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/EncoderUtils.kt ================================================ package com.kunfei.bookshelf.utils import android.util.Base64 import java.nio.charset.StandardCharsets import java.security.spec.AlgorithmParameterSpec import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @Suppress("unused") object EncoderUtils { fun escape(src: String): String { val tmp = StringBuilder() for (char in src) { val charCode = char.code if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) { tmp.append(char) continue } val prefix = when { charCode < 16 -> "%0" charCode < 256 -> "%" else -> "%u" } tmp.append(prefix).append(charCode.toString(16)) } return tmp.toString() } fun base64Decode(str: String): String { val bytes = Base64.decode(str, Base64.DEFAULT) return try { String(bytes, StandardCharsets.UTF_8) } catch (e: Exception) { String(bytes) } } fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { return Base64.encodeToString(str.toByteArray(), flags) } //////////AES Start /** * Return the Base64-encode bytes of AES encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the Base64-encode bytes of AES encryption */ fun encryptAES2Base64( data: ByteArray?, key: ByteArray?, transformation: String?, iv: ByteArray? ): ByteArray? { return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP) } /** * Return the bytes of AES encryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES encryption */ fun encryptAES( data: ByteArray?, key: ByteArray?, transformation: String?, iv: ByteArray? ): ByteArray? { return symmetricTemplate(data, key, "AES", transformation!!, iv, true) } /** * Return the bytes of AES decryption for Base64-encode bytes. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption for Base64-encode bytes */ fun decryptBase64AES( data: ByteArray?, key: ByteArray?, transformation: String = "DES/CBC/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv) } /** * Return the bytes of AES decryption. * * @param data The data. * @param key The key. * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. * @param iv The buffer with the IV. The contents of the * buffer are copied to protect against subsequent modification. * @return the bytes of AES decryption */ fun decryptAES( data: ByteArray?, key: ByteArray?, transformation: String = "DES/CBC/PKCS5Padding", iv: ByteArray? = null ): ByteArray? { return symmetricTemplate(data, key, "AES", transformation, iv, false) } /** * Return the bytes of symmetric encryption or decryption. * * @param data The data. * @param key The key. * @param algorithm The name of algorithm. * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. * @param isEncrypt True to encrypt, false otherwise. * @return the bytes of symmetric encryption or decryption */ @Suppress("SameParameterValue") private fun symmetricTemplate( data: ByteArray?, key: ByteArray?, algorithm: String, transformation: String, iv: ByteArray?, isEncrypt: Boolean ): ByteArray? { return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null else try { val keySpec = SecretKeySpec(key, algorithm) val cipher = Cipher.getInstance(transformation) if (iv == null || iv.isEmpty()) { cipher.init(if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE, keySpec) } else { val params: AlgorithmParameterSpec = IvParameterSpec(iv) cipher.init( if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE, keySpec, params ) } cipher.doFinal(data) } catch (e: Throwable) { e.printStackTrace() null } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/EncodingDetect.java ================================================ package com.kunfei.bookshelf.utils; import androidx.annotation.NonNull; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.net.URL; import static android.text.TextUtils.isEmpty; /** * Copyright (C) <2009> *

* This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. *

* This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. *

* EncodingDetect.java
* 自动获取文件的编码 * * @author Billows.Van * @version 1.0 * @since Create on 2010-01-27 11:19:00 */ public class EncodingDetect { public static String getEncodeInHtml(@NonNull byte[] bytes) { try { String charsetStr = "UTF-8"; Document doc = Jsoup.parse(new String(bytes, charsetStr)); int a = doc.childNode(0).toString().indexOf("encoding"); if (a > 0) { String e = doc.childNode(0).toString().substring(a); int b = e.indexOf('"'); int c = e.indexOf('"', b + 1); return e.substring(b + 1, c); } Elements metaTags = doc.getElementsByTag("meta"); for (Element metaTag : metaTags) { String content = metaTag.attr("content"); String http_equiv = metaTag.attr("http-equiv"); charsetStr = metaTag.attr("charset"); if (!charsetStr.isEmpty()) { if (!isEmpty(charsetStr)) { return charsetStr; } } if (http_equiv.toLowerCase().equals("content-type")) { if (content.toLowerCase().contains("charset")) { charsetStr = content.substring(content.toLowerCase().indexOf("charset") + "charset=".length()); } else { charsetStr = content.substring(content.toLowerCase().indexOf(";") + 1); } if (!isEmpty(charsetStr)) { return charsetStr; } } } } catch (Exception ignored) { } return getJavaEncode(bytes); } public static String getJavaEncode(@NonNull byte[] bytes) { int len = bytes.length > 2000 ? 2000 : bytes.length; byte[] cBytes = new byte[len]; System.arraycopy(bytes, 0, cBytes, 0, len); BytesEncodingDetect bytesEncodingDetect = new BytesEncodingDetect(); String code = BytesEncodingDetect.javaname[bytesEncodingDetect.detectEncoding(cBytes)]; // UTF-16LE 特殊处理 if ("Unicode".equals(code)) { if (cBytes[0] == -1) { code = "UTF-16LE"; } } return code; } /** * 得到文件的编码 */ public static String getJavaEncode(@NonNull String filePath) { BytesEncodingDetect s = new BytesEncodingDetect(); String fileCode = BytesEncodingDetect.javaname[s .detectEncoding(new File(filePath))]; // UTF-16LE 特殊处理 if ("Unicode".equals(fileCode)) { byte[] tempByte = BytesEncodingDetect.getFileBytes(new File( filePath)); if (tempByte[0] == -1) { fileCode = "UTF-16LE"; } } return fileCode; } /** * 得到文件的编码 */ public static String getJavaEncode(@NonNull File file) { BytesEncodingDetect s = new BytesEncodingDetect(); String fileCode = BytesEncodingDetect.javaname[s.detectEncoding(file)]; // UTF-16LE 特殊处理 if ("Unicode".equals(fileCode)) { byte[] tempByte = BytesEncodingDetect.getFileBytes(file); if (tempByte[0] == -1) { fileCode = "UTF-16LE"; } } return fileCode; } } class BytesEncodingDetect extends Encoding { // Frequency tables to hold the GB, Big5, and EUC-TW character // frequencies private int[][] GBFreq; private int[][] GBKFreq; private int[][] Big5Freq; private int[][] Big5PFreq; private int[][] EUC_TWFreq; private int[][] KRFreq; private int[][] JPFreq; // int UnicodeFreq[94][128]; // public static String[] nicename; // public static String[] codings; public boolean debug; public BytesEncodingDetect() { super(); debug = false; GBFreq = new int[94][94]; GBKFreq = new int[126][191]; Big5Freq = new int[94][158]; Big5PFreq = new int[126][191]; EUC_TWFreq = new int[94][94]; KRFreq = new int[94][94]; JPFreq = new int[94][94]; // Initialize the Frequency Table for GB, GBK, Big5, EUC-TW, KR, JP initialize_frequencies(); } /** * Function : detectEncoding Aruguments: URL Returns : One of the encodings * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER) * Description: This function looks at the URL contents and assigns it a * probability score for each encoding type. The encoding type with the * highest probability is returned. */ public int detectEncoding(URL testurl) { byte[] rawtext = new byte[10000]; int bytesread = 0, byteoffset = 0; int guess = OTHER; InputStream chinesestream; try { chinesestream = testurl.openStream(); while ((bytesread = chinesestream.read(rawtext, byteoffset, rawtext.length - byteoffset)) > 0) { byteoffset += bytesread; } ; chinesestream.close(); guess = detectEncoding(rawtext); } catch (Exception e) { System.err.println("Error loading or using URL " + e.toString()); guess = -1; } return guess; } /** * Function : detectEncoding Aruguments: File Returns : One of the encodings * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER) * Description: This function looks at the file and assigns it a probability * score for each encoding type. The encoding type with the highest * probability is returned. */ public int detectEncoding(File testfile) { byte[] rawtext = getFileBytes(testfile); return detectEncoding(rawtext); } public static byte[] getFileBytes(File testfile) { FileInputStream chinesefile; byte[] rawtext; rawtext = new byte[2000]; try { chinesefile = new FileInputStream(testfile); chinesefile.read(rawtext); chinesefile.close(); } catch (Exception e) { System.err.println("Error: " + e); } return rawtext; } /** * Function : detectEncoding Aruguments: byte array Returns : One of the * encodings from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, * or OTHER) Description: This function looks at the byte array and assigns * it a probability score for each encoding type. The encoding type with the * highest probability is returned. */ public int detectEncoding(byte[] rawtext) { int[] scores; int index, maxscore = 0; int encoding_guess = OTHER; scores = new int[TOTALTYPES]; // Assign Scores scores[GB2312] = gb2312_probability(rawtext); scores[GBK] = gbk_probability(rawtext); scores[GB18030] = gb18030_probability(rawtext); scores[HZ] = hz_probability(rawtext); scores[BIG5] = big5_probability(rawtext); scores[CNS11643] = euc_tw_probability(rawtext); scores[ISO2022CN] = iso_2022_cn_probability(rawtext); scores[UTF8] = utf8_probability(rawtext); scores[UNICODE] = utf16_probability(rawtext); scores[EUC_KR] = euc_kr_probability(rawtext); scores[CP949] = cp949_probability(rawtext); scores[JOHAB] = 0; scores[ISO2022KR] = iso_2022_kr_probability(rawtext); scores[ASCII] = ascii_probability(rawtext); scores[SJIS] = sjis_probability(rawtext); scores[EUC_JP] = euc_jp_probability(rawtext); scores[ISO2022JP] = iso_2022_jp_probability(rawtext); scores[UNICODET] = 0; scores[UNICODES] = 0; scores[ISO2022CN_GB] = 0; scores[ISO2022CN_CNS] = 0; scores[OTHER] = 0; // Tabulate Scores for (index = 0; index < TOTALTYPES; index++) { if (debug) System.err.println("Encoding " + nicename[index] + " score " + scores[index]); if (scores[index] > maxscore) { encoding_guess = index; maxscore = scores[index]; } } // Return OTHER if nothing scored above 50 if (maxscore <= 50) { encoding_guess = OTHER; } return encoding_guess; } /* * Function: gb2312_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses GB-2312 * encoding */ private int gb2312_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, gbchars = 1; long gbfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (GBFreq[row][column] != 0) { gbfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { // In GB high-freq character range gbfreq += 200; } } i++; } } rangeval = 50 * ((float) gbchars / (float) dbchars); freqval = 50 * ((float) gbfreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: gbk_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses GBK * encoding */ private int gbk_probability(byte[] rawText) { int i, rawTextLen = 0; int dbChars = 1, gbChars = 1; long gbFreq = 0, totalFreq = 1; float rangeVal = 0, freqVal = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawTextLen = rawText.length; for (i = 0; i < rawTextLen - 1; i++) { // System.err.println(rawText[i]); if (rawText[i] < 0) { dbChars++; if ((byte) 0xA1 <= rawText[i] && rawText[i] <= (byte) 0xF7 && // Original GB range (byte) 0xA1 <= rawText[i + 1] && rawText[i + 1] <= (byte) 0xFE) { gbChars++; totalFreq += 500; row = rawText[i] + 256 - 0xA1; column = rawText[i + 1] + 256 - 0xA1; // System.out.println("original row " + row + " column " + // column); if (GBFreq[row][column] != 0) { gbFreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { gbFreq += 200; } } else if ((byte) 0x81 <= rawText[i] && rawText[i] <= (byte) 0xFE && // Extended GB range (((byte) 0x80 <= rawText[i + 1] && rawText[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawText[i + 1] && rawText[i + 1] <= (byte) 0x7E))) { gbChars++; totalFreq += 500; row = rawText[i] + 256 - 0x81; if (0x40 <= rawText[i + 1] && rawText[i + 1] <= 0x7E) { column = rawText[i + 1] - 0x40; } else { column = rawText[i + 1] + 256 - 0x40; } // System.out.println("extended row " + row + " column " + // column + " rawtext[i] " + rawtext[i]); if (GBKFreq[row][column] != 0) { gbFreq += GBKFreq[row][column]; } } i++; } } rangeVal = 50 * ((float) gbChars / (float) dbChars); freqVal = 50 * ((float) gbFreq / (float) totalFreq); // For regular GB files, this would give the same score, so I handicap // it slightly return (int) (rangeVal + freqVal) - 1; } /* * Function: gb18030_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses GBK * encoding */ private int gb18030_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, gbchars = 1; long gbfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawText[i]); if (rawtext[i] < 0) { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 && // Original GB range i + 1 < rawtextlen && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; // System.out.println("original row " + row + " column " + // column); if (GBFreq[row][column] != 0) { gbfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { gbfreq += 200; } } else if ((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && // Extended GB range i + 1 < rawtextlen && (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) { gbchars++; totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { column = rawtext[i + 1] - 0x40; } else { column = rawtext[i + 1] + 256 - 0x40; } // System.out.println("extended row " + row + " column " + // column + " rawtext[i] " + rawtext[i]); if (GBKFreq[row][column] != 0) { gbfreq += GBKFreq[row][column]; } } else if ((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && // Extended GB range i + 3 < rawtextlen && (byte) 0x30 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x39 && (byte) 0x81 <= rawtext[i + 2] && rawtext[i + 2] <= (byte) 0xFE && (byte) 0x30 <= rawtext[i + 3] && rawtext[i + 3] <= (byte) 0x39) { gbchars++; /* * totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40 * <= rawtext[i+1] && rawtext[i+1] <= 0x7E) { column = * rawtext[i+1] - 0x40; } else { column = rawtext[i+1] + 256 * - 0x40; } //System.out.println("extended row " + row + " * column " + column + " rawtext[i] " + rawtext[i]); if * (GBKFreq[row][column] != 0) { gbfreq += * GBKFreq[row][column]; } */ } i++; } } rangeval = 50 * ((float) gbchars / (float) dbchars); freqval = 50 * ((float) gbfreq / (float) totalfreq); // For regular GB files, this would give the same score, so I handicap // it slightly return (int) (rangeval + freqval) - 1; } /* * Function: hz_probability Argument: byte array Returns : number from 0 to * 100 representing probability text in array uses HZ encoding */ private int hz_probability(byte[] rawtext) { int i, rawtextlen; int hzchars = 0, dbchars = 1; long hzfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int hzstart = 0, hzend = 0; int row, column; rawtextlen = rawtext.length; for (i = 0; i < rawtextlen; i++) { if (rawtext[i] == '~') { if (rawtext[i + 1] == '{') { hzstart++; i += 2; while (i < rawtextlen - 1) { if (rawtext[i] == 0x0A || rawtext[i] == 0x0D) { break; } else if (rawtext[i] == '~' && rawtext[i + 1] == '}') { hzend++; i++; break; } else if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77) && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) { hzchars += 2; row = rawtext[i] - 0x21; column = rawtext[i + 1] - 0x21; totalfreq += 500; if (GBFreq[row][column] != 0) { hzfreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { hzfreq += 200; } } dbchars += 2; i += 2; } } else if (rawtext[i + 1] == '}') { hzend++; i++; } else if (rawtext[i + 1] == '~') { i++; } } } if (hzstart > 4) { rangeval = 50; } else if (hzstart > 1) { rangeval = 41; } else if (hzstart > 0) { // Only 39 in case the sequence happened to // occur rangeval = 39; // in otherwise non-Hz text } else { rangeval = 0; } freqval = 50 * ((float) hzfreq / (float) totalfreq); return (int) (rangeval + freqval); } /** * Function: big5_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses Big5 encoding */ private int big5_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, bfchars = 1; float rangeval = 0, freqval = 0; long bffreq = 0, totalfreq = 1; int row, column; // Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { if (rawtext[i] < 0) { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF9 && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE))) { bfchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { column = rawtext[i + 1] - 0x40; } else { column = rawtext[i + 1] + 256 - 0x61; } if (Big5Freq[row][column] != 0) { bffreq += Big5Freq[row][column]; } else if (3 <= row && row <= 37) { bffreq += 200; } } i++; } } rangeval = 50 * ((float) bfchars / (float) dbchars); freqval = 50 * ((float) bffreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: euc_tw_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses EUC-TW (CNS 11643) * encoding */ private int euc_tw_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, cnschars = 1; long cnsfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Check to see if characters fit into acceptable ranges // and have expected frequency of use rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { if (rawtext[i] < 0) { // high bit set dbchars++; if (i + 3 < rawtextlen && (byte) 0x8E == rawtext[i] && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xB0 && (byte) 0xA1 <= rawtext[i + 2] && rawtext[i + 2] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 3] && rawtext[i + 3] <= (byte) 0xFE) { // Planes 1 - 16 cnschars++; // System.out.println("plane 2 or above CNS char"); // These are all less frequent chars so just ignore freq i += 3; } else if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && // Plane 1 (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { cnschars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (EUC_TWFreq[row][column] != 0) { cnsfreq += EUC_TWFreq[row][column]; } else if (35 <= row && row <= 92) { cnsfreq += 150; } i++; } } } rangeval = 50 * ((float) cnschars / (float) dbchars); freqval = 50 * ((float) cnsfreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: iso_2022_cn_probability Argument: byte array Returns : number * from 0 to 100 representing probability text in array uses ISO 2022-CN * encoding WORKS FOR BASIC CASES, BUT STILL NEEDS MORE WORK */ int iso_2022_cn_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, isochars = 1; long isofreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Check to see if characters fit into acceptable ranges // and have expected frequency of use rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { if (rawtext[i] == (byte) 0x1B && i + 3 < rawtextlen) { // Escape // char ESC if (rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == 0x29 && rawtext[i + 3] == (byte) 0x41) { // GB Escape $ ) A i += 4; while (rawtext[i] != (byte) 0x1B) { dbchars++; if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77) && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) { isochars++; row = rawtext[i] - 0x21; column = rawtext[i + 1] - 0x21; totalfreq += 500; if (GBFreq[row][column] != 0) { isofreq += GBFreq[row][column]; } else if (15 <= row && row < 55) { isofreq += 200; } i++; } i++; } } else if (i + 3 < rawtextlen && rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == (byte) 0x29 && rawtext[i + 3] == (byte) 0x47) { // CNS Escape $ ) G i += 4; while (rawtext[i] != (byte) 0x1B) { dbchars++; if ((byte) 0x21 <= rawtext[i] && rawtext[i] <= (byte) 0x7E && (byte) 0x21 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) { isochars++; totalfreq += 500; row = rawtext[i] - 0x21; column = rawtext[i + 1] - 0x21; if (EUC_TWFreq[row][column] != 0) { isofreq += EUC_TWFreq[row][column]; } else if (35 <= row && row <= 92) { isofreq += 150; } i++; } i++; } } if (rawtext[i] == (byte) 0x1B && i + 2 < rawtextlen && rawtext[i + 1] == (byte) 0x28 && rawtext[i + 2] == (byte) 0x42) { // ASCII: // ESC // ( B i += 2; } } } rangeval = 50 * ((float) isochars / (float) dbchars); freqval = 50 * ((float) isofreq / (float) totalfreq); // System.out.println("isochars dbchars isofreq totalfreq " + isochars + // " " + dbchars + " " + isofreq + " " + totalfreq + " // " + rangeval + " " + freqval); return (int) (rangeval + freqval); // return 0; } /* * Function: utf8_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses UTF-8 encoding of * Unicode */ int utf8_probability(byte[] rawtext) { int score = 0; int i, rawtextlen = 0; int goodbytes = 0, asciibytes = 0; // Maybe also use UTF8 Byte Order Mark: EF BB BF // Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] & (byte) 0x7F) == rawtext[i]) { // One byte asciibytes++; // Ignore ASCII, can throw off count } else if (-64 <= rawtext[i] && rawtext[i] <= -33 && // Two bytes i + 1 < rawtextlen && -128 <= rawtext[i + 1] && rawtext[i + 1] <= -65) { goodbytes += 2; i++; } else if (-32 <= rawtext[i] && rawtext[i] <= -17 && // Three bytes i + 2 < rawtextlen && -128 <= rawtext[i + 1] && rawtext[i + 1] <= -65 && -128 <= rawtext[i + 2] && rawtext[i + 2] <= -65) { goodbytes += 3; i += 2; } } if (asciibytes == rawtextlen) { return 0; } score = (int) (100 * ((float) goodbytes / (float) (rawtextlen - asciibytes))); // System.out.println("rawtextlen " + rawtextlen + " goodbytes " + // goodbytes + " asciibytes " + asciibytes + " score " + // score); // If not above 98, reduce to zero to prevent coincidental matches // Allows for some (few) bad formed sequences if (score > 98) { return score; } else if (score > 95 && goodbytes > 30) { return score; } else { return 0; } } /* * Function: utf16_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses UTF-16 encoding of * Unicode, guess based on BOM // NOT VERY GENERAL, NEEDS MUCH MORE WORK */ int utf16_probability(byte[] rawtext) { // int score = 0; // int i, rawtextlen = 0; // int goodbytes = 0, asciibytes = 0; if (rawtext.length > 1 && ((byte) 0xFE == rawtext[0] && (byte) 0xFF == rawtext[1]) || // Big-endian ((byte) 0xFF == rawtext[0] && (byte) 0xFE == rawtext[1])) { // Little-endian return 100; } return 0; /* * // Check to see if characters fit into acceptable ranges rawtextlen = * rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] & * (byte)0x7F) == rawtext[i]) { // One byte goodbytes += 1; * asciibytes++; } else if ((rawtext[i] & (byte)0xDF) == rawtext[i]) { * // Two bytes if (i+1 < rawtextlen && (rawtext[i+1] & (byte)0xBF) == * rawtext[i+1]) { goodbytes += 2; i++; } } else if ((rawtext[i] & * (byte)0xEF) == rawtext[i]) { // Three bytes if (i+2 < rawtextlen && * (rawtext[i+1] & (byte)0xBF) == rawtext[i+1] && (rawtext[i+2] & * (byte)0xBF) == rawtext[i+2]) { goodbytes += 3; i+=2; } } } * * score = (int)(100 * ((float)goodbytes/(float)rawtext.length)); // An * all ASCII file is also a good UTF8 file, but I'd rather it // get * identified as ASCII. Can delete following 3 lines otherwise if * (goodbytes == asciibytes) { score = 0; } // If not above 90, reduce * to zero to prevent coincidental matches if (score > 90) { return * score; } else { return 0; } */ } /* * Function: ascii_probability Argument: byte array Returns : number from 0 * to 100 representing probability text in array uses all ASCII Description: * Sees if array has any characters not in ASCII range, if so, score is * reduced */ int ascii_probability(byte[] rawtext) { int score = 75; int i, rawtextlen; rawtextlen = rawtext.length; for (i = 0; i < rawtextlen; i++) { if (rawtext[i] < 0) { score = score - 5; } else if (rawtext[i] == (byte) 0x1B) { // ESC (used by ISO 2022) score = score - 5; } if (score <= 0) { return 0; } } return score; } /* * Function: euc_kr__probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses EUC-KR * encoding */ int euc_kr_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, krchars = 1; long krfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { krchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (KRFreq[row][column] != 0) { krfreq += KRFreq[row][column]; } else if (15 <= row && row < 55) { krfreq += 0; } } i++; } } rangeval = 50 * ((float) krchars / (float) dbchars); freqval = 50 * ((float) krfreq / (float) totalfreq); return (int) (rangeval + freqval); } /* * Function: cp949__probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses Cp949 * encoding */ int cp949_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, krchars = 1; long krfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if ((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && ((byte) 0x41 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x5A || (byte) 0x61 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7A || (byte) 0x81 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE)) { krchars++; totalfreq += 500; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (KRFreq[row][column] != 0) { krfreq += KRFreq[row][column]; } } } i++; } } rangeval = 50 * ((float) krchars / (float) dbchars); freqval = 50 * ((float) krfreq / (float) totalfreq); return (int) (rangeval + freqval); } private int iso_2022_kr_probability(byte[] rawtext) { int i; for (i = 0; i < rawtext.length; i++) { if (i + 3 < rawtext.length && rawtext[i] == 0x1b && (char) rawtext[i + 1] == '$' && (char) rawtext[i + 2] == ')' && (char) rawtext[i + 3] == 'C') { return 100; } } return 0; } /* * Function: euc_jp_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses EUC-JP * encoding */ private int euc_jp_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, jpchars = 1; long jpfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawText[i]); if (rawtext[i] >= 0) { // asciiChars++; } else { dbchars++; if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE && (byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) { jpchars++; totalfreq += 500; row = rawtext[i] + 256 - 0xA1; column = rawtext[i + 1] + 256 - 0xA1; if (JPFreq[row][column] != 0) { jpfreq += JPFreq[row][column]; } else if (15 <= row && row < 55) { jpfreq += 0; } } i++; } } rangeval = 50 * ((float) jpchars / (float) dbchars); freqval = 50 * ((float) jpfreq / (float) totalfreq); return (int) (rangeval + freqval); } int iso_2022_jp_probability(byte[] rawtext) { int i; for (i = 0; i < rawtext.length; i++) { if (i + 2 < rawtext.length && rawtext[i] == 0x1b && (char) rawtext[i + 1] == '$' && (char) rawtext[i + 2] == 'B') { return 100; } } return 0; } /* * Function: sjis_probability Argument: pointer to byte array Returns : * number from 0 to 100 representing probability text in array uses * Shift-JIS encoding */ int sjis_probability(byte[] rawtext) { int i, rawtextlen = 0; int dbchars = 1, jpchars = 1; long jpfreq = 0, totalfreq = 1; float rangeval = 0, freqval = 0; int row, column, adjust; // Stage 1: Check to see if characters fit into acceptable ranges rawtextlen = rawtext.length; for (i = 0; i < rawtextlen - 1; i++) { // System.err.println(rawtext[i]); if (rawtext[i] >= 0) { // asciichars++; } else { dbchars++; if (i + 1 < rawtext.length && (((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0x9F) || ((byte) 0xE0 <= rawtext[i] && rawtext[i] <= (byte) 0xEF)) && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFC))) { jpchars++; totalfreq += 500; row = rawtext[i] + 256; column = rawtext[i + 1] + 256; if (column < 0x9f) { adjust = 1; if (column > 0x7f) { column -= 0x20; } else { column -= 0x19; } } else { adjust = 0; column -= 0x7e; } if (row < 0xa0) { row = ((row - 0x70) << 1) - adjust; } else { row = ((row - 0xb0) << 1) - adjust; } row -= 0x20; column = 0x20; // System.out.println("original row " + row + " column " + // column); if (row < JPFreq.length && column < JPFreq[row].length && JPFreq[row][column] != 0) { jpfreq += JPFreq[row][column]; } i++; } else if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xDF) { // half-width katakana, convert to full-width } } } rangeval = 50 * ((float) jpchars / (float) dbchars); freqval = 50 * ((float) jpfreq / (float) totalfreq); // For regular GB files, this would give the same score, so I handicap // it slightly return (int) (rangeval + freqval) - 1; } void initialize_frequencies() { int i, j; for (i = 93; i >= 0; i--) { for (j = 93; j >= 0; j--) { GBFreq[i][j] = 0; } } for (i = 125; i >= 0; i--) { for (j = 190; j >= 0; j--) { GBKFreq[i][j] = 0; } } // for (i = 0; i < 94; i++) { // for (j = 0; j < 158; j++) { for (i = 93; i >= 0; i--) { for (j = 157; j >= 0; j--) { Big5Freq[i][j] = 0; } } // for (i = 0; i < 126; i++) { // for (j = 0; j < 191; j++) { for (i = 125; i >= 0; i--) { for (j = 190; j >= 0; j--) { Big5PFreq[i][j] = 0; } } // for (i = 0; i < 94; i++) { // for (j = 0; j < 94; j++) { for (i = 93; i >= 0; i--) { for (j = 93; j >= 0; j--) { EUC_TWFreq[i][j] = 0; } } for (i = 93; i >= 0; i--) { for (j = 93; j >= 0; j--) { JPFreq[i][j] = 0; } } GBFreq[20][35] = 599; GBFreq[49][26] = 598; GBFreq[41][38] = 597; GBFreq[17][26] = 596; GBFreq[32][42] = 595; GBFreq[39][42] = 594; GBFreq[45][49] = 593; GBFreq[51][57] = 592; GBFreq[50][47] = 591; GBFreq[42][90] = 590; GBFreq[52][65] = 589; GBFreq[53][47] = 588; GBFreq[19][82] = 587; GBFreq[31][19] = 586; GBFreq[40][46] = 585; GBFreq[24][89] = 584; GBFreq[23][85] = 583; GBFreq[20][28] = 582; GBFreq[42][20] = 581; GBFreq[34][38] = 580; GBFreq[45][9] = 579; GBFreq[54][50] = 578; GBFreq[25][44] = 577; GBFreq[35][66] = 576; GBFreq[20][55] = 575; GBFreq[18][85] = 574; GBFreq[20][31] = 573; GBFreq[49][17] = 572; GBFreq[41][16] = 571; GBFreq[35][73] = 570; GBFreq[20][34] = 569; GBFreq[29][44] = 568; GBFreq[35][38] = 567; GBFreq[49][9] = 566; GBFreq[46][33] = 565; GBFreq[49][51] = 564; GBFreq[40][89] = 563; GBFreq[26][64] = 562; GBFreq[54][51] = 561; GBFreq[54][36] = 560; GBFreq[39][4] = 559; GBFreq[53][13] = 558; GBFreq[24][92] = 557; GBFreq[27][49] = 556; GBFreq[48][6] = 555; GBFreq[21][51] = 554; GBFreq[30][40] = 553; GBFreq[42][92] = 552; GBFreq[31][78] = 551; GBFreq[25][82] = 550; GBFreq[47][0] = 549; GBFreq[34][19] = 548; GBFreq[47][35] = 547; GBFreq[21][63] = 546; GBFreq[43][75] = 545; GBFreq[21][87] = 544; GBFreq[35][59] = 543; GBFreq[25][34] = 542; GBFreq[21][27] = 541; GBFreq[39][26] = 540; GBFreq[34][26] = 539; GBFreq[39][52] = 538; GBFreq[50][57] = 537; GBFreq[37][79] = 536; GBFreq[26][24] = 535; GBFreq[22][1] = 534; GBFreq[18][40] = 533; GBFreq[41][33] = 532; GBFreq[53][26] = 531; GBFreq[54][86] = 530; GBFreq[20][16] = 529; GBFreq[46][74] = 528; GBFreq[30][19] = 527; GBFreq[45][35] = 526; GBFreq[45][61] = 525; GBFreq[30][9] = 524; GBFreq[41][53] = 523; GBFreq[41][13] = 522; GBFreq[50][34] = 521; GBFreq[53][86] = 520; GBFreq[47][47] = 519; GBFreq[22][28] = 518; GBFreq[50][53] = 517; GBFreq[39][70] = 516; GBFreq[38][15] = 515; GBFreq[42][88] = 514; GBFreq[16][29] = 513; GBFreq[27][90] = 512; GBFreq[29][12] = 511; GBFreq[44][22] = 510; GBFreq[34][69] = 509; GBFreq[24][10] = 508; GBFreq[44][11] = 507; GBFreq[39][92] = 506; GBFreq[49][48] = 505; GBFreq[31][46] = 504; GBFreq[19][50] = 503; GBFreq[21][14] = 502; GBFreq[32][28] = 501; GBFreq[18][3] = 500; GBFreq[53][9] = 499; GBFreq[34][80] = 498; GBFreq[48][88] = 497; GBFreq[46][53] = 496; GBFreq[22][53] = 495; GBFreq[28][10] = 494; GBFreq[44][65] = 493; GBFreq[20][10] = 492; GBFreq[40][76] = 491; GBFreq[47][8] = 490; GBFreq[50][74] = 489; GBFreq[23][62] = 488; GBFreq[49][65] = 487; GBFreq[28][87] = 486; GBFreq[15][48] = 485; GBFreq[22][7] = 484; GBFreq[19][42] = 483; GBFreq[41][20] = 482; GBFreq[26][55] = 481; GBFreq[21][93] = 480; GBFreq[31][76] = 479; GBFreq[34][31] = 478; GBFreq[20][66] = 477; GBFreq[51][33] = 476; GBFreq[34][86] = 475; GBFreq[37][67] = 474; GBFreq[53][53] = 473; GBFreq[40][88] = 472; GBFreq[39][10] = 471; GBFreq[24][3] = 470; GBFreq[27][25] = 469; GBFreq[26][15] = 468; GBFreq[21][88] = 467; GBFreq[52][62] = 466; GBFreq[46][81] = 465; GBFreq[38][72] = 464; GBFreq[17][30] = 463; GBFreq[52][92] = 462; GBFreq[34][90] = 461; GBFreq[21][7] = 460; GBFreq[36][13] = 459; GBFreq[45][41] = 458; GBFreq[32][5] = 457; GBFreq[26][89] = 456; GBFreq[23][87] = 455; GBFreq[20][39] = 454; GBFreq[27][23] = 453; GBFreq[25][59] = 452; GBFreq[49][20] = 451; GBFreq[54][77] = 450; GBFreq[27][67] = 449; GBFreq[47][33] = 448; GBFreq[41][17] = 447; GBFreq[19][81] = 446; GBFreq[16][66] = 445; GBFreq[45][26] = 444; GBFreq[49][81] = 443; GBFreq[53][55] = 442; GBFreq[16][26] = 441; GBFreq[54][62] = 440; GBFreq[20][70] = 439; GBFreq[42][35] = 438; GBFreq[20][57] = 437; GBFreq[34][36] = 436; GBFreq[46][63] = 435; GBFreq[19][45] = 434; GBFreq[21][10] = 433; GBFreq[52][93] = 432; GBFreq[25][2] = 431; GBFreq[30][57] = 430; GBFreq[41][24] = 429; GBFreq[28][43] = 428; GBFreq[45][86] = 427; GBFreq[51][56] = 426; GBFreq[37][28] = 425; GBFreq[52][69] = 424; GBFreq[43][92] = 423; GBFreq[41][31] = 422; GBFreq[37][87] = 421; GBFreq[47][36] = 420; GBFreq[16][16] = 419; GBFreq[40][56] = 418; GBFreq[24][55] = 417; GBFreq[17][1] = 416; GBFreq[35][57] = 415; GBFreq[27][50] = 414; GBFreq[26][14] = 413; GBFreq[50][40] = 412; GBFreq[39][19] = 411; GBFreq[19][89] = 410; GBFreq[29][91] = 409; GBFreq[17][89] = 408; GBFreq[39][74] = 407; GBFreq[46][39] = 406; GBFreq[40][28] = 405; GBFreq[45][68] = 404; GBFreq[43][10] = 403; GBFreq[42][13] = 402; GBFreq[44][81] = 401; GBFreq[41][47] = 400; GBFreq[48][58] = 399; GBFreq[43][68] = 398; GBFreq[16][79] = 397; GBFreq[19][5] = 396; GBFreq[54][59] = 395; GBFreq[17][36] = 394; GBFreq[18][0] = 393; GBFreq[41][5] = 392; GBFreq[41][72] = 391; GBFreq[16][39] = 390; GBFreq[54][0] = 389; GBFreq[51][16] = 388; GBFreq[29][36] = 387; GBFreq[47][5] = 386; GBFreq[47][51] = 385; GBFreq[44][7] = 384; GBFreq[35][30] = 383; GBFreq[26][9] = 382; GBFreq[16][7] = 381; GBFreq[32][1] = 380; GBFreq[33][76] = 379; GBFreq[34][91] = 378; GBFreq[52][36] = 377; GBFreq[26][77] = 376; GBFreq[35][48] = 375; GBFreq[40][80] = 374; GBFreq[41][92] = 373; GBFreq[27][93] = 372; GBFreq[15][17] = 371; GBFreq[16][76] = 370; GBFreq[51][12] = 369; GBFreq[18][20] = 368; GBFreq[15][54] = 367; GBFreq[50][5] = 366; GBFreq[33][22] = 365; GBFreq[37][57] = 364; GBFreq[28][47] = 363; GBFreq[42][31] = 362; GBFreq[18][2] = 361; GBFreq[43][64] = 360; GBFreq[23][47] = 359; GBFreq[28][79] = 358; GBFreq[25][45] = 357; GBFreq[23][91] = 356; GBFreq[22][19] = 355; GBFreq[25][46] = 354; GBFreq[22][36] = 353; GBFreq[54][85] = 352; GBFreq[46][20] = 351; GBFreq[27][37] = 350; GBFreq[26][81] = 349; GBFreq[42][29] = 348; GBFreq[31][90] = 347; GBFreq[41][59] = 346; GBFreq[24][65] = 345; GBFreq[44][84] = 344; GBFreq[24][90] = 343; GBFreq[38][54] = 342; GBFreq[28][70] = 341; GBFreq[27][15] = 340; GBFreq[28][80] = 339; GBFreq[29][8] = 338; GBFreq[45][80] = 337; GBFreq[53][37] = 336; GBFreq[28][65] = 335; GBFreq[23][86] = 334; GBFreq[39][45] = 333; GBFreq[53][32] = 332; GBFreq[38][68] = 331; GBFreq[45][78] = 330; GBFreq[43][7] = 329; GBFreq[46][82] = 328; GBFreq[27][38] = 327; GBFreq[16][62] = 326; GBFreq[24][17] = 325; GBFreq[22][70] = 324; GBFreq[52][28] = 323; GBFreq[23][40] = 322; GBFreq[28][50] = 321; GBFreq[42][91] = 320; GBFreq[47][76] = 319; GBFreq[15][42] = 318; GBFreq[43][55] = 317; GBFreq[29][84] = 316; GBFreq[44][90] = 315; GBFreq[53][16] = 314; GBFreq[22][93] = 313; GBFreq[34][10] = 312; GBFreq[32][53] = 311; GBFreq[43][65] = 310; GBFreq[28][7] = 309; GBFreq[35][46] = 308; GBFreq[21][39] = 307; GBFreq[44][18] = 306; GBFreq[40][10] = 305; GBFreq[54][53] = 304; GBFreq[38][74] = 303; GBFreq[28][26] = 302; GBFreq[15][13] = 301; GBFreq[39][34] = 300; GBFreq[39][46] = 299; GBFreq[42][66] = 298; GBFreq[33][58] = 297; GBFreq[15][56] = 296; GBFreq[18][51] = 295; GBFreq[49][68] = 294; GBFreq[30][37] = 293; GBFreq[51][84] = 292; GBFreq[51][9] = 291; GBFreq[40][70] = 290; GBFreq[41][84] = 289; GBFreq[28][64] = 288; GBFreq[32][88] = 287; GBFreq[24][5] = 286; GBFreq[53][23] = 285; GBFreq[42][27] = 284; GBFreq[22][38] = 283; GBFreq[32][86] = 282; GBFreq[34][30] = 281; GBFreq[38][63] = 280; GBFreq[24][59] = 279; GBFreq[22][81] = 278; GBFreq[32][11] = 277; GBFreq[51][21] = 276; GBFreq[54][41] = 275; GBFreq[21][50] = 274; GBFreq[23][89] = 273; GBFreq[19][87] = 272; GBFreq[26][7] = 271; GBFreq[30][75] = 270; GBFreq[43][84] = 269; GBFreq[51][25] = 268; GBFreq[16][67] = 267; GBFreq[32][9] = 266; GBFreq[48][51] = 265; GBFreq[39][7] = 264; GBFreq[44][88] = 263; GBFreq[52][24] = 262; GBFreq[23][34] = 261; GBFreq[32][75] = 260; GBFreq[19][10] = 259; GBFreq[28][91] = 258; GBFreq[32][83] = 257; GBFreq[25][75] = 256; GBFreq[53][45] = 255; GBFreq[29][85] = 254; GBFreq[53][59] = 253; GBFreq[16][2] = 252; GBFreq[19][78] = 251; GBFreq[15][75] = 250; GBFreq[51][42] = 249; GBFreq[45][67] = 248; GBFreq[15][74] = 247; GBFreq[25][81] = 246; GBFreq[37][62] = 245; GBFreq[16][55] = 244; GBFreq[18][38] = 243; GBFreq[23][23] = 242; GBFreq[38][30] = 241; GBFreq[17][28] = 240; GBFreq[44][73] = 239; GBFreq[23][78] = 238; GBFreq[40][77] = 237; GBFreq[38][87] = 236; GBFreq[27][19] = 235; GBFreq[38][82] = 234; GBFreq[37][22] = 233; GBFreq[41][30] = 232; GBFreq[54][9] = 231; GBFreq[32][30] = 230; GBFreq[30][52] = 229; GBFreq[40][84] = 228; GBFreq[53][57] = 227; GBFreq[27][27] = 226; GBFreq[38][64] = 225; GBFreq[18][43] = 224; GBFreq[23][69] = 223; GBFreq[28][12] = 222; GBFreq[50][78] = 221; GBFreq[50][1] = 220; GBFreq[26][88] = 219; GBFreq[36][40] = 218; GBFreq[33][89] = 217; GBFreq[41][28] = 216; GBFreq[31][77] = 215; GBFreq[46][1] = 214; GBFreq[47][19] = 213; GBFreq[35][55] = 212; GBFreq[41][21] = 211; GBFreq[27][10] = 210; GBFreq[32][77] = 209; GBFreq[26][37] = 208; GBFreq[20][33] = 207; GBFreq[41][52] = 206; GBFreq[32][18] = 205; GBFreq[38][13] = 204; GBFreq[20][18] = 203; GBFreq[20][24] = 202; GBFreq[45][19] = 201; GBFreq[18][53] = 200; /* * GBFreq[39][0] = 199; GBFreq[40][71] = 198; GBFreq[41][27] = 197; * GBFreq[15][69] = 196; GBFreq[42][10] = 195; GBFreq[31][89] = 194; * GBFreq[51][28] = 193; GBFreq[41][22] = 192; GBFreq[40][43] = 191; * GBFreq[38][6] = 190; GBFreq[37][11] = 189; GBFreq[39][60] = 188; * GBFreq[48][47] = 187; GBFreq[46][80] = 186; GBFreq[52][49] = 185; * GBFreq[50][48] = 184; GBFreq[25][1] = 183; GBFreq[52][29] = 182; * GBFreq[24][66] = 181; GBFreq[23][35] = 180; GBFreq[49][72] = 179; * GBFreq[47][45] = 178; GBFreq[45][14] = 177; GBFreq[51][70] = 176; * GBFreq[22][30] = 175; GBFreq[49][83] = 174; GBFreq[26][79] = 173; * GBFreq[27][41] = 172; GBFreq[51][81] = 171; GBFreq[41][54] = 170; * GBFreq[20][4] = 169; GBFreq[29][60] = 168; GBFreq[20][27] = 167; * GBFreq[50][15] = 166; GBFreq[41][6] = 165; GBFreq[35][34] = 164; * GBFreq[44][87] = 163; GBFreq[46][66] = 162; GBFreq[42][37] = 161; * GBFreq[42][24] = 160; GBFreq[54][7] = 159; GBFreq[41][14] = 158; * GBFreq[39][83] = 157; GBFreq[16][87] = 156; GBFreq[20][59] = 155; * GBFreq[42][12] = 154; GBFreq[47][2] = 153; GBFreq[21][32] = 152; * GBFreq[53][29] = 151; GBFreq[22][40] = 150; GBFreq[24][58] = 149; * GBFreq[52][88] = 148; GBFreq[29][30] = 147; GBFreq[15][91] = 146; * GBFreq[54][72] = 145; GBFreq[51][75] = 144; GBFreq[33][67] = 143; * GBFreq[41][50] = 142; GBFreq[27][34] = 141; GBFreq[46][17] = 140; * GBFreq[31][74] = 139; GBFreq[42][67] = 138; GBFreq[54][87] = 137; * GBFreq[27][14] = 136; GBFreq[16][63] = 135; GBFreq[16][5] = 134; * GBFreq[43][23] = 133; GBFreq[23][13] = 132; GBFreq[31][12] = 131; * GBFreq[25][57] = 130; GBFreq[38][49] = 129; GBFreq[42][69] = 128; * GBFreq[23][80] = 127; GBFreq[29][0] = 126; GBFreq[28][2] = 125; * GBFreq[28][17] = 124; GBFreq[17][27] = 123; GBFreq[40][16] = 122; * GBFreq[45][1] = 121; GBFreq[36][33] = 120; GBFreq[35][23] = 119; * GBFreq[20][86] = 118; GBFreq[29][53] = 117; GBFreq[23][88] = 116; * GBFreq[51][87] = 115; GBFreq[54][27] = 114; GBFreq[44][36] = 113; * GBFreq[21][45] = 112; GBFreq[53][52] = 111; GBFreq[31][53] = 110; * GBFreq[38][47] = 109; GBFreq[27][21] = 108; GBFreq[30][42] = 107; * GBFreq[29][10] = 106; GBFreq[35][35] = 105; GBFreq[24][56] = 104; * GBFreq[41][29] = 103; GBFreq[18][68] = 102; GBFreq[29][24] = 101; * GBFreq[25][84] = 100; GBFreq[35][47] = 99; GBFreq[29][56] = 98; * GBFreq[30][44] = 97; GBFreq[53][3] = 96; GBFreq[30][63] = 95; * GBFreq[52][52] = 94; GBFreq[54][1] = 93; GBFreq[22][48] = 92; * GBFreq[54][66] = 91; GBFreq[21][90] = 90; GBFreq[52][47] = 89; * GBFreq[39][25] = 88; GBFreq[39][39] = 87; GBFreq[44][37] = 86; * GBFreq[44][76] = 85; GBFreq[46][75] = 84; GBFreq[18][37] = 83; * GBFreq[47][42] = 82; GBFreq[19][92] = 81; GBFreq[51][27] = 80; * GBFreq[48][83] = 79; GBFreq[23][70] = 78; GBFreq[29][9] = 77; * GBFreq[33][79] = 76; GBFreq[52][90] = 75; GBFreq[53][6] = 74; * GBFreq[24][36] = 73; GBFreq[25][25] = 72; GBFreq[44][26] = 71; * GBFreq[25][36] = 70; GBFreq[29][87] = 69; GBFreq[48][0] = 68; * GBFreq[15][40] = 67; GBFreq[17][45] = 66; GBFreq[30][14] = 65; * GBFreq[48][38] = 64; GBFreq[23][19] = 63; GBFreq[40][42] = 62; * GBFreq[31][63] = 61; GBFreq[16][23] = 60; GBFreq[26][21] = 59; * GBFreq[32][76] = 58; GBFreq[23][58] = 57; GBFreq[41][37] = 56; * GBFreq[30][43] = 55; GBFreq[47][38] = 54; GBFreq[21][46] = 53; * GBFreq[18][33] = 52; GBFreq[52][37] = 51; GBFreq[36][8] = 50; * GBFreq[49][24] = 49; GBFreq[15][66] = 48; GBFreq[35][77] = 47; * GBFreq[27][58] = 46; GBFreq[35][51] = 45; GBFreq[24][69] = 44; * GBFreq[20][54] = 43; GBFreq[24][41] = 42; GBFreq[41][0] = 41; * GBFreq[33][71] = 40; GBFreq[23][52] = 39; GBFreq[29][67] = 38; * GBFreq[46][51] = 37; GBFreq[46][90] = 36; GBFreq[49][33] = 35; * GBFreq[33][28] = 34; GBFreq[37][86] = 33; GBFreq[39][22] = 32; * GBFreq[37][37] = 31; GBFreq[29][62] = 30; GBFreq[29][50] = 29; * GBFreq[36][89] = 28; GBFreq[42][44] = 27; GBFreq[51][82] = 26; * GBFreq[28][83] = 25; GBFreq[15][78] = 24; GBFreq[46][62] = 23; * GBFreq[19][69] = 22; GBFreq[51][23] = 21; GBFreq[37][69] = 20; * GBFreq[25][5] = 19; GBFreq[51][85] = 18; GBFreq[48][77] = 17; * GBFreq[32][46] = 16; GBFreq[53][60] = 15; GBFreq[28][57] = 14; * GBFreq[54][82] = 13; GBFreq[54][15] = 12; GBFreq[49][54] = 11; * GBFreq[53][87] = 10; GBFreq[27][16] = 9; GBFreq[29][34] = 8; * GBFreq[20][44] = 7; GBFreq[42][73] = 6; GBFreq[47][71] = 5; * GBFreq[29][37] = 4; GBFreq[25][50] = 3; GBFreq[18][84] = 2; * GBFreq[50][45] = 1; GBFreq[48][46] = 0; */ // GBFreq[43][89] = -1; GBFreq[54][68] = -2; Big5Freq[9][89] = 600; Big5Freq[11][15] = 599; Big5Freq[3][66] = 598; Big5Freq[6][121] = 597; Big5Freq[3][0] = 596; Big5Freq[5][82] = 595; Big5Freq[3][42] = 594; Big5Freq[5][34] = 593; Big5Freq[3][8] = 592; Big5Freq[3][6] = 591; Big5Freq[3][67] = 590; Big5Freq[7][139] = 589; Big5Freq[23][137] = 588; Big5Freq[12][46] = 587; Big5Freq[4][8] = 586; Big5Freq[4][41] = 585; Big5Freq[18][47] = 584; Big5Freq[12][114] = 583; Big5Freq[6][1] = 582; Big5Freq[22][60] = 581; Big5Freq[5][46] = 580; Big5Freq[11][79] = 579; Big5Freq[3][23] = 578; Big5Freq[7][114] = 577; Big5Freq[29][102] = 576; Big5Freq[19][14] = 575; Big5Freq[4][133] = 574; Big5Freq[3][29] = 573; Big5Freq[4][109] = 572; Big5Freq[14][127] = 571; Big5Freq[5][48] = 570; Big5Freq[13][104] = 569; Big5Freq[3][132] = 568; Big5Freq[26][64] = 567; Big5Freq[7][19] = 566; Big5Freq[4][12] = 565; Big5Freq[11][124] = 564; Big5Freq[7][89] = 563; Big5Freq[15][124] = 562; Big5Freq[4][108] = 561; Big5Freq[19][66] = 560; Big5Freq[3][21] = 559; Big5Freq[24][12] = 558; Big5Freq[28][111] = 557; Big5Freq[12][107] = 556; Big5Freq[3][112] = 555; Big5Freq[8][113] = 554; Big5Freq[5][40] = 553; Big5Freq[26][145] = 552; Big5Freq[3][48] = 551; Big5Freq[3][70] = 550; Big5Freq[22][17] = 549; Big5Freq[16][47] = 548; Big5Freq[3][53] = 547; Big5Freq[4][24] = 546; Big5Freq[32][120] = 545; Big5Freq[24][49] = 544; Big5Freq[24][142] = 543; Big5Freq[18][66] = 542; Big5Freq[29][150] = 541; Big5Freq[5][122] = 540; Big5Freq[5][114] = 539; Big5Freq[3][44] = 538; Big5Freq[10][128] = 537; Big5Freq[15][20] = 536; Big5Freq[13][33] = 535; Big5Freq[14][87] = 534; Big5Freq[3][126] = 533; Big5Freq[4][53] = 532; Big5Freq[4][40] = 531; Big5Freq[9][93] = 530; Big5Freq[15][137] = 529; Big5Freq[10][123] = 528; Big5Freq[4][56] = 527; Big5Freq[5][71] = 526; Big5Freq[10][8] = 525; Big5Freq[5][16] = 524; Big5Freq[5][146] = 523; Big5Freq[18][88] = 522; Big5Freq[24][4] = 521; Big5Freq[20][47] = 520; Big5Freq[5][33] = 519; Big5Freq[9][43] = 518; Big5Freq[20][12] = 517; Big5Freq[20][13] = 516; Big5Freq[5][156] = 515; Big5Freq[22][140] = 514; Big5Freq[8][146] = 513; Big5Freq[21][123] = 512; Big5Freq[4][90] = 511; Big5Freq[5][62] = 510; Big5Freq[17][59] = 509; Big5Freq[10][37] = 508; Big5Freq[18][107] = 507; Big5Freq[14][53] = 506; Big5Freq[22][51] = 505; Big5Freq[8][13] = 504; Big5Freq[5][29] = 503; Big5Freq[9][7] = 502; Big5Freq[22][14] = 501; Big5Freq[8][55] = 500; Big5Freq[33][9] = 499; Big5Freq[16][64] = 498; Big5Freq[7][131] = 497; Big5Freq[34][4] = 496; Big5Freq[7][101] = 495; Big5Freq[11][139] = 494; Big5Freq[3][135] = 493; Big5Freq[7][102] = 492; Big5Freq[17][13] = 491; Big5Freq[3][20] = 490; Big5Freq[27][106] = 489; Big5Freq[5][88] = 488; Big5Freq[6][33] = 487; Big5Freq[5][139] = 486; Big5Freq[6][0] = 485; Big5Freq[17][58] = 484; Big5Freq[5][133] = 483; Big5Freq[9][107] = 482; Big5Freq[23][39] = 481; Big5Freq[5][23] = 480; Big5Freq[3][79] = 479; Big5Freq[32][97] = 478; Big5Freq[3][136] = 477; Big5Freq[4][94] = 476; Big5Freq[21][61] = 475; Big5Freq[23][123] = 474; Big5Freq[26][16] = 473; Big5Freq[24][137] = 472; Big5Freq[22][18] = 471; Big5Freq[5][1] = 470; Big5Freq[20][119] = 469; Big5Freq[3][7] = 468; Big5Freq[10][79] = 467; Big5Freq[15][105] = 466; Big5Freq[3][144] = 465; Big5Freq[12][80] = 464; Big5Freq[15][73] = 463; Big5Freq[3][19] = 462; Big5Freq[8][109] = 461; Big5Freq[3][15] = 460; Big5Freq[31][82] = 459; Big5Freq[3][43] = 458; Big5Freq[25][119] = 457; Big5Freq[16][111] = 456; Big5Freq[7][77] = 455; Big5Freq[3][95] = 454; Big5Freq[24][82] = 453; Big5Freq[7][52] = 452; Big5Freq[9][151] = 451; Big5Freq[3][129] = 450; Big5Freq[5][87] = 449; Big5Freq[3][55] = 448; Big5Freq[8][153] = 447; Big5Freq[4][83] = 446; Big5Freq[3][114] = 445; Big5Freq[23][147] = 444; Big5Freq[15][31] = 443; Big5Freq[3][54] = 442; Big5Freq[11][122] = 441; Big5Freq[4][4] = 440; Big5Freq[34][149] = 439; Big5Freq[3][17] = 438; Big5Freq[21][64] = 437; Big5Freq[26][144] = 436; Big5Freq[4][62] = 435; Big5Freq[8][15] = 434; Big5Freq[35][80] = 433; Big5Freq[7][110] = 432; Big5Freq[23][114] = 431; Big5Freq[3][108] = 430; Big5Freq[3][62] = 429; Big5Freq[21][41] = 428; Big5Freq[15][99] = 427; Big5Freq[5][47] = 426; Big5Freq[4][96] = 425; Big5Freq[20][122] = 424; Big5Freq[5][21] = 423; Big5Freq[4][157] = 422; Big5Freq[16][14] = 421; Big5Freq[3][117] = 420; Big5Freq[7][129] = 419; Big5Freq[4][27] = 418; Big5Freq[5][30] = 417; Big5Freq[22][16] = 416; Big5Freq[5][64] = 415; Big5Freq[17][99] = 414; Big5Freq[17][57] = 413; Big5Freq[8][105] = 412; Big5Freq[5][112] = 411; Big5Freq[20][59] = 410; Big5Freq[6][129] = 409; Big5Freq[18][17] = 408; Big5Freq[3][92] = 407; Big5Freq[28][118] = 406; Big5Freq[3][109] = 405; Big5Freq[31][51] = 404; Big5Freq[13][116] = 403; Big5Freq[6][15] = 402; Big5Freq[36][136] = 401; Big5Freq[12][74] = 400; Big5Freq[20][88] = 399; Big5Freq[36][68] = 398; Big5Freq[3][147] = 397; Big5Freq[15][84] = 396; Big5Freq[16][32] = 395; Big5Freq[16][58] = 394; Big5Freq[7][66] = 393; Big5Freq[23][107] = 392; Big5Freq[9][6] = 391; Big5Freq[12][86] = 390; Big5Freq[23][112] = 389; Big5Freq[37][23] = 388; Big5Freq[3][138] = 387; Big5Freq[20][68] = 386; Big5Freq[15][116] = 385; Big5Freq[18][64] = 384; Big5Freq[12][139] = 383; Big5Freq[11][155] = 382; Big5Freq[4][156] = 381; Big5Freq[12][84] = 380; Big5Freq[18][49] = 379; Big5Freq[25][125] = 378; Big5Freq[25][147] = 377; Big5Freq[15][110] = 376; Big5Freq[19][96] = 375; Big5Freq[30][152] = 374; Big5Freq[6][31] = 373; Big5Freq[27][117] = 372; Big5Freq[3][10] = 371; Big5Freq[6][131] = 370; Big5Freq[13][112] = 369; Big5Freq[36][156] = 368; Big5Freq[4][60] = 367; Big5Freq[15][121] = 366; Big5Freq[4][112] = 365; Big5Freq[30][142] = 364; Big5Freq[23][154] = 363; Big5Freq[27][101] = 362; Big5Freq[9][140] = 361; Big5Freq[3][89] = 360; Big5Freq[18][148] = 359; Big5Freq[4][69] = 358; Big5Freq[16][49] = 357; Big5Freq[6][117] = 356; Big5Freq[36][55] = 355; Big5Freq[5][123] = 354; Big5Freq[4][126] = 353; Big5Freq[4][119] = 352; Big5Freq[9][95] = 351; Big5Freq[5][24] = 350; Big5Freq[16][133] = 349; Big5Freq[10][134] = 348; Big5Freq[26][59] = 347; Big5Freq[6][41] = 346; Big5Freq[6][146] = 345; Big5Freq[19][24] = 344; Big5Freq[5][113] = 343; Big5Freq[10][118] = 342; Big5Freq[34][151] = 341; Big5Freq[9][72] = 340; Big5Freq[31][25] = 339; Big5Freq[18][126] = 338; Big5Freq[18][28] = 337; Big5Freq[4][153] = 336; Big5Freq[3][84] = 335; Big5Freq[21][18] = 334; Big5Freq[25][129] = 333; Big5Freq[6][107] = 332; Big5Freq[12][25] = 331; Big5Freq[17][109] = 330; Big5Freq[7][76] = 329; Big5Freq[15][15] = 328; Big5Freq[4][14] = 327; Big5Freq[23][88] = 326; Big5Freq[18][2] = 325; Big5Freq[6][88] = 324; Big5Freq[16][84] = 323; Big5Freq[12][48] = 322; Big5Freq[7][68] = 321; Big5Freq[5][50] = 320; Big5Freq[13][54] = 319; Big5Freq[7][98] = 318; Big5Freq[11][6] = 317; Big5Freq[9][80] = 316; Big5Freq[16][41] = 315; Big5Freq[7][43] = 314; Big5Freq[28][117] = 313; Big5Freq[3][51] = 312; Big5Freq[7][3] = 311; Big5Freq[20][81] = 310; Big5Freq[4][2] = 309; Big5Freq[11][16] = 308; Big5Freq[10][4] = 307; Big5Freq[10][119] = 306; Big5Freq[6][142] = 305; Big5Freq[18][51] = 304; Big5Freq[8][144] = 303; Big5Freq[10][65] = 302; Big5Freq[11][64] = 301; Big5Freq[11][130] = 300; Big5Freq[9][92] = 299; Big5Freq[18][29] = 298; Big5Freq[18][78] = 297; Big5Freq[18][151] = 296; Big5Freq[33][127] = 295; Big5Freq[35][113] = 294; Big5Freq[10][155] = 293; Big5Freq[3][76] = 292; Big5Freq[36][123] = 291; Big5Freq[13][143] = 290; Big5Freq[5][135] = 289; Big5Freq[23][116] = 288; Big5Freq[6][101] = 287; Big5Freq[14][74] = 286; Big5Freq[7][153] = 285; Big5Freq[3][101] = 284; Big5Freq[9][74] = 283; Big5Freq[3][156] = 282; Big5Freq[4][147] = 281; Big5Freq[9][12] = 280; Big5Freq[18][133] = 279; Big5Freq[4][0] = 278; Big5Freq[7][155] = 277; Big5Freq[9][144] = 276; Big5Freq[23][49] = 275; Big5Freq[5][89] = 274; Big5Freq[10][11] = 273; Big5Freq[3][110] = 272; Big5Freq[3][40] = 271; Big5Freq[29][115] = 270; Big5Freq[9][100] = 269; Big5Freq[21][67] = 268; Big5Freq[23][145] = 267; Big5Freq[10][47] = 266; Big5Freq[4][31] = 265; Big5Freq[4][81] = 264; Big5Freq[22][62] = 263; Big5Freq[4][28] = 262; Big5Freq[27][39] = 261; Big5Freq[27][54] = 260; Big5Freq[32][46] = 259; Big5Freq[4][76] = 258; Big5Freq[26][15] = 257; Big5Freq[12][154] = 256; Big5Freq[9][150] = 255; Big5Freq[15][17] = 254; Big5Freq[5][129] = 253; Big5Freq[10][40] = 252; Big5Freq[13][37] = 251; Big5Freq[31][104] = 250; Big5Freq[3][152] = 249; Big5Freq[5][22] = 248; Big5Freq[8][48] = 247; Big5Freq[4][74] = 246; Big5Freq[6][17] = 245; Big5Freq[30][82] = 244; Big5Freq[4][116] = 243; Big5Freq[16][42] = 242; Big5Freq[5][55] = 241; Big5Freq[4][64] = 240; Big5Freq[14][19] = 239; Big5Freq[35][82] = 238; Big5Freq[30][139] = 237; Big5Freq[26][152] = 236; Big5Freq[32][32] = 235; Big5Freq[21][102] = 234; Big5Freq[10][131] = 233; Big5Freq[9][128] = 232; Big5Freq[3][87] = 231; Big5Freq[4][51] = 230; Big5Freq[10][15] = 229; Big5Freq[4][150] = 228; Big5Freq[7][4] = 227; Big5Freq[7][51] = 226; Big5Freq[7][157] = 225; Big5Freq[4][146] = 224; Big5Freq[4][91] = 223; Big5Freq[7][13] = 222; Big5Freq[17][116] = 221; Big5Freq[23][21] = 220; Big5Freq[5][106] = 219; Big5Freq[14][100] = 218; Big5Freq[10][152] = 217; Big5Freq[14][89] = 216; Big5Freq[6][138] = 215; Big5Freq[12][157] = 214; Big5Freq[10][102] = 213; Big5Freq[19][94] = 212; Big5Freq[7][74] = 211; Big5Freq[18][128] = 210; Big5Freq[27][111] = 209; Big5Freq[11][57] = 208; Big5Freq[3][131] = 207; Big5Freq[30][23] = 206; Big5Freq[30][126] = 205; Big5Freq[4][36] = 204; Big5Freq[26][124] = 203; Big5Freq[4][19] = 202; Big5Freq[9][152] = 201; /* * Big5Freq[5][0] = 200; Big5Freq[26][57] = 199; Big5Freq[13][155] = * 198; Big5Freq[3][38] = 197; Big5Freq[9][155] = 196; Big5Freq[28][53] * = 195; Big5Freq[15][71] = 194; Big5Freq[21][95] = 193; * Big5Freq[15][112] = 192; Big5Freq[14][138] = 191; Big5Freq[8][18] = * 190; Big5Freq[20][151] = 189; Big5Freq[37][27] = 188; * Big5Freq[32][48] = 187; Big5Freq[23][66] = 186; Big5Freq[9][2] = 185; * Big5Freq[13][133] = 184; Big5Freq[7][127] = 183; Big5Freq[3][11] = * 182; Big5Freq[12][118] = 181; Big5Freq[13][101] = 180; * Big5Freq[30][153] = 179; Big5Freq[4][65] = 178; Big5Freq[5][25] = * 177; Big5Freq[5][140] = 176; Big5Freq[6][25] = 175; Big5Freq[4][52] = * 174; Big5Freq[30][156] = 173; Big5Freq[16][13] = 172; Big5Freq[21][8] * = 171; Big5Freq[19][74] = 170; Big5Freq[15][145] = 169; * Big5Freq[9][15] = 168; Big5Freq[13][82] = 167; Big5Freq[26][86] = * 166; Big5Freq[18][52] = 165; Big5Freq[6][109] = 164; Big5Freq[10][99] * = 163; Big5Freq[18][101] = 162; Big5Freq[25][49] = 161; * Big5Freq[31][79] = 160; Big5Freq[28][20] = 159; Big5Freq[12][115] = * 158; Big5Freq[15][66] = 157; Big5Freq[11][104] = 156; * Big5Freq[23][106] = 155; Big5Freq[34][157] = 154; Big5Freq[32][94] = * 153; Big5Freq[29][88] = 152; Big5Freq[10][46] = 151; * Big5Freq[13][118] = 150; Big5Freq[20][37] = 149; Big5Freq[12][30] = * 148; Big5Freq[21][4] = 147; Big5Freq[16][33] = 146; Big5Freq[13][52] * = 145; Big5Freq[4][7] = 144; Big5Freq[21][49] = 143; Big5Freq[3][27] * = 142; Big5Freq[16][91] = 141; Big5Freq[5][155] = 140; * Big5Freq[29][130] = 139; Big5Freq[3][125] = 138; Big5Freq[14][26] = * 137; Big5Freq[15][39] = 136; Big5Freq[24][110] = 135; * Big5Freq[7][141] = 134; Big5Freq[21][15] = 133; Big5Freq[32][104] = * 132; Big5Freq[8][31] = 131; Big5Freq[34][112] = 130; Big5Freq[10][75] * = 129; Big5Freq[21][23] = 128; Big5Freq[34][131] = 127; * Big5Freq[12][3] = 126; Big5Freq[10][62] = 125; Big5Freq[9][120] = * 124; Big5Freq[32][149] = 123; Big5Freq[8][44] = 122; Big5Freq[24][2] * = 121; Big5Freq[6][148] = 120; Big5Freq[15][103] = 119; * Big5Freq[36][54] = 118; Big5Freq[36][134] = 117; Big5Freq[11][7] = * 116; Big5Freq[3][90] = 115; Big5Freq[36][73] = 114; Big5Freq[8][102] * = 113; Big5Freq[12][87] = 112; Big5Freq[25][64] = 111; Big5Freq[9][1] * = 110; Big5Freq[24][121] = 109; Big5Freq[5][75] = 108; * Big5Freq[17][83] = 107; Big5Freq[18][57] = 106; Big5Freq[8][95] = * 105; Big5Freq[14][36] = 104; Big5Freq[28][113] = 103; * Big5Freq[12][56] = 102; Big5Freq[14][61] = 101; Big5Freq[25][138] = * 100; Big5Freq[4][34] = 99; Big5Freq[11][152] = 98; Big5Freq[35][0] = * 97; Big5Freq[4][15] = 96; Big5Freq[8][82] = 95; Big5Freq[20][73] = * 94; Big5Freq[25][52] = 93; Big5Freq[24][6] = 92; Big5Freq[21][78] = * 91; Big5Freq[17][32] = 90; Big5Freq[17][91] = 89; Big5Freq[5][76] = * 88; Big5Freq[15][60] = 87; Big5Freq[15][150] = 86; Big5Freq[5][80] = * 85; Big5Freq[15][81] = 84; Big5Freq[28][108] = 83; Big5Freq[18][14] = * 82; Big5Freq[19][109] = 81; Big5Freq[28][133] = 80; Big5Freq[21][97] * = 79; Big5Freq[5][105] = 78; Big5Freq[18][114] = 77; Big5Freq[16][95] * = 76; Big5Freq[5][51] = 75; Big5Freq[3][148] = 74; Big5Freq[22][102] * = 73; Big5Freq[4][123] = 72; Big5Freq[8][88] = 71; Big5Freq[25][111] * = 70; Big5Freq[8][149] = 69; Big5Freq[9][48] = 68; Big5Freq[16][126] * = 67; Big5Freq[33][150] = 66; Big5Freq[9][54] = 65; Big5Freq[29][104] * = 64; Big5Freq[3][3] = 63; Big5Freq[11][49] = 62; Big5Freq[24][109] = * 61; Big5Freq[28][116] = 60; Big5Freq[34][113] = 59; Big5Freq[5][3] = * 58; Big5Freq[21][106] = 57; Big5Freq[4][98] = 56; Big5Freq[12][135] = * 55; Big5Freq[16][101] = 54; Big5Freq[12][147] = 53; Big5Freq[27][55] * = 52; Big5Freq[3][5] = 51; Big5Freq[11][101] = 50; Big5Freq[16][157] * = 49; Big5Freq[22][114] = 48; Big5Freq[18][46] = 47; Big5Freq[4][29] * = 46; Big5Freq[8][103] = 45; Big5Freq[16][151] = 44; Big5Freq[8][29] * = 43; Big5Freq[15][114] = 42; Big5Freq[22][70] = 41; * Big5Freq[13][121] = 40; Big5Freq[7][112] = 39; Big5Freq[20][83] = 38; * Big5Freq[3][36] = 37; Big5Freq[10][103] = 36; Big5Freq[3][96] = 35; * Big5Freq[21][79] = 34; Big5Freq[25][120] = 33; Big5Freq[29][121] = * 32; Big5Freq[23][71] = 31; Big5Freq[21][22] = 30; Big5Freq[18][89] = * 29; Big5Freq[25][104] = 28; Big5Freq[10][124] = 27; Big5Freq[26][4] = * 26; Big5Freq[21][136] = 25; Big5Freq[6][112] = 24; Big5Freq[12][103] * = 23; Big5Freq[17][66] = 22; Big5Freq[13][151] = 21; * Big5Freq[33][152] = 20; Big5Freq[11][148] = 19; Big5Freq[13][57] = * 18; Big5Freq[13][41] = 17; Big5Freq[7][60] = 16; Big5Freq[21][29] = * 15; Big5Freq[9][157] = 14; Big5Freq[24][95] = 13; Big5Freq[15][148] = * 12; Big5Freq[15][122] = 11; Big5Freq[6][125] = 10; Big5Freq[11][25] = * 9; Big5Freq[20][55] = 8; Big5Freq[19][84] = 7; Big5Freq[21][82] = 6; * Big5Freq[24][3] = 5; Big5Freq[13][70] = 4; Big5Freq[6][21] = 3; * Big5Freq[21][86] = 2; Big5Freq[12][23] = 1; Big5Freq[3][85] = 0; * EUC_TWFreq[45][90] = 600; */ Big5PFreq[41][122] = 600; Big5PFreq[35][0] = 599; Big5PFreq[43][15] = 598; Big5PFreq[35][99] = 597; Big5PFreq[35][6] = 596; Big5PFreq[35][8] = 595; Big5PFreq[38][154] = 594; Big5PFreq[37][34] = 593; Big5PFreq[37][115] = 592; Big5PFreq[36][12] = 591; Big5PFreq[18][77] = 590; Big5PFreq[35][100] = 589; Big5PFreq[35][42] = 588; Big5PFreq[120][75] = 587; Big5PFreq[35][23] = 586; Big5PFreq[13][72] = 585; Big5PFreq[0][67] = 584; Big5PFreq[39][172] = 583; Big5PFreq[22][182] = 582; Big5PFreq[15][186] = 581; Big5PFreq[15][165] = 580; Big5PFreq[35][44] = 579; Big5PFreq[40][13] = 578; Big5PFreq[38][1] = 577; Big5PFreq[37][33] = 576; Big5PFreq[36][24] = 575; Big5PFreq[56][4] = 574; Big5PFreq[35][29] = 573; Big5PFreq[9][96] = 572; Big5PFreq[37][62] = 571; Big5PFreq[48][47] = 570; Big5PFreq[51][14] = 569; Big5PFreq[39][122] = 568; Big5PFreq[44][46] = 567; Big5PFreq[35][21] = 566; Big5PFreq[36][8] = 565; Big5PFreq[36][141] = 564; Big5PFreq[3][81] = 563; Big5PFreq[37][155] = 562; Big5PFreq[42][84] = 561; Big5PFreq[36][40] = 560; Big5PFreq[35][103] = 559; Big5PFreq[11][84] = 558; Big5PFreq[45][33] = 557; Big5PFreq[121][79] = 556; Big5PFreq[2][77] = 555; Big5PFreq[36][41] = 554; Big5PFreq[37][47] = 553; Big5PFreq[39][125] = 552; Big5PFreq[37][26] = 551; Big5PFreq[35][48] = 550; Big5PFreq[35][28] = 549; Big5PFreq[35][159] = 548; Big5PFreq[37][40] = 547; Big5PFreq[35][145] = 546; Big5PFreq[37][147] = 545; Big5PFreq[46][160] = 544; Big5PFreq[37][46] = 543; Big5PFreq[50][99] = 542; Big5PFreq[52][13] = 541; Big5PFreq[10][82] = 540; Big5PFreq[35][169] = 539; Big5PFreq[35][31] = 538; Big5PFreq[47][31] = 537; Big5PFreq[18][79] = 536; Big5PFreq[16][113] = 535; Big5PFreq[37][104] = 534; Big5PFreq[39][134] = 533; Big5PFreq[36][53] = 532; Big5PFreq[38][0] = 531; Big5PFreq[4][86] = 530; Big5PFreq[54][17] = 529; Big5PFreq[43][157] = 528; Big5PFreq[35][165] = 527; Big5PFreq[69][147] = 526; Big5PFreq[117][95] = 525; Big5PFreq[35][162] = 524; Big5PFreq[35][17] = 523; Big5PFreq[36][142] = 522; Big5PFreq[36][4] = 521; Big5PFreq[37][166] = 520; Big5PFreq[35][168] = 519; Big5PFreq[35][19] = 518; Big5PFreq[37][48] = 517; Big5PFreq[42][37] = 516; Big5PFreq[40][146] = 515; Big5PFreq[36][123] = 514; Big5PFreq[22][41] = 513; Big5PFreq[20][119] = 512; Big5PFreq[2][74] = 511; Big5PFreq[44][113] = 510; Big5PFreq[35][125] = 509; Big5PFreq[37][16] = 508; Big5PFreq[35][20] = 507; Big5PFreq[35][55] = 506; Big5PFreq[37][145] = 505; Big5PFreq[0][88] = 504; Big5PFreq[3][94] = 503; Big5PFreq[6][65] = 502; Big5PFreq[26][15] = 501; Big5PFreq[41][126] = 500; Big5PFreq[36][129] = 499; Big5PFreq[31][75] = 498; Big5PFreq[19][61] = 497; Big5PFreq[35][128] = 496; Big5PFreq[29][79] = 495; Big5PFreq[36][62] = 494; Big5PFreq[37][189] = 493; Big5PFreq[39][109] = 492; Big5PFreq[39][135] = 491; Big5PFreq[72][15] = 490; Big5PFreq[47][106] = 489; Big5PFreq[54][14] = 488; Big5PFreq[24][52] = 487; Big5PFreq[38][162] = 486; Big5PFreq[41][43] = 485; Big5PFreq[37][121] = 484; Big5PFreq[14][66] = 483; Big5PFreq[37][30] = 482; Big5PFreq[35][7] = 481; Big5PFreq[49][58] = 480; Big5PFreq[43][188] = 479; Big5PFreq[24][66] = 478; Big5PFreq[35][171] = 477; Big5PFreq[40][186] = 476; Big5PFreq[39][164] = 475; Big5PFreq[78][186] = 474; Big5PFreq[8][72] = 473; Big5PFreq[36][190] = 472; Big5PFreq[35][53] = 471; Big5PFreq[35][54] = 470; Big5PFreq[22][159] = 469; Big5PFreq[35][9] = 468; Big5PFreq[41][140] = 467; Big5PFreq[37][22] = 466; Big5PFreq[48][97] = 465; Big5PFreq[50][97] = 464; Big5PFreq[36][127] = 463; Big5PFreq[37][23] = 462; Big5PFreq[40][55] = 461; Big5PFreq[35][43] = 460; Big5PFreq[26][22] = 459; Big5PFreq[35][15] = 458; Big5PFreq[72][179] = 457; Big5PFreq[20][129] = 456; Big5PFreq[52][101] = 455; Big5PFreq[35][12] = 454; Big5PFreq[42][156] = 453; Big5PFreq[15][157] = 452; Big5PFreq[50][140] = 451; Big5PFreq[26][28] = 450; Big5PFreq[54][51] = 449; Big5PFreq[35][112] = 448; Big5PFreq[36][116] = 447; Big5PFreq[42][11] = 446; Big5PFreq[37][172] = 445; Big5PFreq[37][29] = 444; Big5PFreq[44][107] = 443; Big5PFreq[50][17] = 442; Big5PFreq[39][107] = 441; Big5PFreq[19][109] = 440; Big5PFreq[36][60] = 439; Big5PFreq[49][132] = 438; Big5PFreq[26][16] = 437; Big5PFreq[43][155] = 436; Big5PFreq[37][120] = 435; Big5PFreq[15][159] = 434; Big5PFreq[43][6] = 433; Big5PFreq[45][188] = 432; Big5PFreq[35][38] = 431; Big5PFreq[39][143] = 430; Big5PFreq[48][144] = 429; Big5PFreq[37][168] = 428; Big5PFreq[37][1] = 427; Big5PFreq[36][109] = 426; Big5PFreq[46][53] = 425; Big5PFreq[38][54] = 424; Big5PFreq[36][0] = 423; Big5PFreq[72][33] = 422; Big5PFreq[42][8] = 421; Big5PFreq[36][31] = 420; Big5PFreq[35][150] = 419; Big5PFreq[118][93] = 418; Big5PFreq[37][61] = 417; Big5PFreq[0][85] = 416; Big5PFreq[36][27] = 415; Big5PFreq[35][134] = 414; Big5PFreq[36][145] = 413; Big5PFreq[6][96] = 412; Big5PFreq[36][14] = 411; Big5PFreq[16][36] = 410; Big5PFreq[15][175] = 409; Big5PFreq[35][10] = 408; Big5PFreq[36][189] = 407; Big5PFreq[35][51] = 406; Big5PFreq[35][109] = 405; Big5PFreq[35][147] = 404; Big5PFreq[35][180] = 403; Big5PFreq[72][5] = 402; Big5PFreq[36][107] = 401; Big5PFreq[49][116] = 400; Big5PFreq[73][30] = 399; Big5PFreq[6][90] = 398; Big5PFreq[2][70] = 397; Big5PFreq[17][141] = 396; Big5PFreq[35][62] = 395; Big5PFreq[16][180] = 394; Big5PFreq[4][91] = 393; Big5PFreq[15][171] = 392; Big5PFreq[35][177] = 391; Big5PFreq[37][173] = 390; Big5PFreq[16][121] = 389; Big5PFreq[35][5] = 388; Big5PFreq[46][122] = 387; Big5PFreq[40][138] = 386; Big5PFreq[50][49] = 385; Big5PFreq[36][152] = 384; Big5PFreq[13][43] = 383; Big5PFreq[9][88] = 382; Big5PFreq[36][159] = 381; Big5PFreq[27][62] = 380; Big5PFreq[40][18] = 379; Big5PFreq[17][129] = 378; Big5PFreq[43][97] = 377; Big5PFreq[13][131] = 376; Big5PFreq[46][107] = 375; Big5PFreq[60][64] = 374; Big5PFreq[36][179] = 373; Big5PFreq[37][55] = 372; Big5PFreq[41][173] = 371; Big5PFreq[44][172] = 370; Big5PFreq[23][187] = 369; Big5PFreq[36][149] = 368; Big5PFreq[17][125] = 367; Big5PFreq[55][180] = 366; Big5PFreq[51][129] = 365; Big5PFreq[36][51] = 364; Big5PFreq[37][122] = 363; Big5PFreq[48][32] = 362; Big5PFreq[51][99] = 361; Big5PFreq[54][16] = 360; Big5PFreq[41][183] = 359; Big5PFreq[37][179] = 358; Big5PFreq[38][179] = 357; Big5PFreq[35][143] = 356; Big5PFreq[37][24] = 355; Big5PFreq[40][177] = 354; Big5PFreq[47][117] = 353; Big5PFreq[39][52] = 352; Big5PFreq[22][99] = 351; Big5PFreq[40][142] = 350; Big5PFreq[36][49] = 349; Big5PFreq[38][17] = 348; Big5PFreq[39][188] = 347; Big5PFreq[36][186] = 346; Big5PFreq[35][189] = 345; Big5PFreq[41][7] = 344; Big5PFreq[18][91] = 343; Big5PFreq[43][137] = 342; Big5PFreq[35][142] = 341; Big5PFreq[35][117] = 340; Big5PFreq[39][138] = 339; Big5PFreq[16][59] = 338; Big5PFreq[39][174] = 337; Big5PFreq[55][145] = 336; Big5PFreq[37][21] = 335; Big5PFreq[36][180] = 334; Big5PFreq[37][156] = 333; Big5PFreq[49][13] = 332; Big5PFreq[41][107] = 331; Big5PFreq[36][56] = 330; Big5PFreq[53][8] = 329; Big5PFreq[22][114] = 328; Big5PFreq[5][95] = 327; Big5PFreq[37][0] = 326; Big5PFreq[26][183] = 325; Big5PFreq[22][66] = 324; Big5PFreq[35][58] = 323; Big5PFreq[48][117] = 322; Big5PFreq[36][102] = 321; Big5PFreq[22][122] = 320; Big5PFreq[35][11] = 319; Big5PFreq[46][19] = 318; Big5PFreq[22][49] = 317; Big5PFreq[48][166] = 316; Big5PFreq[41][125] = 315; Big5PFreq[41][1] = 314; Big5PFreq[35][178] = 313; Big5PFreq[41][12] = 312; Big5PFreq[26][167] = 311; Big5PFreq[42][152] = 310; Big5PFreq[42][46] = 309; Big5PFreq[42][151] = 308; Big5PFreq[20][135] = 307; Big5PFreq[37][162] = 306; Big5PFreq[37][50] = 305; Big5PFreq[22][185] = 304; Big5PFreq[36][166] = 303; Big5PFreq[19][40] = 302; Big5PFreq[22][107] = 301; Big5PFreq[22][102] = 300; Big5PFreq[57][162] = 299; Big5PFreq[22][124] = 298; Big5PFreq[37][138] = 297; Big5PFreq[37][25] = 296; Big5PFreq[0][69] = 295; Big5PFreq[43][172] = 294; Big5PFreq[42][167] = 293; Big5PFreq[35][120] = 292; Big5PFreq[41][128] = 291; Big5PFreq[2][88] = 290; Big5PFreq[20][123] = 289; Big5PFreq[35][123] = 288; Big5PFreq[36][28] = 287; Big5PFreq[42][188] = 286; Big5PFreq[42][164] = 285; Big5PFreq[42][4] = 284; Big5PFreq[43][57] = 283; Big5PFreq[39][3] = 282; Big5PFreq[42][3] = 281; Big5PFreq[57][158] = 280; Big5PFreq[35][146] = 279; Big5PFreq[24][54] = 278; Big5PFreq[13][110] = 277; Big5PFreq[23][132] = 276; Big5PFreq[26][102] = 275; Big5PFreq[55][178] = 274; Big5PFreq[17][117] = 273; Big5PFreq[41][161] = 272; Big5PFreq[38][150] = 271; Big5PFreq[10][71] = 270; Big5PFreq[47][60] = 269; Big5PFreq[16][114] = 268; Big5PFreq[21][47] = 267; Big5PFreq[39][101] = 266; Big5PFreq[18][45] = 265; Big5PFreq[40][121] = 264; Big5PFreq[45][41] = 263; Big5PFreq[22][167] = 262; Big5PFreq[26][149] = 261; Big5PFreq[15][189] = 260; Big5PFreq[41][177] = 259; Big5PFreq[46][36] = 258; Big5PFreq[20][40] = 257; Big5PFreq[41][54] = 256; Big5PFreq[3][87] = 255; Big5PFreq[40][16] = 254; Big5PFreq[42][15] = 253; Big5PFreq[11][83] = 252; Big5PFreq[0][94] = 251; Big5PFreq[122][81] = 250; Big5PFreq[41][26] = 249; Big5PFreq[36][34] = 248; Big5PFreq[44][148] = 247; Big5PFreq[35][3] = 246; Big5PFreq[36][114] = 245; Big5PFreq[42][112] = 244; Big5PFreq[35][183] = 243; Big5PFreq[49][73] = 242; Big5PFreq[39][2] = 241; Big5PFreq[38][121] = 240; Big5PFreq[44][114] = 239; Big5PFreq[49][32] = 238; Big5PFreq[1][65] = 237; Big5PFreq[38][25] = 236; Big5PFreq[39][4] = 235; Big5PFreq[42][62] = 234; Big5PFreq[35][40] = 233; Big5PFreq[24][2] = 232; Big5PFreq[53][49] = 231; Big5PFreq[41][133] = 230; Big5PFreq[43][134] = 229; Big5PFreq[3][83] = 228; Big5PFreq[38][158] = 227; Big5PFreq[24][17] = 226; Big5PFreq[52][59] = 225; Big5PFreq[38][41] = 224; Big5PFreq[37][127] = 223; Big5PFreq[22][175] = 222; Big5PFreq[44][30] = 221; Big5PFreq[47][178] = 220; Big5PFreq[43][99] = 219; Big5PFreq[19][4] = 218; Big5PFreq[37][97] = 217; Big5PFreq[38][181] = 216; Big5PFreq[45][103] = 215; Big5PFreq[1][86] = 214; Big5PFreq[40][15] = 213; Big5PFreq[22][136] = 212; Big5PFreq[75][165] = 211; Big5PFreq[36][15] = 210; Big5PFreq[46][80] = 209; Big5PFreq[59][55] = 208; Big5PFreq[37][108] = 207; Big5PFreq[21][109] = 206; Big5PFreq[24][165] = 205; Big5PFreq[79][158] = 204; Big5PFreq[44][139] = 203; Big5PFreq[36][124] = 202; Big5PFreq[42][185] = 201; Big5PFreq[39][186] = 200; Big5PFreq[22][128] = 199; Big5PFreq[40][44] = 198; Big5PFreq[41][105] = 197; Big5PFreq[1][70] = 196; Big5PFreq[1][68] = 195; Big5PFreq[53][22] = 194; Big5PFreq[36][54] = 193; Big5PFreq[47][147] = 192; Big5PFreq[35][36] = 191; Big5PFreq[35][185] = 190; Big5PFreq[45][37] = 189; Big5PFreq[43][163] = 188; Big5PFreq[56][115] = 187; Big5PFreq[38][164] = 186; Big5PFreq[35][141] = 185; Big5PFreq[42][132] = 184; Big5PFreq[46][120] = 183; Big5PFreq[69][142] = 182; Big5PFreq[38][175] = 181; Big5PFreq[22][112] = 180; Big5PFreq[38][142] = 179; Big5PFreq[40][37] = 178; Big5PFreq[37][109] = 177; Big5PFreq[40][144] = 176; Big5PFreq[44][117] = 175; Big5PFreq[35][181] = 174; Big5PFreq[26][105] = 173; Big5PFreq[16][48] = 172; Big5PFreq[44][122] = 171; Big5PFreq[12][86] = 170; Big5PFreq[84][53] = 169; Big5PFreq[17][44] = 168; Big5PFreq[59][54] = 167; Big5PFreq[36][98] = 166; Big5PFreq[45][115] = 165; Big5PFreq[73][9] = 164; Big5PFreq[44][123] = 163; Big5PFreq[37][188] = 162; Big5PFreq[51][117] = 161; Big5PFreq[15][156] = 160; Big5PFreq[36][155] = 159; Big5PFreq[44][25] = 158; Big5PFreq[38][12] = 157; Big5PFreq[38][140] = 156; Big5PFreq[23][4] = 155; Big5PFreq[45][149] = 154; Big5PFreq[22][189] = 153; Big5PFreq[38][147] = 152; Big5PFreq[27][5] = 151; Big5PFreq[22][42] = 150; Big5PFreq[3][68] = 149; Big5PFreq[39][51] = 148; Big5PFreq[36][29] = 147; Big5PFreq[20][108] = 146; Big5PFreq[50][57] = 145; Big5PFreq[55][104] = 144; Big5PFreq[22][46] = 143; Big5PFreq[18][164] = 142; Big5PFreq[50][159] = 141; Big5PFreq[85][131] = 140; Big5PFreq[26][79] = 139; Big5PFreq[38][100] = 138; Big5PFreq[53][112] = 137; Big5PFreq[20][190] = 136; Big5PFreq[14][69] = 135; Big5PFreq[23][11] = 134; Big5PFreq[40][114] = 133; Big5PFreq[40][148] = 132; Big5PFreq[53][130] = 131; Big5PFreq[36][2] = 130; Big5PFreq[66][82] = 129; Big5PFreq[45][166] = 128; Big5PFreq[4][88] = 127; Big5PFreq[16][57] = 126; Big5PFreq[22][116] = 125; Big5PFreq[36][108] = 124; Big5PFreq[13][48] = 123; Big5PFreq[54][12] = 122; Big5PFreq[40][136] = 121; Big5PFreq[36][128] = 120; Big5PFreq[23][6] = 119; Big5PFreq[38][125] = 118; Big5PFreq[45][154] = 117; Big5PFreq[51][127] = 116; Big5PFreq[44][163] = 115; Big5PFreq[16][173] = 114; Big5PFreq[43][49] = 113; Big5PFreq[20][112] = 112; Big5PFreq[15][168] = 111; Big5PFreq[35][129] = 110; Big5PFreq[20][45] = 109; Big5PFreq[38][10] = 108; Big5PFreq[57][171] = 107; Big5PFreq[44][190] = 106; Big5PFreq[40][56] = 105; Big5PFreq[36][156] = 104; Big5PFreq[3][88] = 103; Big5PFreq[50][122] = 102; Big5PFreq[36][7] = 101; Big5PFreq[39][43] = 100; Big5PFreq[15][166] = 99; Big5PFreq[42][136] = 98; Big5PFreq[22][131] = 97; Big5PFreq[44][23] = 96; Big5PFreq[54][147] = 95; Big5PFreq[41][32] = 94; Big5PFreq[23][121] = 93; Big5PFreq[39][108] = 92; Big5PFreq[2][78] = 91; Big5PFreq[40][155] = 90; Big5PFreq[55][51] = 89; Big5PFreq[19][34] = 88; Big5PFreq[48][128] = 87; Big5PFreq[48][159] = 86; Big5PFreq[20][70] = 85; Big5PFreq[34][71] = 84; Big5PFreq[16][31] = 83; Big5PFreq[42][157] = 82; Big5PFreq[20][44] = 81; Big5PFreq[11][92] = 80; Big5PFreq[44][180] = 79; Big5PFreq[84][33] = 78; Big5PFreq[16][116] = 77; Big5PFreq[61][163] = 76; Big5PFreq[35][164] = 75; Big5PFreq[36][42] = 74; Big5PFreq[13][40] = 73; Big5PFreq[43][176] = 72; Big5PFreq[2][66] = 71; Big5PFreq[20][133] = 70; Big5PFreq[36][65] = 69; Big5PFreq[38][33] = 68; Big5PFreq[12][91] = 67; Big5PFreq[36][26] = 66; Big5PFreq[15][174] = 65; Big5PFreq[77][32] = 64; Big5PFreq[16][1] = 63; Big5PFreq[25][86] = 62; Big5PFreq[17][13] = 61; Big5PFreq[5][75] = 60; Big5PFreq[36][52] = 59; Big5PFreq[51][164] = 58; Big5PFreq[12][85] = 57; Big5PFreq[39][168] = 56; Big5PFreq[43][16] = 55; Big5PFreq[40][69] = 54; Big5PFreq[26][108] = 53; Big5PFreq[51][56] = 52; Big5PFreq[16][37] = 51; Big5PFreq[40][29] = 50; Big5PFreq[46][171] = 49; Big5PFreq[40][128] = 48; Big5PFreq[72][114] = 47; Big5PFreq[21][103] = 46; Big5PFreq[22][44] = 45; Big5PFreq[40][115] = 44; Big5PFreq[43][7] = 43; Big5PFreq[43][153] = 42; Big5PFreq[17][20] = 41; Big5PFreq[16][49] = 40; Big5PFreq[36][57] = 39; Big5PFreq[18][38] = 38; Big5PFreq[45][184] = 37; Big5PFreq[37][167] = 36; Big5PFreq[26][106] = 35; Big5PFreq[61][121] = 34; Big5PFreq[89][140] = 33; Big5PFreq[46][61] = 32; Big5PFreq[39][163] = 31; Big5PFreq[40][62] = 30; Big5PFreq[38][165] = 29; Big5PFreq[47][37] = 28; Big5PFreq[18][155] = 27; Big5PFreq[20][33] = 26; Big5PFreq[29][90] = 25; Big5PFreq[20][103] = 24; Big5PFreq[37][51] = 23; Big5PFreq[57][0] = 22; Big5PFreq[40][31] = 21; Big5PFreq[45][32] = 20; Big5PFreq[59][23] = 19; Big5PFreq[18][47] = 18; Big5PFreq[45][134] = 17; Big5PFreq[37][59] = 16; Big5PFreq[21][128] = 15; Big5PFreq[36][106] = 14; Big5PFreq[31][39] = 13; Big5PFreq[40][182] = 12; Big5PFreq[52][155] = 11; Big5PFreq[42][166] = 10; Big5PFreq[35][27] = 9; Big5PFreq[38][3] = 8; Big5PFreq[13][44] = 7; Big5PFreq[58][157] = 6; Big5PFreq[47][51] = 5; Big5PFreq[41][37] = 4; Big5PFreq[41][172] = 3; Big5PFreq[51][165] = 2; Big5PFreq[15][161] = 1; Big5PFreq[24][181] = 0; EUC_TWFreq[48][49] = 599; EUC_TWFreq[35][65] = 598; EUC_TWFreq[41][27] = 597; EUC_TWFreq[35][0] = 596; EUC_TWFreq[39][19] = 595; EUC_TWFreq[35][42] = 594; EUC_TWFreq[38][66] = 593; EUC_TWFreq[35][8] = 592; EUC_TWFreq[35][6] = 591; EUC_TWFreq[35][66] = 590; EUC_TWFreq[43][14] = 589; EUC_TWFreq[69][80] = 588; EUC_TWFreq[50][48] = 587; EUC_TWFreq[36][71] = 586; EUC_TWFreq[37][10] = 585; EUC_TWFreq[60][52] = 584; EUC_TWFreq[51][21] = 583; EUC_TWFreq[40][2] = 582; EUC_TWFreq[67][35] = 581; EUC_TWFreq[38][78] = 580; EUC_TWFreq[49][18] = 579; EUC_TWFreq[35][23] = 578; EUC_TWFreq[42][83] = 577; EUC_TWFreq[79][47] = 576; EUC_TWFreq[61][82] = 575; EUC_TWFreq[38][7] = 574; EUC_TWFreq[35][29] = 573; EUC_TWFreq[37][77] = 572; EUC_TWFreq[54][67] = 571; EUC_TWFreq[38][80] = 570; EUC_TWFreq[52][74] = 569; EUC_TWFreq[36][37] = 568; EUC_TWFreq[74][8] = 567; EUC_TWFreq[41][83] = 566; EUC_TWFreq[36][75] = 565; EUC_TWFreq[49][63] = 564; EUC_TWFreq[42][58] = 563; EUC_TWFreq[56][33] = 562; EUC_TWFreq[37][76] = 561; EUC_TWFreq[62][39] = 560; EUC_TWFreq[35][21] = 559; EUC_TWFreq[70][19] = 558; EUC_TWFreq[77][88] = 557; EUC_TWFreq[51][14] = 556; EUC_TWFreq[36][17] = 555; EUC_TWFreq[44][51] = 554; EUC_TWFreq[38][72] = 553; EUC_TWFreq[74][90] = 552; EUC_TWFreq[35][48] = 551; EUC_TWFreq[35][69] = 550; EUC_TWFreq[66][86] = 549; EUC_TWFreq[57][20] = 548; EUC_TWFreq[35][53] = 547; EUC_TWFreq[36][87] = 546; EUC_TWFreq[84][67] = 545; EUC_TWFreq[70][56] = 544; EUC_TWFreq[71][54] = 543; EUC_TWFreq[60][70] = 542; EUC_TWFreq[80][1] = 541; EUC_TWFreq[39][59] = 540; EUC_TWFreq[39][51] = 539; EUC_TWFreq[35][44] = 538; EUC_TWFreq[48][4] = 537; EUC_TWFreq[55][24] = 536; EUC_TWFreq[52][4] = 535; EUC_TWFreq[54][26] = 534; EUC_TWFreq[36][31] = 533; EUC_TWFreq[37][22] = 532; EUC_TWFreq[37][9] = 531; EUC_TWFreq[46][0] = 530; EUC_TWFreq[56][46] = 529; EUC_TWFreq[47][93] = 528; EUC_TWFreq[37][25] = 527; EUC_TWFreq[39][8] = 526; EUC_TWFreq[46][73] = 525; EUC_TWFreq[38][48] = 524; EUC_TWFreq[39][83] = 523; EUC_TWFreq[60][92] = 522; EUC_TWFreq[70][11] = 521; EUC_TWFreq[63][84] = 520; EUC_TWFreq[38][65] = 519; EUC_TWFreq[45][45] = 518; EUC_TWFreq[63][49] = 517; EUC_TWFreq[63][50] = 516; EUC_TWFreq[39][93] = 515; EUC_TWFreq[68][20] = 514; EUC_TWFreq[44][84] = 513; EUC_TWFreq[66][34] = 512; EUC_TWFreq[37][58] = 511; EUC_TWFreq[39][0] = 510; EUC_TWFreq[59][1] = 509; EUC_TWFreq[47][8] = 508; EUC_TWFreq[61][17] = 507; EUC_TWFreq[53][87] = 506; EUC_TWFreq[67][26] = 505; EUC_TWFreq[43][46] = 504; EUC_TWFreq[38][61] = 503; EUC_TWFreq[45][9] = 502; EUC_TWFreq[66][83] = 501; EUC_TWFreq[43][88] = 500; EUC_TWFreq[85][20] = 499; EUC_TWFreq[57][36] = 498; EUC_TWFreq[43][6] = 497; EUC_TWFreq[86][77] = 496; EUC_TWFreq[42][70] = 495; EUC_TWFreq[49][78] = 494; EUC_TWFreq[36][40] = 493; EUC_TWFreq[42][71] = 492; EUC_TWFreq[58][49] = 491; EUC_TWFreq[35][20] = 490; EUC_TWFreq[76][20] = 489; EUC_TWFreq[39][25] = 488; EUC_TWFreq[40][34] = 487; EUC_TWFreq[39][76] = 486; EUC_TWFreq[40][1] = 485; EUC_TWFreq[59][0] = 484; EUC_TWFreq[39][70] = 483; EUC_TWFreq[46][14] = 482; EUC_TWFreq[68][77] = 481; EUC_TWFreq[38][55] = 480; EUC_TWFreq[35][78] = 479; EUC_TWFreq[84][44] = 478; EUC_TWFreq[36][41] = 477; EUC_TWFreq[37][62] = 476; EUC_TWFreq[65][67] = 475; EUC_TWFreq[69][66] = 474; EUC_TWFreq[73][55] = 473; EUC_TWFreq[71][49] = 472; EUC_TWFreq[66][87] = 471; EUC_TWFreq[38][33] = 470; EUC_TWFreq[64][61] = 469; EUC_TWFreq[35][7] = 468; EUC_TWFreq[47][49] = 467; EUC_TWFreq[56][14] = 466; EUC_TWFreq[36][49] = 465; EUC_TWFreq[50][81] = 464; EUC_TWFreq[55][76] = 463; EUC_TWFreq[35][19] = 462; EUC_TWFreq[44][47] = 461; EUC_TWFreq[35][15] = 460; EUC_TWFreq[82][59] = 459; EUC_TWFreq[35][43] = 458; EUC_TWFreq[73][0] = 457; EUC_TWFreq[57][83] = 456; EUC_TWFreq[42][46] = 455; EUC_TWFreq[36][0] = 454; EUC_TWFreq[70][88] = 453; EUC_TWFreq[42][22] = 452; EUC_TWFreq[46][58] = 451; EUC_TWFreq[36][34] = 450; EUC_TWFreq[39][24] = 449; EUC_TWFreq[35][55] = 448; EUC_TWFreq[44][91] = 447; EUC_TWFreq[37][51] = 446; EUC_TWFreq[36][19] = 445; EUC_TWFreq[69][90] = 444; EUC_TWFreq[55][35] = 443; EUC_TWFreq[35][54] = 442; EUC_TWFreq[49][61] = 441; EUC_TWFreq[36][67] = 440; EUC_TWFreq[88][34] = 439; EUC_TWFreq[35][17] = 438; EUC_TWFreq[65][69] = 437; EUC_TWFreq[74][89] = 436; EUC_TWFreq[37][31] = 435; EUC_TWFreq[43][48] = 434; EUC_TWFreq[89][27] = 433; EUC_TWFreq[42][79] = 432; EUC_TWFreq[69][57] = 431; EUC_TWFreq[36][13] = 430; EUC_TWFreq[35][62] = 429; EUC_TWFreq[65][47] = 428; EUC_TWFreq[56][8] = 427; EUC_TWFreq[38][79] = 426; EUC_TWFreq[37][64] = 425; EUC_TWFreq[64][64] = 424; EUC_TWFreq[38][53] = 423; EUC_TWFreq[38][31] = 422; EUC_TWFreq[56][81] = 421; EUC_TWFreq[36][22] = 420; EUC_TWFreq[43][4] = 419; EUC_TWFreq[36][90] = 418; EUC_TWFreq[38][62] = 417; EUC_TWFreq[66][85] = 416; EUC_TWFreq[39][1] = 415; EUC_TWFreq[59][40] = 414; EUC_TWFreq[58][93] = 413; EUC_TWFreq[44][43] = 412; EUC_TWFreq[39][49] = 411; EUC_TWFreq[64][2] = 410; EUC_TWFreq[41][35] = 409; EUC_TWFreq[60][22] = 408; EUC_TWFreq[35][91] = 407; EUC_TWFreq[78][1] = 406; EUC_TWFreq[36][14] = 405; EUC_TWFreq[82][29] = 404; EUC_TWFreq[52][86] = 403; EUC_TWFreq[40][16] = 402; EUC_TWFreq[91][52] = 401; EUC_TWFreq[50][75] = 400; EUC_TWFreq[64][30] = 399; EUC_TWFreq[90][78] = 398; EUC_TWFreq[36][52] = 397; EUC_TWFreq[55][87] = 396; EUC_TWFreq[57][5] = 395; EUC_TWFreq[57][31] = 394; EUC_TWFreq[42][35] = 393; EUC_TWFreq[69][50] = 392; EUC_TWFreq[45][8] = 391; EUC_TWFreq[50][87] = 390; EUC_TWFreq[69][55] = 389; EUC_TWFreq[92][3] = 388; EUC_TWFreq[36][43] = 387; EUC_TWFreq[64][10] = 386; EUC_TWFreq[56][25] = 385; EUC_TWFreq[60][68] = 384; EUC_TWFreq[51][46] = 383; EUC_TWFreq[50][0] = 382; EUC_TWFreq[38][30] = 381; EUC_TWFreq[50][85] = 380; EUC_TWFreq[60][54] = 379; EUC_TWFreq[73][6] = 378; EUC_TWFreq[73][28] = 377; EUC_TWFreq[56][19] = 376; EUC_TWFreq[62][69] = 375; EUC_TWFreq[81][66] = 374; EUC_TWFreq[40][32] = 373; EUC_TWFreq[76][31] = 372; EUC_TWFreq[35][10] = 371; EUC_TWFreq[41][37] = 370; EUC_TWFreq[52][82] = 369; EUC_TWFreq[91][72] = 368; EUC_TWFreq[37][29] = 367; EUC_TWFreq[56][30] = 366; EUC_TWFreq[37][80] = 365; EUC_TWFreq[81][56] = 364; EUC_TWFreq[70][3] = 363; EUC_TWFreq[76][15] = 362; EUC_TWFreq[46][47] = 361; EUC_TWFreq[35][88] = 360; EUC_TWFreq[61][58] = 359; EUC_TWFreq[37][37] = 358; EUC_TWFreq[57][22] = 357; EUC_TWFreq[41][23] = 356; EUC_TWFreq[90][66] = 355; EUC_TWFreq[39][60] = 354; EUC_TWFreq[38][0] = 353; EUC_TWFreq[37][87] = 352; EUC_TWFreq[46][2] = 351; EUC_TWFreq[38][56] = 350; EUC_TWFreq[58][11] = 349; EUC_TWFreq[48][10] = 348; EUC_TWFreq[74][4] = 347; EUC_TWFreq[40][42] = 346; EUC_TWFreq[41][52] = 345; EUC_TWFreq[61][92] = 344; EUC_TWFreq[39][50] = 343; EUC_TWFreq[47][88] = 342; EUC_TWFreq[88][36] = 341; EUC_TWFreq[45][73] = 340; EUC_TWFreq[82][3] = 339; EUC_TWFreq[61][36] = 338; EUC_TWFreq[60][33] = 337; EUC_TWFreq[38][27] = 336; EUC_TWFreq[35][83] = 335; EUC_TWFreq[65][24] = 334; EUC_TWFreq[73][10] = 333; EUC_TWFreq[41][13] = 332; EUC_TWFreq[50][27] = 331; EUC_TWFreq[59][50] = 330; EUC_TWFreq[42][45] = 329; EUC_TWFreq[55][19] = 328; EUC_TWFreq[36][77] = 327; EUC_TWFreq[69][31] = 326; EUC_TWFreq[60][7] = 325; EUC_TWFreq[40][88] = 324; EUC_TWFreq[57][56] = 323; EUC_TWFreq[50][50] = 322; EUC_TWFreq[42][37] = 321; EUC_TWFreq[38][82] = 320; EUC_TWFreq[52][25] = 319; EUC_TWFreq[42][67] = 318; EUC_TWFreq[48][40] = 317; EUC_TWFreq[45][81] = 316; EUC_TWFreq[57][14] = 315; EUC_TWFreq[42][13] = 314; EUC_TWFreq[78][0] = 313; EUC_TWFreq[35][51] = 312; EUC_TWFreq[41][67] = 311; EUC_TWFreq[64][23] = 310; EUC_TWFreq[36][65] = 309; EUC_TWFreq[48][50] = 308; EUC_TWFreq[46][69] = 307; EUC_TWFreq[47][89] = 306; EUC_TWFreq[41][48] = 305; EUC_TWFreq[60][56] = 304; EUC_TWFreq[44][82] = 303; EUC_TWFreq[47][35] = 302; EUC_TWFreq[49][3] = 301; EUC_TWFreq[49][69] = 300; EUC_TWFreq[45][93] = 299; EUC_TWFreq[60][34] = 298; EUC_TWFreq[60][82] = 297; EUC_TWFreq[61][61] = 296; EUC_TWFreq[86][42] = 295; EUC_TWFreq[89][60] = 294; EUC_TWFreq[48][31] = 293; EUC_TWFreq[35][75] = 292; EUC_TWFreq[91][39] = 291; EUC_TWFreq[53][19] = 290; EUC_TWFreq[39][72] = 289; EUC_TWFreq[69][59] = 288; EUC_TWFreq[41][7] = 287; EUC_TWFreq[54][13] = 286; EUC_TWFreq[43][28] = 285; EUC_TWFreq[36][6] = 284; EUC_TWFreq[45][75] = 283; EUC_TWFreq[36][61] = 282; EUC_TWFreq[38][21] = 281; EUC_TWFreq[45][14] = 280; EUC_TWFreq[61][43] = 279; EUC_TWFreq[36][63] = 278; EUC_TWFreq[43][30] = 277; EUC_TWFreq[46][51] = 276; EUC_TWFreq[68][87] = 275; EUC_TWFreq[39][26] = 274; EUC_TWFreq[46][76] = 273; EUC_TWFreq[36][15] = 272; EUC_TWFreq[35][40] = 271; EUC_TWFreq[79][60] = 270; EUC_TWFreq[46][7] = 269; EUC_TWFreq[65][72] = 268; EUC_TWFreq[69][88] = 267; EUC_TWFreq[47][18] = 266; EUC_TWFreq[37][0] = 265; EUC_TWFreq[37][49] = 264; EUC_TWFreq[67][37] = 263; EUC_TWFreq[36][91] = 262; EUC_TWFreq[75][48] = 261; EUC_TWFreq[75][63] = 260; EUC_TWFreq[83][87] = 259; EUC_TWFreq[37][44] = 258; EUC_TWFreq[73][54] = 257; EUC_TWFreq[51][61] = 256; EUC_TWFreq[46][57] = 255; EUC_TWFreq[55][21] = 254; EUC_TWFreq[39][66] = 253; EUC_TWFreq[47][11] = 252; EUC_TWFreq[52][8] = 251; EUC_TWFreq[82][81] = 250; EUC_TWFreq[36][57] = 249; EUC_TWFreq[38][54] = 248; EUC_TWFreq[43][81] = 247; EUC_TWFreq[37][42] = 246; EUC_TWFreq[40][18] = 245; EUC_TWFreq[80][90] = 244; EUC_TWFreq[37][84] = 243; EUC_TWFreq[57][15] = 242; EUC_TWFreq[38][87] = 241; EUC_TWFreq[37][32] = 240; EUC_TWFreq[53][53] = 239; EUC_TWFreq[89][29] = 238; EUC_TWFreq[81][53] = 237; EUC_TWFreq[75][3] = 236; EUC_TWFreq[83][73] = 235; EUC_TWFreq[66][13] = 234; EUC_TWFreq[48][7] = 233; EUC_TWFreq[46][35] = 232; EUC_TWFreq[35][86] = 231; EUC_TWFreq[37][20] = 230; EUC_TWFreq[46][80] = 229; EUC_TWFreq[38][24] = 228; EUC_TWFreq[41][68] = 227; EUC_TWFreq[42][21] = 226; EUC_TWFreq[43][32] = 225; EUC_TWFreq[38][20] = 224; EUC_TWFreq[37][59] = 223; EUC_TWFreq[41][77] = 222; EUC_TWFreq[59][57] = 221; EUC_TWFreq[68][59] = 220; EUC_TWFreq[39][43] = 219; EUC_TWFreq[54][39] = 218; EUC_TWFreq[48][28] = 217; EUC_TWFreq[54][28] = 216; EUC_TWFreq[41][44] = 215; EUC_TWFreq[51][64] = 214; EUC_TWFreq[47][72] = 213; EUC_TWFreq[62][67] = 212; EUC_TWFreq[42][43] = 211; EUC_TWFreq[61][38] = 210; EUC_TWFreq[76][25] = 209; EUC_TWFreq[48][91] = 208; EUC_TWFreq[36][36] = 207; EUC_TWFreq[80][32] = 206; EUC_TWFreq[81][40] = 205; EUC_TWFreq[37][5] = 204; EUC_TWFreq[74][69] = 203; EUC_TWFreq[36][82] = 202; EUC_TWFreq[46][59] = 201; /* * EUC_TWFreq[38][32] = 200; EUC_TWFreq[74][2] = 199; EUC_TWFreq[53][31] * = 198; EUC_TWFreq[35][38] = 197; EUC_TWFreq[46][62] = 196; * EUC_TWFreq[77][31] = 195; EUC_TWFreq[55][74] = 194; EUC_TWFreq[66][6] * = 193; EUC_TWFreq[56][21] = 192; EUC_TWFreq[54][78] = 191; * EUC_TWFreq[43][51] = 190; EUC_TWFreq[64][93] = 189; EUC_TWFreq[92][7] * = 188; EUC_TWFreq[83][89] = 187; EUC_TWFreq[69][9] = 186; * EUC_TWFreq[45][4] = 185; EUC_TWFreq[53][9] = 184; EUC_TWFreq[43][2] = * 183; EUC_TWFreq[35][11] = 182; EUC_TWFreq[51][25] = 181; * EUC_TWFreq[52][71] = 180; EUC_TWFreq[81][67] = 179; * EUC_TWFreq[37][33] = 178; EUC_TWFreq[38][57] = 177; * EUC_TWFreq[39][77] = 176; EUC_TWFreq[40][26] = 175; * EUC_TWFreq[37][21] = 174; EUC_TWFreq[81][70] = 173; * EUC_TWFreq[56][80] = 172; EUC_TWFreq[65][14] = 171; * EUC_TWFreq[62][47] = 170; EUC_TWFreq[56][54] = 169; * EUC_TWFreq[45][17] = 168; EUC_TWFreq[52][52] = 167; * EUC_TWFreq[74][30] = 166; EUC_TWFreq[60][57] = 165; * EUC_TWFreq[41][15] = 164; EUC_TWFreq[47][69] = 163; * EUC_TWFreq[61][11] = 162; EUC_TWFreq[72][25] = 161; * EUC_TWFreq[82][56] = 160; EUC_TWFreq[76][92] = 159; * EUC_TWFreq[51][22] = 158; EUC_TWFreq[55][69] = 157; * EUC_TWFreq[49][43] = 156; EUC_TWFreq[69][49] = 155; * EUC_TWFreq[88][42] = 154; EUC_TWFreq[84][41] = 153; * EUC_TWFreq[79][33] = 152; EUC_TWFreq[47][17] = 151; * EUC_TWFreq[52][88] = 150; EUC_TWFreq[63][74] = 149; * EUC_TWFreq[50][32] = 148; EUC_TWFreq[65][10] = 147; EUC_TWFreq[57][6] * = 146; EUC_TWFreq[52][23] = 145; EUC_TWFreq[36][70] = 144; * EUC_TWFreq[65][55] = 143; EUC_TWFreq[35][27] = 142; * EUC_TWFreq[57][63] = 141; EUC_TWFreq[39][92] = 140; * EUC_TWFreq[79][75] = 139; EUC_TWFreq[36][30] = 138; * EUC_TWFreq[53][60] = 137; EUC_TWFreq[55][43] = 136; * EUC_TWFreq[71][22] = 135; EUC_TWFreq[43][16] = 134; * EUC_TWFreq[65][21] = 133; EUC_TWFreq[84][51] = 132; * EUC_TWFreq[43][64] = 131; EUC_TWFreq[87][91] = 130; * EUC_TWFreq[47][45] = 129; EUC_TWFreq[65][29] = 128; * EUC_TWFreq[88][16] = 127; EUC_TWFreq[50][5] = 126; EUC_TWFreq[47][33] * = 125; EUC_TWFreq[46][27] = 124; EUC_TWFreq[85][2] = 123; * EUC_TWFreq[43][77] = 122; EUC_TWFreq[70][9] = 121; EUC_TWFreq[41][54] * = 120; EUC_TWFreq[56][12] = 119; EUC_TWFreq[90][65] = 118; * EUC_TWFreq[91][50] = 117; EUC_TWFreq[48][41] = 116; * EUC_TWFreq[35][89] = 115; EUC_TWFreq[90][83] = 114; * EUC_TWFreq[44][40] = 113; EUC_TWFreq[50][88] = 112; * EUC_TWFreq[72][39] = 111; EUC_TWFreq[45][3] = 110; EUC_TWFreq[71][33] * = 109; EUC_TWFreq[39][12] = 108; EUC_TWFreq[59][24] = 107; * EUC_TWFreq[60][62] = 106; EUC_TWFreq[44][33] = 105; * EUC_TWFreq[53][70] = 104; EUC_TWFreq[77][90] = 103; * EUC_TWFreq[50][58] = 102; EUC_TWFreq[54][1] = 101; EUC_TWFreq[73][19] * = 100; EUC_TWFreq[37][3] = 99; EUC_TWFreq[49][91] = 98; * EUC_TWFreq[88][43] = 97; EUC_TWFreq[36][78] = 96; EUC_TWFreq[44][20] * = 95; EUC_TWFreq[64][15] = 94; EUC_TWFreq[72][28] = 93; * EUC_TWFreq[70][13] = 92; EUC_TWFreq[65][83] = 91; EUC_TWFreq[58][68] * = 90; EUC_TWFreq[59][32] = 89; EUC_TWFreq[39][13] = 88; * EUC_TWFreq[55][64] = 87; EUC_TWFreq[56][59] = 86; EUC_TWFreq[39][17] * = 85; EUC_TWFreq[55][84] = 84; EUC_TWFreq[77][85] = 83; * EUC_TWFreq[60][19] = 82; EUC_TWFreq[62][82] = 81; EUC_TWFreq[78][16] * = 80; EUC_TWFreq[66][8] = 79; EUC_TWFreq[39][42] = 78; * EUC_TWFreq[61][24] = 77; EUC_TWFreq[57][67] = 76; EUC_TWFreq[38][83] * = 75; EUC_TWFreq[36][53] = 74; EUC_TWFreq[67][76] = 73; * EUC_TWFreq[37][91] = 72; EUC_TWFreq[44][26] = 71; EUC_TWFreq[72][86] * = 70; EUC_TWFreq[44][87] = 69; EUC_TWFreq[45][50] = 68; * EUC_TWFreq[58][4] = 67; EUC_TWFreq[86][65] = 66; EUC_TWFreq[45][56] = * 65; EUC_TWFreq[79][49] = 64; EUC_TWFreq[35][3] = 63; * EUC_TWFreq[48][83] = 62; EUC_TWFreq[71][21] = 61; EUC_TWFreq[77][93] * = 60; EUC_TWFreq[87][92] = 59; EUC_TWFreq[38][35] = 58; * EUC_TWFreq[66][17] = 57; EUC_TWFreq[37][66] = 56; EUC_TWFreq[51][42] * = 55; EUC_TWFreq[57][73] = 54; EUC_TWFreq[51][54] = 53; * EUC_TWFreq[75][64] = 52; EUC_TWFreq[35][5] = 51; EUC_TWFreq[49][40] = * 50; EUC_TWFreq[58][35] = 49; EUC_TWFreq[67][88] = 48; * EUC_TWFreq[60][51] = 47; EUC_TWFreq[36][92] = 46; EUC_TWFreq[44][41] * = 45; EUC_TWFreq[58][29] = 44; EUC_TWFreq[43][62] = 43; * EUC_TWFreq[56][23] = 42; EUC_TWFreq[67][44] = 41; EUC_TWFreq[52][91] * = 40; EUC_TWFreq[42][81] = 39; EUC_TWFreq[64][25] = 38; * EUC_TWFreq[35][36] = 37; EUC_TWFreq[47][73] = 36; EUC_TWFreq[36][1] = * 35; EUC_TWFreq[65][84] = 34; EUC_TWFreq[73][1] = 33; * EUC_TWFreq[79][66] = 32; EUC_TWFreq[69][14] = 31; EUC_TWFreq[65][28] * = 30; EUC_TWFreq[60][93] = 29; EUC_TWFreq[72][79] = 28; * EUC_TWFreq[48][0] = 27; EUC_TWFreq[73][43] = 26; EUC_TWFreq[66][47] = * 25; EUC_TWFreq[41][18] = 24; EUC_TWFreq[51][10] = 23; * EUC_TWFreq[59][7] = 22; EUC_TWFreq[53][27] = 21; EUC_TWFreq[86][67] = * 20; EUC_TWFreq[49][87] = 19; EUC_TWFreq[52][28] = 18; * EUC_TWFreq[52][12] = 17; EUC_TWFreq[42][30] = 16; EUC_TWFreq[65][35] * = 15; EUC_TWFreq[46][64] = 14; EUC_TWFreq[71][7] = 13; * EUC_TWFreq[56][57] = 12; EUC_TWFreq[56][31] = 11; EUC_TWFreq[41][31] * = 10; EUC_TWFreq[48][59] = 9; EUC_TWFreq[63][92] = 8; * EUC_TWFreq[62][57] = 7; EUC_TWFreq[65][87] = 6; EUC_TWFreq[70][10] = * 5; EUC_TWFreq[52][40] = 4; EUC_TWFreq[40][22] = 3; EUC_TWFreq[65][91] * = 2; EUC_TWFreq[50][25] = 1; EUC_TWFreq[35][84] = 0; */ GBKFreq[52][132] = 600; GBKFreq[73][135] = 599; GBKFreq[49][123] = 598; GBKFreq[77][146] = 597; GBKFreq[81][123] = 596; GBKFreq[82][144] = 595; GBKFreq[51][179] = 594; GBKFreq[83][154] = 593; GBKFreq[71][139] = 592; GBKFreq[64][139] = 591; GBKFreq[85][144] = 590; GBKFreq[52][125] = 589; GBKFreq[88][25] = 588; GBKFreq[81][106] = 587; GBKFreq[81][148] = 586; GBKFreq[62][137] = 585; GBKFreq[94][0] = 584; GBKFreq[1][64] = 583; GBKFreq[67][163] = 582; GBKFreq[20][190] = 581; GBKFreq[57][131] = 580; GBKFreq[29][169] = 579; GBKFreq[72][143] = 578; GBKFreq[0][173] = 577; GBKFreq[11][23] = 576; GBKFreq[61][141] = 575; GBKFreq[60][123] = 574; GBKFreq[81][114] = 573; GBKFreq[82][131] = 572; GBKFreq[67][156] = 571; GBKFreq[71][167] = 570; GBKFreq[20][50] = 569; GBKFreq[77][132] = 568; GBKFreq[84][38] = 567; GBKFreq[26][29] = 566; GBKFreq[74][187] = 565; GBKFreq[62][116] = 564; GBKFreq[67][135] = 563; GBKFreq[5][86] = 562; GBKFreq[72][186] = 561; GBKFreq[75][161] = 560; GBKFreq[78][130] = 559; GBKFreq[94][30] = 558; GBKFreq[84][72] = 557; GBKFreq[1][67] = 556; GBKFreq[75][172] = 555; GBKFreq[74][185] = 554; GBKFreq[53][160] = 553; GBKFreq[123][14] = 552; GBKFreq[79][97] = 551; GBKFreq[85][110] = 550; GBKFreq[78][171] = 549; GBKFreq[52][131] = 548; GBKFreq[56][100] = 547; GBKFreq[50][182] = 546; GBKFreq[94][64] = 545; GBKFreq[106][74] = 544; GBKFreq[11][102] = 543; GBKFreq[53][124] = 542; GBKFreq[24][3] = 541; GBKFreq[86][148] = 540; GBKFreq[53][184] = 539; GBKFreq[86][147] = 538; GBKFreq[96][161] = 537; GBKFreq[82][77] = 536; GBKFreq[59][146] = 535; GBKFreq[84][126] = 534; GBKFreq[79][132] = 533; GBKFreq[85][123] = 532; GBKFreq[71][101] = 531; GBKFreq[85][106] = 530; GBKFreq[6][184] = 529; GBKFreq[57][156] = 528; GBKFreq[75][104] = 527; GBKFreq[50][137] = 526; GBKFreq[79][133] = 525; GBKFreq[76][108] = 524; GBKFreq[57][142] = 523; GBKFreq[84][130] = 522; GBKFreq[52][128] = 521; GBKFreq[47][44] = 520; GBKFreq[52][152] = 519; GBKFreq[54][104] = 518; GBKFreq[30][47] = 517; GBKFreq[71][123] = 516; GBKFreq[52][107] = 515; GBKFreq[45][84] = 514; GBKFreq[107][118] = 513; GBKFreq[5][161] = 512; GBKFreq[48][126] = 511; GBKFreq[67][170] = 510; GBKFreq[43][6] = 509; GBKFreq[70][112] = 508; GBKFreq[86][174] = 507; GBKFreq[84][166] = 506; GBKFreq[79][130] = 505; GBKFreq[57][141] = 504; GBKFreq[81][178] = 503; GBKFreq[56][187] = 502; GBKFreq[81][162] = 501; GBKFreq[53][104] = 500; GBKFreq[123][35] = 499; GBKFreq[70][169] = 498; GBKFreq[69][164] = 497; GBKFreq[109][61] = 496; GBKFreq[73][130] = 495; GBKFreq[62][134] = 494; GBKFreq[54][125] = 493; GBKFreq[79][105] = 492; GBKFreq[70][165] = 491; GBKFreq[71][189] = 490; GBKFreq[23][147] = 489; GBKFreq[51][139] = 488; GBKFreq[47][137] = 487; GBKFreq[77][123] = 486; GBKFreq[86][183] = 485; GBKFreq[63][173] = 484; GBKFreq[79][144] = 483; GBKFreq[84][159] = 482; GBKFreq[60][91] = 481; GBKFreq[66][187] = 480; GBKFreq[73][114] = 479; GBKFreq[85][56] = 478; GBKFreq[71][149] = 477; GBKFreq[84][189] = 476; GBKFreq[104][31] = 475; GBKFreq[83][82] = 474; GBKFreq[68][35] = 473; GBKFreq[11][77] = 472; GBKFreq[15][155] = 471; GBKFreq[83][153] = 470; GBKFreq[71][1] = 469; GBKFreq[53][190] = 468; GBKFreq[50][135] = 467; GBKFreq[3][147] = 466; GBKFreq[48][136] = 465; GBKFreq[66][166] = 464; GBKFreq[55][159] = 463; GBKFreq[82][150] = 462; GBKFreq[58][178] = 461; GBKFreq[64][102] = 460; GBKFreq[16][106] = 459; GBKFreq[68][110] = 458; GBKFreq[54][14] = 457; GBKFreq[60][140] = 456; GBKFreq[91][71] = 455; GBKFreq[54][150] = 454; GBKFreq[78][177] = 453; GBKFreq[78][117] = 452; GBKFreq[104][12] = 451; GBKFreq[73][150] = 450; GBKFreq[51][142] = 449; GBKFreq[81][145] = 448; GBKFreq[66][183] = 447; GBKFreq[51][178] = 446; GBKFreq[75][107] = 445; GBKFreq[65][119] = 444; GBKFreq[69][176] = 443; GBKFreq[59][122] = 442; GBKFreq[78][160] = 441; GBKFreq[85][183] = 440; GBKFreq[105][16] = 439; GBKFreq[73][110] = 438; GBKFreq[104][39] = 437; GBKFreq[119][16] = 436; GBKFreq[76][162] = 435; GBKFreq[67][152] = 434; GBKFreq[82][24] = 433; GBKFreq[73][121] = 432; GBKFreq[83][83] = 431; GBKFreq[82][145] = 430; GBKFreq[49][133] = 429; GBKFreq[94][13] = 428; GBKFreq[58][139] = 427; GBKFreq[74][189] = 426; GBKFreq[66][177] = 425; GBKFreq[85][184] = 424; GBKFreq[55][183] = 423; GBKFreq[71][107] = 422; GBKFreq[11][98] = 421; GBKFreq[72][153] = 420; GBKFreq[2][137] = 419; GBKFreq[59][147] = 418; GBKFreq[58][152] = 417; GBKFreq[55][144] = 416; GBKFreq[73][125] = 415; GBKFreq[52][154] = 414; GBKFreq[70][178] = 413; GBKFreq[79][148] = 412; GBKFreq[63][143] = 411; GBKFreq[50][140] = 410; GBKFreq[47][145] = 409; GBKFreq[48][123] = 408; GBKFreq[56][107] = 407; GBKFreq[84][83] = 406; GBKFreq[59][112] = 405; GBKFreq[124][72] = 404; GBKFreq[79][99] = 403; GBKFreq[3][37] = 402; GBKFreq[114][55] = 401; GBKFreq[85][152] = 400; GBKFreq[60][47] = 399; GBKFreq[65][96] = 398; GBKFreq[74][110] = 397; GBKFreq[86][182] = 396; GBKFreq[50][99] = 395; GBKFreq[67][186] = 394; GBKFreq[81][74] = 393; GBKFreq[80][37] = 392; GBKFreq[21][60] = 391; GBKFreq[110][12] = 390; GBKFreq[60][162] = 389; GBKFreq[29][115] = 388; GBKFreq[83][130] = 387; GBKFreq[52][136] = 386; GBKFreq[63][114] = 385; GBKFreq[49][127] = 384; GBKFreq[83][109] = 383; GBKFreq[66][128] = 382; GBKFreq[78][136] = 381; GBKFreq[81][180] = 380; GBKFreq[76][104] = 379; GBKFreq[56][156] = 378; GBKFreq[61][23] = 377; GBKFreq[4][30] = 376; GBKFreq[69][154] = 375; GBKFreq[100][37] = 374; GBKFreq[54][177] = 373; GBKFreq[23][119] = 372; GBKFreq[71][171] = 371; GBKFreq[84][146] = 370; GBKFreq[20][184] = 369; GBKFreq[86][76] = 368; GBKFreq[74][132] = 367; GBKFreq[47][97] = 366; GBKFreq[82][137] = 365; GBKFreq[94][56] = 364; GBKFreq[92][30] = 363; GBKFreq[19][117] = 362; GBKFreq[48][173] = 361; GBKFreq[2][136] = 360; GBKFreq[7][182] = 359; GBKFreq[74][188] = 358; GBKFreq[14][132] = 357; GBKFreq[62][172] = 356; GBKFreq[25][39] = 355; GBKFreq[85][129] = 354; GBKFreq[64][98] = 353; GBKFreq[67][127] = 352; GBKFreq[72][167] = 351; GBKFreq[57][143] = 350; GBKFreq[76][187] = 349; GBKFreq[83][181] = 348; GBKFreq[84][10] = 347; GBKFreq[55][166] = 346; GBKFreq[55][188] = 345; GBKFreq[13][151] = 344; GBKFreq[62][124] = 343; GBKFreq[53][136] = 342; GBKFreq[106][57] = 341; GBKFreq[47][166] = 340; GBKFreq[109][30] = 339; GBKFreq[78][114] = 338; GBKFreq[83][19] = 337; GBKFreq[56][162] = 336; GBKFreq[60][177] = 335; GBKFreq[88][9] = 334; GBKFreq[74][163] = 333; GBKFreq[52][156] = 332; GBKFreq[71][180] = 331; GBKFreq[60][57] = 330; GBKFreq[72][173] = 329; GBKFreq[82][91] = 328; GBKFreq[51][186] = 327; GBKFreq[75][86] = 326; GBKFreq[75][78] = 325; GBKFreq[76][170] = 324; GBKFreq[60][147] = 323; GBKFreq[82][75] = 322; GBKFreq[80][148] = 321; GBKFreq[86][150] = 320; GBKFreq[13][95] = 319; GBKFreq[0][11] = 318; GBKFreq[84][190] = 317; GBKFreq[76][166] = 316; GBKFreq[14][72] = 315; GBKFreq[67][144] = 314; GBKFreq[84][44] = 313; GBKFreq[72][125] = 312; GBKFreq[66][127] = 311; GBKFreq[60][25] = 310; GBKFreq[70][146] = 309; GBKFreq[79][135] = 308; GBKFreq[54][135] = 307; GBKFreq[60][104] = 306; GBKFreq[55][132] = 305; GBKFreq[94][2] = 304; GBKFreq[54][133] = 303; GBKFreq[56][190] = 302; GBKFreq[58][174] = 301; GBKFreq[80][144] = 300; GBKFreq[85][113] = 299; /* * GBKFreq[83][15] = 298; GBKFreq[105][80] = 297; GBKFreq[7][179] = 296; * GBKFreq[93][4] = 295; GBKFreq[123][40] = 294; GBKFreq[85][120] = 293; * GBKFreq[77][165] = 292; GBKFreq[86][67] = 291; GBKFreq[25][162] = * 290; GBKFreq[77][183] = 289; GBKFreq[83][71] = 288; GBKFreq[78][99] = * 287; GBKFreq[72][177] = 286; GBKFreq[71][97] = 285; GBKFreq[58][111] * = 284; GBKFreq[77][175] = 283; GBKFreq[76][181] = 282; * GBKFreq[71][142] = 281; GBKFreq[64][150] = 280; GBKFreq[5][142] = * 279; GBKFreq[73][128] = 278; GBKFreq[73][156] = 277; GBKFreq[60][188] * = 276; GBKFreq[64][56] = 275; GBKFreq[74][128] = 274; * GBKFreq[48][163] = 273; GBKFreq[54][116] = 272; GBKFreq[73][127] = * 271; GBKFreq[16][176] = 270; GBKFreq[62][149] = 269; GBKFreq[105][96] * = 268; GBKFreq[55][186] = 267; GBKFreq[4][51] = 266; GBKFreq[48][113] * = 265; GBKFreq[48][152] = 264; GBKFreq[23][9] = 263; GBKFreq[56][102] * = 262; GBKFreq[11][81] = 261; GBKFreq[82][112] = 260; GBKFreq[65][85] * = 259; GBKFreq[69][125] = 258; GBKFreq[68][31] = 257; GBKFreq[5][20] * = 256; GBKFreq[60][176] = 255; GBKFreq[82][81] = 254; * GBKFreq[72][107] = 253; GBKFreq[3][52] = 252; GBKFreq[71][157] = 251; * GBKFreq[24][46] = 250; GBKFreq[69][108] = 249; GBKFreq[78][178] = * 248; GBKFreq[9][69] = 247; GBKFreq[73][144] = 246; GBKFreq[63][187] = * 245; GBKFreq[68][36] = 244; GBKFreq[47][151] = 243; GBKFreq[14][74] = * 242; GBKFreq[47][114] = 241; GBKFreq[80][171] = 240; GBKFreq[75][152] * = 239; GBKFreq[86][40] = 238; GBKFreq[93][43] = 237; GBKFreq[2][50] = * 236; GBKFreq[62][66] = 235; GBKFreq[1][183] = 234; GBKFreq[74][124] = * 233; GBKFreq[58][104] = 232; GBKFreq[83][106] = 231; GBKFreq[60][144] * = 230; GBKFreq[48][99] = 229; GBKFreq[54][157] = 228; * GBKFreq[70][179] = 227; GBKFreq[61][127] = 226; GBKFreq[57][135] = * 225; GBKFreq[59][190] = 224; GBKFreq[77][116] = 223; GBKFreq[26][17] * = 222; GBKFreq[60][13] = 221; GBKFreq[71][38] = 220; GBKFreq[85][177] * = 219; GBKFreq[59][73] = 218; GBKFreq[50][150] = 217; * GBKFreq[79][102] = 216; GBKFreq[76][118] = 215; GBKFreq[67][132] = * 214; GBKFreq[73][146] = 213; GBKFreq[83][184] = 212; GBKFreq[86][159] * = 211; GBKFreq[95][120] = 210; GBKFreq[23][139] = 209; * GBKFreq[64][183] = 208; GBKFreq[85][103] = 207; GBKFreq[41][90] = * 206; GBKFreq[87][72] = 205; GBKFreq[62][104] = 204; GBKFreq[79][168] * = 203; GBKFreq[79][150] = 202; GBKFreq[104][20] = 201; * GBKFreq[56][114] = 200; GBKFreq[84][26] = 199; GBKFreq[57][99] = 198; * GBKFreq[62][154] = 197; GBKFreq[47][98] = 196; GBKFreq[61][64] = 195; * GBKFreq[112][18] = 194; GBKFreq[123][19] = 193; GBKFreq[4][98] = 192; * GBKFreq[47][163] = 191; GBKFreq[66][188] = 190; GBKFreq[81][85] = * 189; GBKFreq[82][30] = 188; GBKFreq[65][83] = 187; GBKFreq[67][24] = * 186; GBKFreq[68][179] = 185; GBKFreq[55][177] = 184; GBKFreq[2][122] * = 183; GBKFreq[47][139] = 182; GBKFreq[79][158] = 181; * GBKFreq[64][143] = 180; GBKFreq[100][24] = 179; GBKFreq[73][103] = * 178; GBKFreq[50][148] = 177; GBKFreq[86][97] = 176; GBKFreq[59][116] * = 175; GBKFreq[64][173] = 174; GBKFreq[99][91] = 173; GBKFreq[11][99] * = 172; GBKFreq[78][179] = 171; GBKFreq[18][17] = 170; * GBKFreq[58][185] = 169; GBKFreq[47][165] = 168; GBKFreq[67][131] = * 167; GBKFreq[94][40] = 166; GBKFreq[74][153] = 165; GBKFreq[79][142] * = 164; GBKFreq[57][98] = 163; GBKFreq[1][164] = 162; GBKFreq[55][168] * = 161; GBKFreq[13][141] = 160; GBKFreq[51][31] = 159; * GBKFreq[57][178] = 158; GBKFreq[50][189] = 157; GBKFreq[60][167] = * 156; GBKFreq[80][34] = 155; GBKFreq[109][80] = 154; GBKFreq[85][54] = * 153; GBKFreq[69][183] = 152; GBKFreq[67][143] = 151; GBKFreq[47][120] * = 150; GBKFreq[45][75] = 149; GBKFreq[82][98] = 148; GBKFreq[83][22] * = 147; GBKFreq[13][103] = 146; GBKFreq[49][174] = 145; * GBKFreq[57][181] = 144; GBKFreq[64][127] = 143; GBKFreq[61][131] = * 142; GBKFreq[52][180] = 141; GBKFreq[74][134] = 140; GBKFreq[84][187] * = 139; GBKFreq[81][189] = 138; GBKFreq[47][160] = 137; * GBKFreq[66][148] = 136; GBKFreq[7][4] = 135; GBKFreq[85][134] = 134; * GBKFreq[88][13] = 133; GBKFreq[88][80] = 132; GBKFreq[69][166] = 131; * GBKFreq[86][18] = 130; GBKFreq[79][141] = 129; GBKFreq[50][108] = * 128; GBKFreq[94][69] = 127; GBKFreq[81][110] = 126; GBKFreq[69][119] * = 125; GBKFreq[72][161] = 124; GBKFreq[106][45] = 123; * GBKFreq[73][124] = 122; GBKFreq[94][28] = 121; GBKFreq[63][174] = * 120; GBKFreq[3][149] = 119; GBKFreq[24][160] = 118; GBKFreq[113][94] * = 117; GBKFreq[56][138] = 116; GBKFreq[64][185] = 115; * GBKFreq[86][56] = 114; GBKFreq[56][150] = 113; GBKFreq[110][55] = * 112; GBKFreq[28][13] = 111; GBKFreq[54][190] = 110; GBKFreq[8][180] = * 109; GBKFreq[73][149] = 108; GBKFreq[80][155] = 107; GBKFreq[83][172] * = 106; GBKFreq[67][174] = 105; GBKFreq[64][180] = 104; * GBKFreq[84][46] = 103; GBKFreq[91][74] = 102; GBKFreq[69][134] = 101; * GBKFreq[61][107] = 100; GBKFreq[47][171] = 99; GBKFreq[59][51] = 98; * GBKFreq[109][74] = 97; GBKFreq[64][174] = 96; GBKFreq[52][151] = 95; * GBKFreq[51][176] = 94; GBKFreq[80][157] = 93; GBKFreq[94][31] = 92; * GBKFreq[79][155] = 91; GBKFreq[72][174] = 90; GBKFreq[69][113] = 89; * GBKFreq[83][167] = 88; GBKFreq[83][122] = 87; GBKFreq[8][178] = 86; * GBKFreq[70][186] = 85; GBKFreq[59][153] = 84; GBKFreq[84][68] = 83; * GBKFreq[79][39] = 82; GBKFreq[47][180] = 81; GBKFreq[88][53] = 80; * GBKFreq[57][154] = 79; GBKFreq[47][153] = 78; GBKFreq[3][153] = 77; * GBKFreq[76][134] = 76; GBKFreq[51][166] = 75; GBKFreq[58][176] = 74; * GBKFreq[27][138] = 73; GBKFreq[73][126] = 72; GBKFreq[76][185] = 71; * GBKFreq[52][186] = 70; GBKFreq[81][151] = 69; GBKFreq[26][50] = 68; * GBKFreq[76][173] = 67; GBKFreq[106][56] = 66; GBKFreq[85][142] = 65; * GBKFreq[11][103] = 64; GBKFreq[69][159] = 63; GBKFreq[53][142] = 62; * GBKFreq[7][6] = 61; GBKFreq[84][59] = 60; GBKFreq[86][3] = 59; * GBKFreq[64][144] = 58; GBKFreq[1][187] = 57; GBKFreq[82][128] = 56; * GBKFreq[3][66] = 55; GBKFreq[68][133] = 54; GBKFreq[55][167] = 53; * GBKFreq[52][130] = 52; GBKFreq[61][133] = 51; GBKFreq[72][181] = 50; * GBKFreq[25][98] = 49; GBKFreq[84][149] = 48; GBKFreq[91][91] = 47; * GBKFreq[47][188] = 46; GBKFreq[68][130] = 45; GBKFreq[22][44] = 44; * GBKFreq[81][121] = 43; GBKFreq[72][140] = 42; GBKFreq[55][133] = 41; * GBKFreq[55][185] = 40; GBKFreq[56][105] = 39; GBKFreq[60][30] = 38; * GBKFreq[70][103] = 37; GBKFreq[62][141] = 36; GBKFreq[70][144] = 35; * GBKFreq[59][111] = 34; GBKFreq[54][17] = 33; GBKFreq[18][190] = 32; * GBKFreq[65][164] = 31; GBKFreq[83][125] = 30; GBKFreq[61][121] = 29; * GBKFreq[48][13] = 28; GBKFreq[51][189] = 27; GBKFreq[65][68] = 26; * GBKFreq[7][0] = 25; GBKFreq[76][188] = 24; GBKFreq[85][117] = 23; * GBKFreq[45][33] = 22; GBKFreq[78][187] = 21; GBKFreq[106][48] = 20; * GBKFreq[59][52] = 19; GBKFreq[86][185] = 18; GBKFreq[84][121] = 17; * GBKFreq[82][189] = 16; GBKFreq[68][156] = 15; GBKFreq[55][125] = 14; * GBKFreq[65][175] = 13; GBKFreq[7][140] = 12; GBKFreq[50][106] = 11; * GBKFreq[59][124] = 10; GBKFreq[67][115] = 9; GBKFreq[82][114] = 8; * GBKFreq[74][121] = 7; GBKFreq[106][69] = 6; GBKFreq[94][27] = 5; * GBKFreq[78][98] = 4; GBKFreq[85][186] = 3; GBKFreq[108][90] = 2; * GBKFreq[62][160] = 1; GBKFreq[60][169] = 0; */ KRFreq[31][43] = 600; KRFreq[19][56] = 599; KRFreq[38][46] = 598; KRFreq[3][3] = 597; KRFreq[29][77] = 596; KRFreq[19][33] = 595; KRFreq[30][0] = 594; KRFreq[29][89] = 593; KRFreq[31][26] = 592; KRFreq[31][38] = 591; KRFreq[32][85] = 590; KRFreq[15][0] = 589; KRFreq[16][54] = 588; KRFreq[15][76] = 587; KRFreq[31][25] = 586; KRFreq[23][13] = 585; KRFreq[28][34] = 584; KRFreq[18][9] = 583; KRFreq[29][37] = 582; KRFreq[22][45] = 581; KRFreq[19][46] = 580; KRFreq[16][65] = 579; KRFreq[23][5] = 578; KRFreq[26][70] = 577; KRFreq[31][53] = 576; KRFreq[27][12] = 575; KRFreq[30][67] = 574; KRFreq[31][57] = 573; KRFreq[20][20] = 572; KRFreq[30][31] = 571; KRFreq[20][72] = 570; KRFreq[15][51] = 569; KRFreq[3][8] = 568; KRFreq[32][53] = 567; KRFreq[27][85] = 566; KRFreq[25][23] = 565; KRFreq[15][44] = 564; KRFreq[32][3] = 563; KRFreq[31][68] = 562; KRFreq[30][24] = 561; KRFreq[29][49] = 560; KRFreq[27][49] = 559; KRFreq[23][23] = 558; KRFreq[31][91] = 557; KRFreq[31][46] = 556; KRFreq[19][74] = 555; KRFreq[27][27] = 554; KRFreq[3][17] = 553; KRFreq[20][38] = 552; KRFreq[21][82] = 551; KRFreq[28][25] = 550; KRFreq[32][5] = 549; KRFreq[31][23] = 548; KRFreq[25][45] = 547; KRFreq[32][87] = 546; KRFreq[18][26] = 545; KRFreq[24][10] = 544; KRFreq[26][82] = 543; KRFreq[15][89] = 542; KRFreq[28][36] = 541; KRFreq[28][31] = 540; KRFreq[16][23] = 539; KRFreq[16][77] = 538; KRFreq[19][84] = 537; KRFreq[23][72] = 536; KRFreq[38][48] = 535; KRFreq[23][2] = 534; KRFreq[30][20] = 533; KRFreq[38][47] = 532; KRFreq[39][12] = 531; KRFreq[23][21] = 530; KRFreq[18][17] = 529; KRFreq[30][87] = 528; KRFreq[29][62] = 527; KRFreq[29][87] = 526; KRFreq[34][53] = 525; KRFreq[32][29] = 524; KRFreq[35][0] = 523; KRFreq[24][43] = 522; KRFreq[36][44] = 521; KRFreq[20][30] = 520; KRFreq[39][86] = 519; KRFreq[22][14] = 518; KRFreq[29][39] = 517; KRFreq[28][38] = 516; KRFreq[23][79] = 515; KRFreq[24][56] = 514; KRFreq[29][63] = 513; KRFreq[31][45] = 512; KRFreq[23][26] = 511; KRFreq[15][87] = 510; KRFreq[30][74] = 509; KRFreq[24][69] = 508; KRFreq[20][4] = 507; KRFreq[27][50] = 506; KRFreq[30][75] = 505; KRFreq[24][13] = 504; KRFreq[30][8] = 503; KRFreq[31][6] = 502; KRFreq[25][80] = 501; KRFreq[36][8] = 500; KRFreq[15][18] = 499; KRFreq[39][23] = 498; KRFreq[16][24] = 497; KRFreq[31][89] = 496; KRFreq[15][71] = 495; KRFreq[15][57] = 494; KRFreq[30][11] = 493; KRFreq[15][36] = 492; KRFreq[16][60] = 491; KRFreq[24][45] = 490; KRFreq[37][35] = 489; KRFreq[24][87] = 488; KRFreq[20][45] = 487; KRFreq[31][90] = 486; KRFreq[32][21] = 485; KRFreq[19][70] = 484; KRFreq[24][15] = 483; KRFreq[26][92] = 482; KRFreq[37][13] = 481; KRFreq[39][2] = 480; KRFreq[23][70] = 479; KRFreq[27][25] = 478; KRFreq[15][69] = 477; KRFreq[19][61] = 476; KRFreq[31][58] = 475; KRFreq[24][57] = 474; KRFreq[36][74] = 473; KRFreq[21][6] = 472; KRFreq[30][44] = 471; KRFreq[15][91] = 470; KRFreq[27][16] = 469; KRFreq[29][42] = 468; KRFreq[33][86] = 467; KRFreq[29][41] = 466; KRFreq[20][68] = 465; KRFreq[25][47] = 464; KRFreq[22][0] = 463; KRFreq[18][14] = 462; KRFreq[31][28] = 461; KRFreq[15][2] = 460; KRFreq[23][76] = 459; KRFreq[38][32] = 458; KRFreq[29][82] = 457; KRFreq[21][86] = 456; KRFreq[24][62] = 455; KRFreq[31][64] = 454; KRFreq[38][26] = 453; KRFreq[32][86] = 452; KRFreq[22][32] = 451; KRFreq[19][59] = 450; KRFreq[34][18] = 449; KRFreq[18][54] = 448; KRFreq[38][63] = 447; KRFreq[36][23] = 446; KRFreq[35][35] = 445; KRFreq[32][62] = 444; KRFreq[28][35] = 443; KRFreq[27][13] = 442; KRFreq[31][59] = 441; KRFreq[29][29] = 440; KRFreq[15][64] = 439; KRFreq[26][84] = 438; KRFreq[21][90] = 437; KRFreq[20][24] = 436; KRFreq[16][18] = 435; KRFreq[22][23] = 434; KRFreq[31][14] = 433; KRFreq[15][1] = 432; KRFreq[18][63] = 431; KRFreq[19][10] = 430; KRFreq[25][49] = 429; KRFreq[36][57] = 428; KRFreq[20][22] = 427; KRFreq[15][15] = 426; KRFreq[31][51] = 425; KRFreq[24][60] = 424; KRFreq[31][70] = 423; KRFreq[15][7] = 422; KRFreq[28][40] = 421; KRFreq[18][41] = 420; KRFreq[15][38] = 419; KRFreq[32][0] = 418; KRFreq[19][51] = 417; KRFreq[34][62] = 416; KRFreq[16][27] = 415; KRFreq[20][70] = 414; KRFreq[22][33] = 413; KRFreq[26][73] = 412; KRFreq[20][79] = 411; KRFreq[23][6] = 410; KRFreq[24][85] = 409; KRFreq[38][51] = 408; KRFreq[29][88] = 407; KRFreq[38][55] = 406; KRFreq[32][32] = 405; KRFreq[27][18] = 404; KRFreq[23][87] = 403; KRFreq[35][6] = 402; KRFreq[34][27] = 401; KRFreq[39][35] = 400; KRFreq[30][88] = 399; KRFreq[32][92] = 398; KRFreq[32][49] = 397; KRFreq[24][61] = 396; KRFreq[18][74] = 395; KRFreq[23][77] = 394; KRFreq[23][50] = 393; KRFreq[23][32] = 392; KRFreq[23][36] = 391; KRFreq[38][38] = 390; KRFreq[29][86] = 389; KRFreq[36][15] = 388; KRFreq[31][50] = 387; KRFreq[15][86] = 386; KRFreq[39][13] = 385; KRFreq[34][26] = 384; KRFreq[19][34] = 383; KRFreq[16][3] = 382; KRFreq[26][93] = 381; KRFreq[19][67] = 380; KRFreq[24][72] = 379; KRFreq[29][17] = 378; KRFreq[23][24] = 377; KRFreq[25][19] = 376; KRFreq[18][65] = 375; KRFreq[30][78] = 374; KRFreq[27][52] = 373; KRFreq[22][18] = 372; KRFreq[16][38] = 371; KRFreq[21][26] = 370; KRFreq[34][20] = 369; KRFreq[15][42] = 368; KRFreq[16][71] = 367; KRFreq[17][17] = 366; KRFreq[24][71] = 365; KRFreq[18][84] = 364; KRFreq[15][40] = 363; KRFreq[31][62] = 362; KRFreq[15][8] = 361; KRFreq[16][69] = 360; KRFreq[29][79] = 359; KRFreq[38][91] = 358; KRFreq[31][92] = 357; KRFreq[20][77] = 356; KRFreq[3][16] = 355; KRFreq[27][87] = 354; KRFreq[16][25] = 353; KRFreq[36][33] = 352; KRFreq[37][76] = 351; KRFreq[30][12] = 350; KRFreq[26][75] = 349; KRFreq[25][14] = 348; KRFreq[32][26] = 347; KRFreq[23][22] = 346; KRFreq[20][90] = 345; KRFreq[19][8] = 344; KRFreq[38][41] = 343; KRFreq[34][2] = 342; KRFreq[39][4] = 341; KRFreq[27][89] = 340; KRFreq[28][41] = 339; KRFreq[28][44] = 338; KRFreq[24][92] = 337; KRFreq[34][65] = 336; KRFreq[39][14] = 335; KRFreq[21][38] = 334; KRFreq[19][31] = 333; KRFreq[37][39] = 332; KRFreq[33][41] = 331; KRFreq[38][4] = 330; KRFreq[23][80] = 329; KRFreq[25][24] = 328; KRFreq[37][17] = 327; KRFreq[22][16] = 326; KRFreq[22][46] = 325; KRFreq[33][91] = 324; KRFreq[24][89] = 323; KRFreq[30][52] = 322; KRFreq[29][38] = 321; KRFreq[38][85] = 320; KRFreq[15][12] = 319; KRFreq[27][58] = 318; KRFreq[29][52] = 317; KRFreq[37][38] = 316; KRFreq[34][41] = 315; KRFreq[31][65] = 314; KRFreq[29][53] = 313; KRFreq[22][47] = 312; KRFreq[22][19] = 311; KRFreq[26][0] = 310; KRFreq[37][86] = 309; KRFreq[35][4] = 308; KRFreq[36][54] = 307; KRFreq[20][76] = 306; KRFreq[30][9] = 305; KRFreq[30][33] = 304; KRFreq[23][17] = 303; KRFreq[23][33] = 302; KRFreq[38][52] = 301; KRFreq[15][19] = 300; KRFreq[28][45] = 299; KRFreq[29][78] = 298; KRFreq[23][15] = 297; KRFreq[33][5] = 296; KRFreq[17][40] = 295; KRFreq[30][83] = 294; KRFreq[18][1] = 293; KRFreq[30][81] = 292; KRFreq[19][40] = 291; KRFreq[24][47] = 290; KRFreq[17][56] = 289; KRFreq[39][80] = 288; KRFreq[30][46] = 287; KRFreq[16][61] = 286; KRFreq[26][78] = 285; KRFreq[26][57] = 284; KRFreq[20][46] = 283; KRFreq[25][15] = 282; KRFreq[25][91] = 281; KRFreq[21][83] = 280; KRFreq[30][77] = 279; KRFreq[35][30] = 278; KRFreq[30][34] = 277; KRFreq[20][69] = 276; KRFreq[35][10] = 275; KRFreq[29][70] = 274; KRFreq[22][50] = 273; KRFreq[18][0] = 272; KRFreq[22][64] = 271; KRFreq[38][65] = 270; KRFreq[22][70] = 269; KRFreq[24][58] = 268; KRFreq[19][66] = 267; KRFreq[30][59] = 266; KRFreq[37][14] = 265; KRFreq[16][56] = 264; KRFreq[29][85] = 263; KRFreq[31][15] = 262; KRFreq[36][84] = 261; KRFreq[39][15] = 260; KRFreq[39][90] = 259; KRFreq[18][12] = 258; KRFreq[21][93] = 257; KRFreq[24][66] = 256; KRFreq[27][90] = 255; KRFreq[25][90] = 254; KRFreq[22][24] = 253; KRFreq[36][67] = 252; KRFreq[33][90] = 251; KRFreq[15][60] = 250; KRFreq[23][85] = 249; KRFreq[34][1] = 248; KRFreq[39][37] = 247; KRFreq[21][18] = 246; KRFreq[34][4] = 245; KRFreq[28][33] = 244; KRFreq[15][13] = 243; KRFreq[32][22] = 242; KRFreq[30][76] = 241; KRFreq[20][21] = 240; KRFreq[38][66] = 239; KRFreq[32][55] = 238; KRFreq[32][89] = 237; KRFreq[25][26] = 236; KRFreq[16][80] = 235; KRFreq[15][43] = 234; KRFreq[38][54] = 233; KRFreq[39][68] = 232; KRFreq[22][88] = 231; KRFreq[21][84] = 230; KRFreq[21][17] = 229; KRFreq[20][28] = 228; KRFreq[32][1] = 227; KRFreq[33][87] = 226; KRFreq[38][71] = 225; KRFreq[37][47] = 224; KRFreq[18][77] = 223; KRFreq[37][58] = 222; KRFreq[34][74] = 221; KRFreq[32][54] = 220; KRFreq[27][33] = 219; KRFreq[32][93] = 218; KRFreq[23][51] = 217; KRFreq[20][57] = 216; KRFreq[22][37] = 215; KRFreq[39][10] = 214; KRFreq[39][17] = 213; KRFreq[33][4] = 212; KRFreq[32][84] = 211; KRFreq[34][3] = 210; KRFreq[28][27] = 209; KRFreq[15][79] = 208; KRFreq[34][21] = 207; KRFreq[34][69] = 206; KRFreq[21][62] = 205; KRFreq[36][24] = 204; KRFreq[16][89] = 203; KRFreq[18][48] = 202; KRFreq[38][15] = 201; KRFreq[36][58] = 200; KRFreq[21][56] = 199; KRFreq[34][48] = 198; KRFreq[21][15] = 197; KRFreq[39][3] = 196; KRFreq[16][44] = 195; KRFreq[18][79] = 194; KRFreq[25][13] = 193; KRFreq[29][47] = 192; KRFreq[38][88] = 191; KRFreq[20][71] = 190; KRFreq[16][58] = 189; KRFreq[35][57] = 188; KRFreq[29][30] = 187; KRFreq[29][23] = 186; KRFreq[34][93] = 185; KRFreq[30][85] = 184; KRFreq[15][80] = 183; KRFreq[32][78] = 182; KRFreq[37][82] = 181; KRFreq[22][40] = 180; KRFreq[21][69] = 179; KRFreq[26][85] = 178; KRFreq[31][31] = 177; KRFreq[28][64] = 176; KRFreq[38][13] = 175; KRFreq[25][2] = 174; KRFreq[22][34] = 173; KRFreq[28][28] = 172; KRFreq[24][91] = 171; KRFreq[33][74] = 170; KRFreq[29][40] = 169; KRFreq[15][77] = 168; KRFreq[32][80] = 167; KRFreq[30][41] = 166; KRFreq[23][30] = 165; KRFreq[24][63] = 164; KRFreq[30][53] = 163; KRFreq[39][70] = 162; KRFreq[23][61] = 161; KRFreq[37][27] = 160; KRFreq[16][55] = 159; KRFreq[22][74] = 158; KRFreq[26][50] = 157; KRFreq[16][10] = 156; KRFreq[34][63] = 155; KRFreq[35][14] = 154; KRFreq[17][7] = 153; KRFreq[15][59] = 152; KRFreq[27][23] = 151; KRFreq[18][70] = 150; KRFreq[32][56] = 149; KRFreq[37][87] = 148; KRFreq[17][61] = 147; KRFreq[18][83] = 146; KRFreq[23][86] = 145; KRFreq[17][31] = 144; KRFreq[23][83] = 143; KRFreq[35][2] = 142; KRFreq[18][64] = 141; KRFreq[27][43] = 140; KRFreq[32][42] = 139; KRFreq[25][76] = 138; KRFreq[19][85] = 137; KRFreq[37][81] = 136; KRFreq[38][83] = 135; KRFreq[35][7] = 134; KRFreq[16][51] = 133; KRFreq[27][22] = 132; KRFreq[16][76] = 131; KRFreq[22][4] = 130; KRFreq[38][84] = 129; KRFreq[17][83] = 128; KRFreq[24][46] = 127; KRFreq[33][15] = 126; KRFreq[20][48] = 125; KRFreq[17][30] = 124; KRFreq[30][93] = 123; KRFreq[28][11] = 122; KRFreq[28][30] = 121; KRFreq[15][62] = 120; KRFreq[17][87] = 119; KRFreq[32][81] = 118; KRFreq[23][37] = 117; KRFreq[30][22] = 116; KRFreq[32][66] = 115; KRFreq[33][78] = 114; KRFreq[21][4] = 113; KRFreq[31][17] = 112; KRFreq[39][61] = 111; KRFreq[18][76] = 110; KRFreq[15][85] = 109; KRFreq[31][47] = 108; KRFreq[19][57] = 107; KRFreq[23][55] = 106; KRFreq[27][29] = 105; KRFreq[29][46] = 104; KRFreq[33][0] = 103; KRFreq[16][83] = 102; KRFreq[39][78] = 101; KRFreq[32][77] = 100; KRFreq[36][25] = 99; KRFreq[34][19] = 98; KRFreq[38][49] = 97; KRFreq[19][25] = 96; KRFreq[23][53] = 95; KRFreq[28][43] = 94; KRFreq[31][44] = 93; KRFreq[36][34] = 92; KRFreq[16][34] = 91; KRFreq[35][1] = 90; KRFreq[19][87] = 89; KRFreq[18][53] = 88; KRFreq[29][54] = 87; KRFreq[22][41] = 86; KRFreq[38][18] = 85; KRFreq[22][2] = 84; KRFreq[20][3] = 83; KRFreq[39][69] = 82; KRFreq[30][29] = 81; KRFreq[28][19] = 80; KRFreq[29][90] = 79; KRFreq[17][86] = 78; KRFreq[15][9] = 77; KRFreq[39][73] = 76; KRFreq[15][37] = 75; KRFreq[35][40] = 74; KRFreq[33][77] = 73; KRFreq[27][86] = 72; KRFreq[36][79] = 71; KRFreq[23][18] = 70; KRFreq[34][87] = 69; KRFreq[39][24] = 68; KRFreq[26][8] = 67; KRFreq[33][48] = 66; KRFreq[39][30] = 65; KRFreq[33][28] = 64; KRFreq[16][67] = 63; KRFreq[31][78] = 62; KRFreq[32][23] = 61; KRFreq[24][55] = 60; KRFreq[30][68] = 59; KRFreq[18][60] = 58; KRFreq[15][17] = 57; KRFreq[23][34] = 56; KRFreq[20][49] = 55; KRFreq[15][78] = 54; KRFreq[24][14] = 53; KRFreq[19][41] = 52; KRFreq[31][55] = 51; KRFreq[21][39] = 50; KRFreq[35][9] = 49; KRFreq[30][15] = 48; KRFreq[20][52] = 47; KRFreq[35][71] = 46; KRFreq[20][7] = 45; KRFreq[29][72] = 44; KRFreq[37][77] = 43; KRFreq[22][35] = 42; KRFreq[20][61] = 41; KRFreq[31][60] = 40; KRFreq[20][93] = 39; KRFreq[27][92] = 38; KRFreq[28][16] = 37; KRFreq[36][26] = 36; KRFreq[18][89] = 35; KRFreq[21][63] = 34; KRFreq[22][52] = 33; KRFreq[24][65] = 32; KRFreq[31][8] = 31; KRFreq[31][49] = 30; KRFreq[33][30] = 29; KRFreq[37][15] = 28; KRFreq[18][18] = 27; KRFreq[25][50] = 26; KRFreq[29][20] = 25; KRFreq[35][48] = 24; KRFreq[38][75] = 23; KRFreq[26][83] = 22; KRFreq[21][87] = 21; KRFreq[27][71] = 20; KRFreq[32][91] = 19; KRFreq[25][73] = 18; KRFreq[16][84] = 17; KRFreq[25][31] = 16; KRFreq[17][90] = 15; KRFreq[18][40] = 14; KRFreq[17][77] = 13; KRFreq[17][35] = 12; KRFreq[23][52] = 11; KRFreq[23][35] = 10; KRFreq[16][5] = 9; KRFreq[23][58] = 8; KRFreq[19][60] = 7; KRFreq[30][32] = 6; KRFreq[38][34] = 5; KRFreq[23][4] = 4; KRFreq[23][1] = 3; KRFreq[27][57] = 2; KRFreq[39][38] = 1; KRFreq[32][33] = 0; JPFreq[3][74] = 600; JPFreq[3][45] = 599; JPFreq[3][3] = 598; JPFreq[3][24] = 597; JPFreq[3][30] = 596; JPFreq[3][42] = 595; JPFreq[3][46] = 594; JPFreq[3][39] = 593; JPFreq[3][11] = 592; JPFreq[3][37] = 591; JPFreq[3][38] = 590; JPFreq[3][31] = 589; JPFreq[3][41] = 588; JPFreq[3][5] = 587; JPFreq[3][10] = 586; JPFreq[3][75] = 585; JPFreq[3][65] = 584; JPFreq[3][72] = 583; JPFreq[37][91] = 582; JPFreq[0][27] = 581; JPFreq[3][18] = 580; JPFreq[3][22] = 579; JPFreq[3][61] = 578; JPFreq[3][14] = 577; JPFreq[24][80] = 576; JPFreq[4][82] = 575; JPFreq[17][80] = 574; JPFreq[30][44] = 573; JPFreq[3][73] = 572; JPFreq[3][64] = 571; JPFreq[38][14] = 570; JPFreq[33][70] = 569; JPFreq[3][1] = 568; JPFreq[3][16] = 567; JPFreq[3][35] = 566; JPFreq[3][40] = 565; JPFreq[4][74] = 564; JPFreq[4][24] = 563; JPFreq[42][59] = 562; JPFreq[3][7] = 561; JPFreq[3][71] = 560; JPFreq[3][12] = 559; JPFreq[15][75] = 558; JPFreq[3][20] = 557; JPFreq[4][39] = 556; JPFreq[34][69] = 555; JPFreq[3][28] = 554; JPFreq[35][24] = 553; JPFreq[3][82] = 552; JPFreq[28][47] = 551; JPFreq[3][67] = 550; JPFreq[37][16] = 549; JPFreq[26][93] = 548; JPFreq[4][1] = 547; JPFreq[26][85] = 546; JPFreq[31][14] = 545; JPFreq[4][3] = 544; JPFreq[4][72] = 543; JPFreq[24][51] = 542; JPFreq[27][51] = 541; JPFreq[27][49] = 540; JPFreq[22][77] = 539; JPFreq[27][10] = 538; JPFreq[29][68] = 537; JPFreq[20][35] = 536; JPFreq[41][11] = 535; JPFreq[24][70] = 534; JPFreq[36][61] = 533; JPFreq[31][23] = 532; JPFreq[43][16] = 531; JPFreq[23][68] = 530; JPFreq[32][15] = 529; JPFreq[3][32] = 528; JPFreq[19][53] = 527; JPFreq[40][83] = 526; JPFreq[4][14] = 525; JPFreq[36][9] = 524; JPFreq[4][73] = 523; JPFreq[23][10] = 522; JPFreq[3][63] = 521; JPFreq[39][14] = 520; JPFreq[3][78] = 519; JPFreq[33][47] = 518; JPFreq[21][39] = 517; JPFreq[34][46] = 516; JPFreq[36][75] = 515; JPFreq[41][92] = 514; JPFreq[37][93] = 513; JPFreq[4][34] = 512; JPFreq[15][86] = 511; JPFreq[46][1] = 510; JPFreq[37][65] = 509; JPFreq[3][62] = 508; JPFreq[32][73] = 507; JPFreq[21][65] = 506; JPFreq[29][75] = 505; JPFreq[26][51] = 504; JPFreq[3][34] = 503; JPFreq[4][10] = 502; JPFreq[30][22] = 501; JPFreq[35][73] = 500; JPFreq[17][82] = 499; JPFreq[45][8] = 498; JPFreq[27][73] = 497; JPFreq[18][55] = 496; JPFreq[25][2] = 495; JPFreq[3][26] = 494; JPFreq[45][46] = 493; JPFreq[4][22] = 492; JPFreq[4][40] = 491; JPFreq[18][10] = 490; JPFreq[32][9] = 489; JPFreq[26][49] = 488; JPFreq[3][47] = 487; JPFreq[24][65] = 486; JPFreq[4][76] = 485; JPFreq[43][67] = 484; JPFreq[3][9] = 483; JPFreq[41][37] = 482; JPFreq[33][68] = 481; JPFreq[43][31] = 480; JPFreq[19][55] = 479; JPFreq[4][30] = 478; JPFreq[27][33] = 477; JPFreq[16][62] = 476; JPFreq[36][35] = 475; JPFreq[37][15] = 474; JPFreq[27][70] = 473; JPFreq[22][71] = 472; JPFreq[33][45] = 471; JPFreq[31][78] = 470; JPFreq[43][59] = 469; JPFreq[32][19] = 468; JPFreq[17][28] = 467; JPFreq[40][28] = 466; JPFreq[20][93] = 465; JPFreq[18][15] = 464; JPFreq[4][23] = 463; JPFreq[3][23] = 462; JPFreq[26][64] = 461; JPFreq[44][92] = 460; JPFreq[17][27] = 459; JPFreq[3][56] = 458; JPFreq[25][38] = 457; JPFreq[23][31] = 456; JPFreq[35][43] = 455; JPFreq[4][54] = 454; JPFreq[35][19] = 453; JPFreq[22][47] = 452; JPFreq[42][0] = 451; JPFreq[23][28] = 450; JPFreq[46][33] = 449; JPFreq[36][85] = 448; JPFreq[31][12] = 447; JPFreq[3][76] = 446; JPFreq[4][75] = 445; JPFreq[36][56] = 444; JPFreq[4][64] = 443; JPFreq[25][77] = 442; JPFreq[15][52] = 441; JPFreq[33][73] = 440; JPFreq[3][55] = 439; JPFreq[43][82] = 438; JPFreq[27][82] = 437; JPFreq[20][3] = 436; JPFreq[40][51] = 435; JPFreq[3][17] = 434; JPFreq[27][71] = 433; JPFreq[4][52] = 432; JPFreq[44][48] = 431; JPFreq[27][2] = 430; JPFreq[17][39] = 429; JPFreq[31][8] = 428; JPFreq[44][54] = 427; JPFreq[43][18] = 426; JPFreq[43][77] = 425; JPFreq[4][61] = 424; JPFreq[19][91] = 423; JPFreq[31][13] = 422; JPFreq[44][71] = 421; JPFreq[20][0] = 420; JPFreq[23][87] = 419; JPFreq[21][14] = 418; JPFreq[29][13] = 417; JPFreq[3][58] = 416; JPFreq[26][18] = 415; JPFreq[4][47] = 414; JPFreq[4][18] = 413; JPFreq[3][53] = 412; JPFreq[26][92] = 411; JPFreq[21][7] = 410; JPFreq[4][37] = 409; JPFreq[4][63] = 408; JPFreq[36][51] = 407; JPFreq[4][32] = 406; JPFreq[28][73] = 405; JPFreq[4][50] = 404; JPFreq[41][60] = 403; JPFreq[23][1] = 402; JPFreq[36][92] = 401; JPFreq[15][41] = 400; JPFreq[21][71] = 399; JPFreq[41][30] = 398; JPFreq[32][76] = 397; JPFreq[17][34] = 396; JPFreq[26][15] = 395; JPFreq[26][25] = 394; JPFreq[31][77] = 393; JPFreq[31][3] = 392; JPFreq[46][34] = 391; JPFreq[27][84] = 390; JPFreq[23][8] = 389; JPFreq[16][0] = 388; JPFreq[28][80] = 387; JPFreq[26][54] = 386; JPFreq[33][18] = 385; JPFreq[31][20] = 384; JPFreq[31][62] = 383; JPFreq[30][41] = 382; JPFreq[33][30] = 381; JPFreq[45][45] = 380; JPFreq[37][82] = 379; JPFreq[15][33] = 378; JPFreq[20][12] = 377; JPFreq[18][5] = 376; JPFreq[28][86] = 375; JPFreq[30][19] = 374; JPFreq[42][43] = 373; JPFreq[36][31] = 372; JPFreq[17][93] = 371; JPFreq[4][15] = 370; JPFreq[21][20] = 369; JPFreq[23][21] = 368; JPFreq[28][72] = 367; JPFreq[4][20] = 366; JPFreq[26][55] = 365; JPFreq[21][5] = 364; JPFreq[19][16] = 363; JPFreq[23][64] = 362; JPFreq[40][59] = 361; JPFreq[37][26] = 360; JPFreq[26][56] = 359; JPFreq[4][12] = 358; JPFreq[33][71] = 357; JPFreq[32][39] = 356; JPFreq[38][40] = 355; JPFreq[22][74] = 354; JPFreq[3][25] = 353; JPFreq[15][48] = 352; JPFreq[41][82] = 351; JPFreq[41][9] = 350; JPFreq[25][48] = 349; JPFreq[31][71] = 348; JPFreq[43][29] = 347; JPFreq[26][80] = 346; JPFreq[4][5] = 345; JPFreq[18][71] = 344; JPFreq[29][0] = 343; JPFreq[43][43] = 342; JPFreq[23][81] = 341; JPFreq[4][42] = 340; JPFreq[44][28] = 339; JPFreq[23][93] = 338; JPFreq[17][81] = 337; JPFreq[25][25] = 336; JPFreq[41][23] = 335; JPFreq[34][35] = 334; JPFreq[4][53] = 333; JPFreq[28][36] = 332; JPFreq[4][41] = 331; JPFreq[25][60] = 330; JPFreq[23][20] = 329; JPFreq[3][43] = 328; JPFreq[24][79] = 327; JPFreq[29][41] = 326; JPFreq[30][83] = 325; JPFreq[3][50] = 324; JPFreq[22][18] = 323; JPFreq[18][3] = 322; JPFreq[39][30] = 321; JPFreq[4][28] = 320; JPFreq[21][64] = 319; JPFreq[4][68] = 318; JPFreq[17][71] = 317; JPFreq[27][0] = 316; JPFreq[39][28] = 315; JPFreq[30][13] = 314; JPFreq[36][70] = 313; JPFreq[20][82] = 312; JPFreq[33][38] = 311; JPFreq[44][87] = 310; JPFreq[34][45] = 309; JPFreq[4][26] = 308; JPFreq[24][44] = 307; JPFreq[38][67] = 306; JPFreq[38][6] = 305; JPFreq[30][68] = 304; JPFreq[15][89] = 303; JPFreq[24][93] = 302; JPFreq[40][41] = 301; JPFreq[38][3] = 300; JPFreq[28][23] = 299; JPFreq[26][17] = 298; JPFreq[4][38] = 297; JPFreq[22][78] = 296; JPFreq[15][37] = 295; JPFreq[25][85] = 294; JPFreq[4][9] = 293; JPFreq[4][7] = 292; JPFreq[27][53] = 291; JPFreq[39][29] = 290; JPFreq[41][43] = 289; JPFreq[25][62] = 288; JPFreq[4][48] = 287; JPFreq[28][28] = 286; JPFreq[21][40] = 285; JPFreq[36][73] = 284; JPFreq[26][39] = 283; JPFreq[22][54] = 282; JPFreq[33][5] = 281; JPFreq[19][21] = 280; JPFreq[46][31] = 279; JPFreq[20][64] = 278; JPFreq[26][63] = 277; JPFreq[22][23] = 276; JPFreq[25][81] = 275; JPFreq[4][62] = 274; JPFreq[37][31] = 273; JPFreq[40][52] = 272; JPFreq[29][79] = 271; JPFreq[41][48] = 270; JPFreq[31][57] = 269; JPFreq[32][92] = 268; JPFreq[36][36] = 267; JPFreq[27][7] = 266; JPFreq[35][29] = 265; JPFreq[37][34] = 264; JPFreq[34][42] = 263; JPFreq[27][15] = 262; JPFreq[33][27] = 261; JPFreq[31][38] = 260; JPFreq[19][79] = 259; JPFreq[4][31] = 258; JPFreq[4][66] = 257; JPFreq[17][32] = 256; JPFreq[26][67] = 255; JPFreq[16][30] = 254; JPFreq[26][46] = 253; JPFreq[24][26] = 252; JPFreq[35][10] = 251; JPFreq[18][37] = 250; JPFreq[3][19] = 249; JPFreq[33][69] = 248; JPFreq[31][9] = 247; JPFreq[45][29] = 246; JPFreq[3][15] = 245; JPFreq[18][54] = 244; JPFreq[3][44] = 243; JPFreq[31][29] = 242; JPFreq[18][45] = 241; JPFreq[38][28] = 240; JPFreq[24][12] = 239; JPFreq[35][82] = 238; JPFreq[17][43] = 237; JPFreq[28][9] = 236; JPFreq[23][25] = 235; JPFreq[44][37] = 234; JPFreq[23][75] = 233; JPFreq[23][92] = 232; JPFreq[0][24] = 231; JPFreq[19][74] = 230; JPFreq[45][32] = 229; JPFreq[16][72] = 228; JPFreq[16][93] = 227; JPFreq[45][13] = 226; JPFreq[24][8] = 225; JPFreq[25][47] = 224; JPFreq[28][26] = 223; JPFreq[43][81] = 222; JPFreq[32][71] = 221; JPFreq[18][41] = 220; JPFreq[26][62] = 219; JPFreq[41][24] = 218; JPFreq[40][11] = 217; JPFreq[43][57] = 216; JPFreq[34][53] = 215; JPFreq[20][32] = 214; JPFreq[34][43] = 213; JPFreq[41][91] = 212; JPFreq[29][57] = 211; JPFreq[15][43] = 210; JPFreq[22][89] = 209; JPFreq[33][83] = 208; JPFreq[43][20] = 207; JPFreq[25][58] = 206; JPFreq[30][30] = 205; JPFreq[4][56] = 204; JPFreq[17][64] = 203; JPFreq[23][0] = 202; JPFreq[44][12] = 201; JPFreq[25][37] = 200; JPFreq[35][13] = 199; JPFreq[20][30] = 198; JPFreq[21][84] = 197; JPFreq[29][14] = 196; JPFreq[30][5] = 195; JPFreq[37][2] = 194; JPFreq[4][78] = 193; JPFreq[29][78] = 192; JPFreq[29][84] = 191; JPFreq[32][86] = 190; JPFreq[20][68] = 189; JPFreq[30][39] = 188; JPFreq[15][69] = 187; JPFreq[4][60] = 186; JPFreq[20][61] = 185; JPFreq[41][67] = 184; JPFreq[16][35] = 183; JPFreq[36][57] = 182; JPFreq[39][80] = 181; JPFreq[4][59] = 180; JPFreq[4][44] = 179; JPFreq[40][54] = 178; JPFreq[30][8] = 177; JPFreq[44][30] = 176; JPFreq[31][93] = 175; JPFreq[31][47] = 174; JPFreq[16][70] = 173; JPFreq[21][0] = 172; JPFreq[17][35] = 171; JPFreq[21][67] = 170; JPFreq[44][18] = 169; JPFreq[36][29] = 168; JPFreq[18][67] = 167; JPFreq[24][28] = 166; JPFreq[36][24] = 165; JPFreq[23][5] = 164; JPFreq[31][65] = 163; JPFreq[26][59] = 162; JPFreq[28][2] = 161; JPFreq[39][69] = 160; JPFreq[42][40] = 159; JPFreq[37][80] = 158; JPFreq[15][66] = 157; JPFreq[34][38] = 156; JPFreq[28][48] = 155; JPFreq[37][77] = 154; JPFreq[29][34] = 153; JPFreq[33][12] = 152; JPFreq[4][65] = 151; JPFreq[30][31] = 150; JPFreq[27][92] = 149; JPFreq[4][2] = 148; JPFreq[4][51] = 147; JPFreq[23][77] = 146; JPFreq[4][35] = 145; JPFreq[3][13] = 144; JPFreq[26][26] = 143; JPFreq[44][4] = 142; JPFreq[39][53] = 141; JPFreq[20][11] = 140; JPFreq[40][33] = 139; JPFreq[45][7] = 138; JPFreq[4][70] = 137; JPFreq[3][49] = 136; JPFreq[20][59] = 135; JPFreq[21][12] = 134; JPFreq[33][53] = 133; JPFreq[20][14] = 132; JPFreq[37][18] = 131; JPFreq[18][17] = 130; JPFreq[36][23] = 129; JPFreq[18][57] = 128; JPFreq[26][74] = 127; JPFreq[35][2] = 126; JPFreq[38][58] = 125; JPFreq[34][68] = 124; JPFreq[29][81] = 123; JPFreq[20][69] = 122; JPFreq[39][86] = 121; JPFreq[4][16] = 120; JPFreq[16][49] = 119; JPFreq[15][72] = 118; JPFreq[26][35] = 117; JPFreq[32][14] = 116; JPFreq[40][90] = 115; JPFreq[33][79] = 114; JPFreq[35][4] = 113; JPFreq[23][33] = 112; JPFreq[19][19] = 111; JPFreq[31][41] = 110; JPFreq[44][1] = 109; JPFreq[22][56] = 108; JPFreq[31][27] = 107; JPFreq[32][18] = 106; JPFreq[27][32] = 105; JPFreq[37][39] = 104; JPFreq[42][11] = 103; JPFreq[29][71] = 102; JPFreq[32][58] = 101; JPFreq[46][10] = 100; JPFreq[17][30] = 99; JPFreq[38][15] = 98; JPFreq[29][60] = 97; JPFreq[4][11] = 96; JPFreq[38][31] = 95; JPFreq[40][79] = 94; JPFreq[28][49] = 93; JPFreq[28][84] = 92; JPFreq[26][77] = 91; JPFreq[22][32] = 90; JPFreq[33][17] = 89; JPFreq[23][18] = 88; JPFreq[32][64] = 87; JPFreq[4][6] = 86; JPFreq[33][51] = 85; JPFreq[44][77] = 84; JPFreq[29][5] = 83; JPFreq[46][25] = 82; JPFreq[19][58] = 81; JPFreq[4][46] = 80; JPFreq[15][71] = 79; JPFreq[18][58] = 78; JPFreq[26][45] = 77; JPFreq[45][66] = 76; JPFreq[34][10] = 75; JPFreq[19][37] = 74; JPFreq[33][65] = 73; JPFreq[44][52] = 72; JPFreq[16][38] = 71; JPFreq[36][46] = 70; JPFreq[20][26] = 69; JPFreq[30][37] = 68; JPFreq[4][58] = 67; JPFreq[43][2] = 66; JPFreq[30][18] = 65; JPFreq[19][35] = 64; JPFreq[15][68] = 63; JPFreq[3][36] = 62; JPFreq[35][40] = 61; JPFreq[36][32] = 60; JPFreq[37][14] = 59; JPFreq[17][11] = 58; JPFreq[19][78] = 57; JPFreq[37][11] = 56; JPFreq[28][63] = 55; JPFreq[29][61] = 54; JPFreq[33][3] = 53; JPFreq[41][52] = 52; JPFreq[33][63] = 51; JPFreq[22][41] = 50; JPFreq[4][19] = 49; JPFreq[32][41] = 48; JPFreq[24][4] = 47; JPFreq[31][28] = 46; JPFreq[43][30] = 45; JPFreq[17][3] = 44; JPFreq[43][70] = 43; JPFreq[34][19] = 42; JPFreq[20][77] = 41; JPFreq[18][83] = 40; JPFreq[17][15] = 39; JPFreq[23][61] = 38; JPFreq[40][27] = 37; JPFreq[16][48] = 36; JPFreq[39][78] = 35; JPFreq[41][53] = 34; JPFreq[40][91] = 33; JPFreq[40][72] = 32; JPFreq[18][52] = 31; JPFreq[35][66] = 30; JPFreq[39][93] = 29; JPFreq[19][48] = 28; JPFreq[26][36] = 27; JPFreq[27][25] = 26; JPFreq[42][71] = 25; JPFreq[42][85] = 24; JPFreq[26][48] = 23; JPFreq[28][15] = 22; JPFreq[3][66] = 21; JPFreq[25][24] = 20; JPFreq[27][43] = 19; JPFreq[27][78] = 18; JPFreq[45][43] = 17; JPFreq[27][72] = 16; JPFreq[40][29] = 15; JPFreq[41][0] = 14; JPFreq[19][57] = 13; JPFreq[15][59] = 12; JPFreq[29][29] = 11; JPFreq[4][25] = 10; JPFreq[21][42] = 9; JPFreq[23][35] = 8; JPFreq[33][1] = 7; JPFreq[4][57] = 6; JPFreq[17][60] = 5; JPFreq[25][19] = 4; JPFreq[22][65] = 3; JPFreq[42][29] = 2; JPFreq[27][66] = 1; JPFreq[26][89] = 0; } } class Encoding { // Supported Encoding Types public static int GB2312 = 0; public static int GBK = 1; public static int GB18030 = 2; public static int HZ = 3; public static int BIG5 = 4; public static int CNS11643 = 5; public static int UTF8 = 6; public static int UTF8T = 7; public static int UTF8S = 8; public static int UNICODE = 9; public static int UNICODET = 10; public static int UNICODES = 11; public static int ISO2022CN = 12; public static int ISO2022CN_CNS = 13; public static int ISO2022CN_GB = 14; public static int EUC_KR = 15; public static int CP949 = 16; public static int ISO2022KR = 17; public static int JOHAB = 18; public static int SJIS = 19; public static int EUC_JP = 20; public static int ISO2022JP = 21; public static int ASCII = 22; public static int OTHER = 23; public static int TOTALTYPES = 24; public final static int SIMP = 0; public final static int TRAD = 1; // Names of the encodings as understood by Java public static String[] javaname; // Names of the encodings for human viewing public static String[] nicename; // Names of charsets as used in charset parameter of HTML Meta tag public static String[] htmlname; // Constructor public Encoding() { javaname = new String[TOTALTYPES]; nicename = new String[TOTALTYPES]; htmlname = new String[TOTALTYPES]; // Assign encoding names javaname[GB2312] = "GB2312"; javaname[GBK] = "GBK"; javaname[GB18030] = "GB18030"; javaname[HZ] = "ASCII"; // What to put here? Sun doesn't support HZ javaname[ISO2022CN_GB] = "ISO2022CN_GB"; javaname[BIG5] = "BIG5"; javaname[CNS11643] = "EUC-TW"; javaname[ISO2022CN_CNS] = "ISO2022CN_CNS"; javaname[ISO2022CN] = "ISO2022CN"; javaname[UTF8] = "UTF-8"; javaname[UTF8T] = "UTF-8"; javaname[UTF8S] = "UTF-8"; javaname[UNICODE] = "Unicode"; javaname[UNICODET] = "Unicode"; javaname[UNICODES] = "Unicode"; javaname[EUC_KR] = "EUC_KR"; javaname[CP949] = "MS949"; javaname[ISO2022KR] = "ISO2022KR"; javaname[JOHAB] = "Johab"; javaname[SJIS] = "SJIS"; javaname[EUC_JP] = "EUC_JP"; javaname[ISO2022JP] = "ISO2022JP"; javaname[ASCII] = "ASCII"; javaname[OTHER] = "ISO8859_1"; // Assign encoding names htmlname[GB2312] = "GB2312"; htmlname[GBK] = "GBK"; htmlname[GB18030] = "GB18030"; htmlname[HZ] = "HZ-GB-2312"; htmlname[ISO2022CN_GB] = "ISO-2022-CN-EXT"; htmlname[BIG5] = "BIG5"; htmlname[CNS11643] = "EUC-TW"; htmlname[ISO2022CN_CNS] = "ISO-2022-CN-EXT"; htmlname[ISO2022CN] = "ISO-2022-CN"; htmlname[UTF8] = "UTF-8"; htmlname[UTF8T] = "UTF-8"; htmlname[UTF8S] = "UTF-8"; htmlname[UNICODE] = "UTF-16"; htmlname[UNICODET] = "UTF-16"; htmlname[UNICODES] = "UTF-16"; htmlname[EUC_KR] = "EUC-KR"; htmlname[CP949] = "x-windows-949"; htmlname[ISO2022KR] = "ISO-2022-KR"; htmlname[JOHAB] = "x-Johab"; htmlname[SJIS] = "Shift_JIS"; htmlname[EUC_JP] = "EUC-JP"; htmlname[ISO2022JP] = "ISO-2022-JP"; htmlname[ASCII] = "ASCII"; htmlname[OTHER] = "ISO8859-1"; // Assign Human readable names nicename[GB2312] = "GB-2312"; nicename[GBK] = "GBK"; nicename[GB18030] = "GB18030"; nicename[HZ] = "HZ"; nicename[ISO2022CN_GB] = "ISO2022CN-GB"; nicename[BIG5] = "Big5"; nicename[CNS11643] = "CNS11643"; nicename[ISO2022CN_CNS] = "ISO2022CN-CNS"; nicename[ISO2022CN] = "ISO2022 CN"; nicename[UTF8] = "UTF-8"; nicename[UTF8T] = "UTF-8 (Trad)"; nicename[UTF8S] = "UTF-8 (Simp)"; nicename[UNICODE] = "Unicode"; nicename[UNICODET] = "Unicode (Trad)"; nicename[UNICODES] = "Unicode (Simp)"; nicename[EUC_KR] = "EUC-KR"; nicename[CP949] = "CP949"; nicename[ISO2022KR] = "ISO 2022 KR"; nicename[JOHAB] = "Johab"; nicename[SJIS] = "Shift-JIS"; nicename[EUC_JP] = "EUC-JP"; nicename[ISO2022JP] = "ISO 2022 JP"; nicename[ASCII] = "ASCII"; nicename[OTHER] = "OTHER"; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/FastXmlSerializer.java ================================================ package com.kunfei.bookshelf.utils; /* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; /** * This is a quick and dirty implementation of XmlSerializer that isn't horribly * painfully slow like the normal one. It only does what is needed for the * specific XML files being written with it. * {@hide} */ public class FastXmlSerializer implements XmlSerializer { private static final String ESCAPE_TABLE[] = new String[]{ null, null, null, null, null, null, null, null, // 0-7 null, null, null, null, null, null, null, null, // 8-15 null, null, null, null, null, null, null, null, // 16-23 null, null, null, null, null, null, null, null, // 24-31 null, null, """, null, null, null, "&", null, // 32-39 null, null, null, null, null, null, null, null, // 40-47 null, null, null, null, null, null, null, null, // 48-55 null, null, null, null, "<", null, ">", null, // 56-63 }; private static final int BUFFER_LEN = 8192; private static String sSpace = " "; private final char[] mText = new char[BUFFER_LEN]; private int mPos; private Writer mWriter; private OutputStream mOutputStream; private CharsetEncoder mCharset; private ByteBuffer mBytes = ByteBuffer.allocate(BUFFER_LEN); private boolean mIndent = false; private boolean mInTag; private int mNesting = 0; private boolean mLineStart = true; private void append(char c) throws IOException { int pos = mPos; if (pos >= (BUFFER_LEN - 1)) { flush(); pos = mPos; } mText[pos] = c; mPos = pos + 1; } private void append(String str, int i, final int length) throws IOException { if (length > BUFFER_LEN) { final int end = i + length; while (i < end) { int next = i + BUFFER_LEN; append(str, i, next < end ? BUFFER_LEN : (end - i)); i = next; } return; } int pos = mPos; if ((pos + length) > BUFFER_LEN) { flush(); pos = mPos; } str.getChars(i, i + length, mText, pos); mPos = pos + length; } private void append(char[] buf, int i, final int length) throws IOException { if (length > BUFFER_LEN) { final int end = i + length; while (i < end) { int next = i + BUFFER_LEN; append(buf, i, next < end ? BUFFER_LEN : (end - i)); i = next; } return; } int pos = mPos; if ((pos + length) > BUFFER_LEN) { flush(); pos = mPos; } System.arraycopy(buf, i, mText, pos, length); mPos = pos + length; } private void append(String str) throws IOException { append(str, 0, str.length()); } private void appendIndent(int indent) throws IOException { indent *= 4; if (indent > sSpace.length()) { indent = sSpace.length(); } append(sSpace, 0, indent); } private void escapeAndAppendString(final String string) throws IOException { final int N = string.length(); final char NE = (char) ESCAPE_TABLE.length; final String[] escapes = ESCAPE_TABLE; int lastPos = 0; int pos; for (pos = 0; pos < N; pos++) { char c = string.charAt(pos); if (c >= NE) continue; String escape = escapes[c]; if (escape == null) continue; if (lastPos < pos) append(string, lastPos, pos - lastPos); lastPos = pos + 1; append(escape); } if (lastPos < pos) append(string, lastPos, pos - lastPos); } private void escapeAndAppendString(char[] buf, int start, int len) throws IOException { final char NE = (char) ESCAPE_TABLE.length; final String[] escapes = ESCAPE_TABLE; int end = start + len; int lastPos = start; int pos; for (pos = start; pos < end; pos++) { char c = buf[pos]; if (c >= NE) continue; String escape = escapes[c]; if (escape == null) continue; if (lastPos < pos) append(buf, lastPos, pos - lastPos); lastPos = pos + 1; append(escape); } if (lastPos < pos) append(buf, lastPos, pos - lastPos); } public XmlSerializer attribute(String namespace, String name, String value) throws IOException, IllegalArgumentException, IllegalStateException { append(' '); if (namespace != null) { append(namespace); append(':'); } append(name); append("=\""); escapeAndAppendString(value); append('"'); mLineStart = false; return this; } public void cdsect(String text) throws IOException, IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } public void comment(String text) throws IOException, IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } public void docdecl(String text) throws IOException, IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } public void endDocument() throws IOException, IllegalArgumentException, IllegalStateException { flush(); } public XmlSerializer endTag(String namespace, String name) throws IOException, IllegalArgumentException, IllegalStateException { mNesting--; if (mInTag) { append(" />\n"); } else { if (mIndent && mLineStart) { appendIndent(mNesting); } append("\n"); } mLineStart = true; mInTag = false; return this; } public void entityRef(String text) throws IOException, IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } private void flushBytes() throws IOException { int position; if ((position = mBytes.position()) > 0) { mBytes.flip(); mOutputStream.write(mBytes.array(), 0, position); mBytes.clear(); } } public void flush() throws IOException { //Log.i("PackageManager", "flush mPos=" + mPos); if (mPos > 0) { if (mOutputStream != null) { CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos); CoderResult result = mCharset.encode(charBuffer, mBytes, true); while (true) { if (result.isError()) { throw new IOException(result.toString()); } else if (result.isOverflow()) { flushBytes(); result = mCharset.encode(charBuffer, mBytes, true); continue; } break; } flushBytes(); mOutputStream.flush(); } else { mWriter.write(mText, 0, mPos); mWriter.flush(); } mPos = 0; } } public int getDepth() { throw new UnsupportedOperationException(); } public boolean getFeature(String name) { throw new UnsupportedOperationException(); } public String getName() { throw new UnsupportedOperationException(); } public String getNamespace() { throw new UnsupportedOperationException(); } public String getPrefix(String namespace, boolean generatePrefix) throws IllegalArgumentException { throw new UnsupportedOperationException(); } public Object getProperty(String name) { throw new UnsupportedOperationException(); } public void ignorableWhitespace(String text) throws IOException, IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } public void processingInstruction(String text) throws IOException, IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } public void setFeature(String name, boolean state) throws IllegalArgumentException, IllegalStateException { if (name.equals("http://xmlpull.org/v1/doc/features.html#indent-output")) { mIndent = true; return; } throw new UnsupportedOperationException(); } public void setOutput(OutputStream os, String encoding) throws IOException, IllegalArgumentException, IllegalStateException { if (os == null) throw new IllegalArgumentException(); if (true) { try { mCharset = Charset.forName(encoding).newEncoder(); } catch (IllegalCharsetNameException e) { throw (UnsupportedEncodingException) (new UnsupportedEncodingException( encoding).initCause(e)); } catch (UnsupportedCharsetException e) { throw (UnsupportedEncodingException) (new UnsupportedEncodingException( encoding).initCause(e)); } mOutputStream = os; } else { setOutput( encoding == null ? new OutputStreamWriter(os) : new OutputStreamWriter(os, encoding)); } } public void setOutput(Writer writer) throws IOException, IllegalArgumentException, IllegalStateException { mWriter = writer; } public void setPrefix(String prefix, String namespace) throws IOException, IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } public void setProperty(String name, Object value) throws IllegalArgumentException, IllegalStateException { throw new UnsupportedOperationException(); } public void startDocument(String encoding, Boolean standalone) throws IOException, IllegalArgumentException, IllegalStateException { append("\n"); mLineStart = true; } public XmlSerializer startTag(String namespace, String name) throws IOException, IllegalArgumentException, IllegalStateException { if (mInTag) { append(">\n"); } if (mIndent) { appendIndent(mNesting); } mNesting++; append('<'); if (namespace != null) { append(namespace); append(':'); } append(name); mInTag = true; mLineStart = false; return this; } public XmlSerializer text(char[] buf, int start, int len) throws IOException, IllegalArgumentException, IllegalStateException { if (mInTag) { append(">"); mInTag = false; } escapeAndAppendString(buf, start, len); if (mIndent) { mLineStart = buf[start + len - 1] == '\n'; } return this; } public XmlSerializer text(String text) throws IOException, IllegalArgumentException, IllegalStateException { if (mInTag) { append(">"); mInTag = false; } escapeAndAppendString(text); if (mIndent) { mLineStart = text.length() > 0 && (text.charAt(text.length() - 1) == '\n'); } return this; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/FileStack.java ================================================ package com.kunfei.bookshelf.utils; import java.io.File; import java.util.List; /** * Created by newbiechen on 17-5-28. */ public class FileStack { private Node node = null; private int count = 0; public void push(FileSnapshot fileSnapshot) { if (fileSnapshot == null) return; Node fileNode = new Node(); fileNode.fileSnapshot = fileSnapshot; fileNode.next = node; node = fileNode; ++count; } public FileSnapshot pop() { Node fileNode = node; if (fileNode == null) return null; FileSnapshot fileSnapshot = fileNode.fileSnapshot; node = fileNode.next; --count; return fileSnapshot; } public int getSize() { return count; } //文件快照 public static class FileSnapshot { public String filePath; public List files; public int scrollOffset; } //节点 public class Node { FileSnapshot fileSnapshot; Node next; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/FileUtils.kt ================================================ package com.kunfei.bookshelf.utils import android.content.Context import android.os.Environment import android.os.storage.StorageManager import android.webkit.MimeTypeMap import androidx.annotation.IntDef import splitties.init.appCtx import timber.log.Timber import java.io.* import java.nio.charset.Charset import java.text.SimpleDateFormat import java.util.* import java.util.regex.Pattern @Suppress("unused", "MemberVisibilityCanBePrivate") object FileUtils { @JvmStatic fun createFileIfNotExist(root: File, vararg subDirFiles: String): File { val filePath = getPath(root, *subDirFiles) return createFileIfNotExist(filePath) } @JvmStatic fun createFolderIfNotExist(root: File, vararg subDirs: String): File { val filePath = getPath(root, *subDirs) return createFolderIfNotExist(filePath) } @JvmStatic fun createFolderIfNotExist(filePath: String): File { val file = File(filePath) //如果文件夹不存在,就创建它 if (!file.exists()) { file.mkdirs() } return file } @Synchronized fun createFileIfNotExist(filePath: String): File { val file = File(filePath) try { if (!file.exists()) { //创建父类文件夹 file.parent?.let { createFolderIfNotExist(it) } //创建文件 file.createNewFile() } } catch (e: IOException) { Timber.e(e) } return file } fun createFileWithReplace(filePath: String): File { val file = File(filePath) if (!file.exists()) { //创建父类文件夹 file.parent?.let { createFolderIfNotExist(it) } //创建文件 file.createNewFile() } else { file.delete() file.createNewFile() } return file } fun getPath(rootPath: String, vararg subDirFiles: String): String { val path = StringBuilder(rootPath) subDirFiles.forEach { if (it.isNotEmpty()) { if (!path.endsWith(File.separator)) { path.append(File.separator) } path.append(it) } } return path.toString() } fun getPath(root: File, vararg subDirFiles: String): String { val path = StringBuilder(root.absolutePath) subDirFiles.forEach { if (it.isNotEmpty()) { path.append(File.separator).append(it) } } return path.toString() } //递归删除文件夹下的数据 @Synchronized fun deleteFile(filePath: String) { val file = File(filePath) if (!file.exists()) return if (file.isDirectory) { val files = file.listFiles() files?.forEach { subFile -> val path = subFile.path deleteFile(path) } } //删除文件 file.delete() } fun getCachePath(): String { return appCtx.externalCache.absolutePath } @JvmStatic fun getStorageData(pContext: Context): ArrayList? { val storageManager = pContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager try { val getVolumeList = storageManager.javaClass.getMethod("getVolumeList") val storageValumeClazz = Class.forName("android.os.storage.StorageVolume") val getPath = storageValumeClazz.getMethod("getPath") val invokeVolumeList = getVolumeList.invoke(storageManager) val length = java.lang.reflect.Array.getLength(invokeVolumeList) val list = ArrayList() for (i in 0 until length) { val storageValume = java.lang.reflect.Array.get(invokeVolumeList, i) //得到StorageVolume对象 val path = getPath.invoke(storageValume) as String list.add(path) } return list } catch (e: java.lang.Exception) { e.printStackTrace() } return null } @JvmStatic fun getSdCardPath(): String { @Suppress("DEPRECATION") var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath try { sdCardDirectory = File(sdCardDirectory).canonicalPath } catch (e: IOException) { Timber.e(e) } return sdCardDirectory } const val BY_NAME_ASC = 0 const val BY_NAME_DESC = 1 const val BY_TIME_ASC = 2 const val BY_TIME_DESC = 3 const val BY_SIZE_ASC = 4 const val BY_SIZE_DESC = 5 const val BY_EXTENSION_ASC = 6 const val BY_EXTENSION_DESC = 7 @IntDef(value = [BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC]) @kotlin.annotation.Retention(AnnotationRetention.SOURCE) annotation class SortType /** * 将目录分隔符统一为平台默认的分隔符,并为目录结尾添加分隔符 */ fun separator(path: String): String { var path1 = path val separator = File.separator path1 = path1.replace("\\", separator) if (!path1.endsWith(separator)) { path1 += separator } return path1 } fun closeSilently(c: Closeable?) { if (c == null) { return } try { c.close() } catch (ignored: IOException) { } } /** * 列出指定目录下的所有子目录 */ @JvmOverloads fun listDirs( startDirPath: String, excludeDirs: Array? = null, @SortType sortType: Int = BY_NAME_ASC ): Array { var excludeDirs1 = excludeDirs val dirList = ArrayList() val startDir = File(startDirPath) if (!startDir.isDirectory) { return arrayOf() } val dirs = startDir.listFiles(FileFilter { f -> if (f == null) { return@FileFilter false } f.isDirectory }) ?: return arrayOf() if (excludeDirs1 == null) { excludeDirs1 = arrayOf() } for (dir in dirs) { val file = dir.absoluteFile if (!excludeDirs1.contentDeepToString().contains(file.name)) { dirList.add(file) } } when (sortType) { BY_NAME_ASC -> Collections.sort(dirList, SortByName()) BY_NAME_DESC -> { Collections.sort(dirList, SortByName()) dirList.reverse() } BY_TIME_ASC -> Collections.sort(dirList, SortByTime()) BY_TIME_DESC -> { Collections.sort(dirList, SortByTime()) dirList.reverse() } BY_SIZE_ASC -> Collections.sort(dirList, SortBySize()) BY_SIZE_DESC -> { Collections.sort(dirList, SortBySize()) dirList.reverse() } BY_EXTENSION_ASC -> Collections.sort(dirList, SortByExtension()) BY_EXTENSION_DESC -> { Collections.sort(dirList, SortByExtension()) dirList.reverse() } } return dirList.toTypedArray() } /** * 列出指定目录下的所有子目录及所有文件 */ @JvmOverloads fun listDirsAndFiles( startDirPath: String, allowExtensions: Array? = null ): Array? { val dirs: Array? val files: Array? = if (allowExtensions == null) { listFiles(startDirPath) } else { listFiles(startDirPath, allowExtensions) } dirs = listDirs(startDirPath) if (files == null) { return null } return dirs + files } /** * 列出指定目录下的所有文件 */ @JvmOverloads fun listFiles( startDirPath: String, filterPattern: Pattern? = null, @SortType sortType: Int = BY_NAME_ASC ): Array { val fileList = ArrayList() val f = File(startDirPath) if (!f.isDirectory) { return arrayOf() } val files = f.listFiles(FileFilter { file -> if (file == null) { return@FileFilter false } if (file.isDirectory) { return@FileFilter false } filterPattern?.matcher(file.name)?.find() ?: true }) ?: return arrayOf() for (file in files) { fileList.add(file.absoluteFile) } when (sortType) { BY_NAME_ASC -> Collections.sort(fileList, SortByName()) BY_NAME_DESC -> { Collections.sort(fileList, SortByName()) fileList.reverse() } BY_TIME_ASC -> Collections.sort(fileList, SortByTime()) BY_TIME_DESC -> { Collections.sort(fileList, SortByTime()) fileList.reverse() } BY_SIZE_ASC -> Collections.sort(fileList, SortBySize()) BY_SIZE_DESC -> { Collections.sort(fileList, SortBySize()) fileList.reverse() } BY_EXTENSION_ASC -> Collections.sort(fileList, SortByExtension()) BY_EXTENSION_DESC -> { Collections.sort(fileList, SortByExtension()) fileList.reverse() } } return fileList.toTypedArray() } /** * 列出指定目录下的所有文件 */ fun listFiles(startDirPath: String, allowExtensions: Array?): Array? { val file = File(startDirPath) return file.listFiles { _, name -> //返回当前目录所有以某些扩展名结尾的文件 val extension = getExtension(name) allowExtensions?.contentDeepToString()?.contains(extension) == true || allowExtensions == null } } /** * 列出指定目录下的所有文件 */ fun listFiles(startDirPath: String, allowExtension: String?): Array? { return if (allowExtension == null) listFiles(startDirPath, allowExtension = null) else listFiles(startDirPath, arrayOf(allowExtension)) } /** * 判断文件或目录是否存在 */ fun exist(path: String): Boolean { val file = File(path) return file.exists() } /** * 删除文件或目录 */ @JvmOverloads fun delete(file: File, deleteRootDir: Boolean = false): Boolean { var result = false if (file.isFile) { //是文件 result = deleteResolveEBUSY(file) } else { //是目录 val files = file.listFiles() ?: return false if (files.isEmpty()) { result = deleteRootDir && deleteResolveEBUSY(file) } else { for (f in files) { delete(f, deleteRootDir) result = deleteResolveEBUSY(f) } } if (deleteRootDir) { result = deleteResolveEBUSY(file) } } return result } /** * bug: open failed: EBUSY (Device or resource busy) * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy */ private fun deleteResolveEBUSY(file: File): Boolean { // Before you delete a Directory or File: rename it! val to = File(file.absolutePath + System.currentTimeMillis()) file.renameTo(to) return to.delete() } /** * 删除文件或目录 */ @JvmOverloads fun delete(path: String, deleteRootDir: Boolean = false): Boolean { val file = File(path) return if (file.exists()) { delete(file, deleteRootDir) } else false } /** * 复制文件为另一个文件,或复制某目录下的所有文件及目录到另一个目录下 */ fun copy(src: String, tar: String): Boolean { val srcFile = File(src) return srcFile.exists() && copy(srcFile, File(tar)) } /** * 复制文件或目录 */ fun copy(src: File, tar: File): Boolean { try { if (src.isFile) { val `is` = FileInputStream(src) val op = FileOutputStream(tar) val bis = BufferedInputStream(`is`) val bos = BufferedOutputStream(op) val bt = ByteArray(1024 * 8) while (true) { val len = bis.read(bt) if (len == -1) { break } else { bos.write(bt, 0, len) } } bis.close() bos.close() } else if (src.isDirectory) { tar.mkdirs() src.listFiles()?.forEach { file -> copy(file.absoluteFile, File(tar.absoluteFile, file.name)) } } return true } catch (e: Exception) { return false } } /** * 移动文件或目录 */ fun move(src: String, tar: String): Boolean { return move(File(src), File(tar)) } /** * 移动文件或目录 */ fun move(src: File, tar: File): Boolean { return rename(src, tar) } /** * 文件重命名 */ fun rename(oldPath: String, newPath: String): Boolean { return rename(File(oldPath), File(newPath)) } /** * 文件重命名 */ fun rename(src: File, tar: File): Boolean { return src.renameTo(tar) } /** * 读取文本文件, 失败将返回空串 */ @JvmOverloads fun readText(filepath: String, charset: String = "utf-8"): String { try { val data = readBytes(filepath) if (data != null) { return String(data, Charset.forName(charset)).trim { it <= ' ' } } } catch (ignored: UnsupportedEncodingException) { } return "" } /** * 读取文件内容, 失败将返回空串 */ fun readBytes(filepath: String): ByteArray? { var fis: FileInputStream? = null try { fis = FileInputStream(filepath) val baos = ByteArrayOutputStream() val buffer = ByteArray(1024) while (true) { val len = fis.read(buffer, 0, buffer.size) if (len == -1) { break } else { baos.write(buffer, 0, len) } } val data = baos.toByteArray() baos.close() return data } catch (e: IOException) { return null } finally { closeSilently(fis) } } /** * 保存文本内容 */ @JvmOverloads fun writeText(filepath: String, content: String, charset: String = "utf-8"): Boolean { return try { writeBytes(filepath, content.toByteArray(charset(charset))) } catch (e: UnsupportedEncodingException) { false } } /** * 保存文件内容 */ fun writeBytes(filepath: String, data: ByteArray): Boolean { val file = File(filepath) var fos: FileOutputStream? = null return try { if (!file.exists()) { file.parentFile?.mkdirs() file.createNewFile() } fos = FileOutputStream(filepath) fos.write(data) true } catch (e: IOException) { false } finally { closeSilently(fos) } } /** * 保存文件内容 */ fun writeInputStream(filepath: String, data: InputStream): Boolean { val file = File(filepath) return writeInputStream(file, data) } /** * 保存文件内容 */ fun writeInputStream(file: File, data: InputStream): Boolean { var fos: FileOutputStream? = null return try { if (!file.exists()) { file.parentFile?.mkdirs() file.createNewFile() } val buffer = ByteArray(1024 * 4) fos = FileOutputStream(file) while (true) { val len = data.read(buffer, 0, buffer.size) if (len == -1) { break } else { fos.write(buffer, 0, len) } } data.close() fos.flush() true } catch (e: IOException) { false } finally { closeSilently(fos) } } /** * 追加文本内容 */ fun appendText(path: String, content: String): Boolean { val file = File(path) var writer: FileWriter? = null return try { if (!file.exists()) { file.createNewFile() } writer = FileWriter(file, true) writer.write(content) true } catch (e: IOException) { false } finally { closeSilently(writer) } } /** * 获取文件大小 */ fun getLength(path: String): Long { val file = File(path) return if (!file.isFile || !file.exists()) { 0 } else file.length() } /** * 获取文件或网址的名称(包括后缀) */ fun getName(pathOrUrl: String?): String { if (pathOrUrl == null) { return "" } val pos = pathOrUrl.lastIndexOf('/') return if (0 <= pos) { pathOrUrl.substring(pos + 1) } else { System.currentTimeMillis().toString() + "." + getExtension(pathOrUrl) } } /** * 获取文件名(不包括扩展名) */ fun getNameExcludeExtension(path: String): String { return try { var fileName = File(path).name val lastIndexOf = fileName.lastIndexOf(".") if (lastIndexOf != -1) { fileName = fileName.substring(0, lastIndexOf) } fileName } catch (e: Exception) { "" } } /** * 获取格式化后的文件大小 */ fun getSize(path: String): String { val fileSize = getLength(path) return ConvertUtils.toFileSizeString(fileSize) } /** * 获取文件后缀,不包括“.” */ fun getExtension(pathOrUrl: String): String { val dotPos = pathOrUrl.lastIndexOf('.') return if (0 <= dotPos) { pathOrUrl.substring(dotPos + 1) } else { "ext" } } /** * 获取文件的MIME类型 */ fun getMimeType(pathOrUrl: String): String { val ext = getExtension(pathOrUrl) val map = MimeTypeMap.getSingleton() return map.getMimeTypeFromExtension(ext) ?: "*/*" } /** * 获取格式化后的文件/目录创建或最后修改时间 */ @JvmOverloads fun getDateTime(path: String, format: String = "yyyy年MM月dd日HH:mm"): String { val file = File(path) return getDateTime(file, format) } /** * 获取格式化后的文件/目录创建或最后修改时间 */ fun getDateTime(file: File, format: String): String { val cal = Calendar.getInstance() cal.timeInMillis = file.lastModified() return SimpleDateFormat(format, Locale.PRC).format(cal.time) } /** * 比较两个文件的最后修改时间 */ fun compareLastModified(path1: String, path2: String): Int { val stamp1 = File(path1).lastModified() val stamp2 = File(path2).lastModified() return when { stamp1 > stamp2 -> { 1 } stamp1 < stamp2 -> { -1 } else -> { 0 } } } /** * 创建多级别的目录 */ fun makeDirs(path: String): Boolean { return makeDirs(File(path)) } /** * 创建多级别的目录 */ fun makeDirs(file: File): Boolean { return file.mkdirs() } class SortByExtension : Comparator { override fun compare(f1: File?, f2: File?): Int { return if (f1 == null || f2 == null) { if (f1 == null) { -1 } else { 1 } } else { if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { f1.name.compareTo(f2.name, ignoreCase = true) } } } } class SortByName : Comparator { private var caseSensitive: Boolean = false constructor(caseSensitive: Boolean) { this.caseSensitive = caseSensitive } constructor() { this.caseSensitive = false } override fun compare(f1: File?, f2: File?): Int { if (f1 == null || f2 == null) { return if (f1 == null) { -1 } else { 1 } } else { return if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { val s1 = f1.name val s2 = f2.name if (caseSensitive) { s1.cnCompare(s2) } else { s1.compareTo(s2, ignoreCase = true) } } } } } class SortBySize : Comparator { override fun compare(f1: File?, f2: File?): Int { return if (f1 == null || f2 == null) { if (f1 == null) { -1 } else { 1 } } else { if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { if (f1.length() < f2.length()) { -1 } else { 1 } } } } } class SortByTime : Comparator { override fun compare(f1: File?, f2: File?): Int { return if (f1 == null || f2 == null) { if (f1 == null) { -1 } else { 1 } } else { if (f1.isDirectory && f2.isFile) { -1 } else if (f1.isFile && f2.isDirectory) { 1 } else { if (f1.lastModified() > f2.lastModified()) { -1 } else { 1 } } } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/FloatExtensions.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils import android.content.res.Resources val Float.dp: Float get() = android.util.TypedValue.applyDimension( android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics ) val Float.sp: Float get() = android.util.TypedValue.applyDimension( android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics ) ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/GsonExtensions.kt ================================================ package com.kunfei.bookshelf.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) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/GsonUtils.java ================================================ package com.kunfei.bookshelf.utils; import android.text.TextUtils; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import java.util.ArrayList; import java.util.List; @SuppressWarnings({"unused"}) public class GsonUtils { /** * 将Json数据解析成相应的映射对象 */ public static T parseJObject(String jsonData, Class type) { T result = null; if (!TextUtils.isEmpty(jsonData)) { Gson gson = new GsonBuilder().create(); try { result = gson.fromJson(jsonData, type); } catch (Exception e) { e.printStackTrace(); } } return result; } /** * 将Json数组解析成相应的映射对象List */ public static List parseJArray(String jsonData, Class type) { List result = null; if (!TextUtils.isEmpty(jsonData)) { Gson gson = new GsonBuilder().create(); try { JsonParser parser = new JsonParser(); JsonArray JArray = parser.parse(jsonData).getAsJsonArray(); if (JArray != null) { result = new ArrayList<>(); for (JsonElement obj : JArray) { try { T cse = gson.fromJson(obj, type); result.add(cse); } catch (Exception e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } } return result; } /** * 将对象转换成Json */ public static String toJsonWithSerializeNulls(T entity) { entity.getClass(); Gson gson = new GsonBuilder().serializeNulls().create(); String result = ""; try { result = gson.toJson(entity); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 将list排除值为null的字段转换成Json数组 */ public static String toJsonArrayWithSerializeNulls(List list) { Gson gson = new GsonBuilder().serializeNulls().create(); String result = ""; try { result = gson.toJson(list); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 将list中将Expose注解的字段转换成Json数组 */ public static String toJsonArrayWithExpose(List list) { Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); String result = ""; try { result = gson.toJson(list); } catch (Exception e) { e.printStackTrace(); } return result; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/HandlerUtils.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils import android.os.Build.VERSION.SDK_INT import android.os.Handler import android.os.Looper /** This main looper cache avoids synchronization overhead when accessed repeatedly. */ @JvmField val mainLooper: Looper = Looper.getMainLooper()!! @JvmField val mainThread: Thread = mainLooper.thread val isMainThread: Boolean inline get() = mainThread === Thread.currentThread() @PublishedApi internal val currentThread: Any? inline get() = Thread.currentThread() @JvmField val mainHandler: Handler = if (SDK_INT >= 28) Handler.createAsync(mainLooper) else try { Handler::class.java.getDeclaredConstructor( Looper::class.java, Handler.Callback::class.java, Boolean::class.javaPrimitiveType // async ).newInstance(mainLooper, null, true) } catch (ignored: NoSuchMethodException) { Handler(mainLooper) // Hidden constructor absent. Fall back to non-async constructor. } fun runOnUI(function: () -> Unit) { if (isMainThread) { function() } else { mainHandler.post(function) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/IOUtils.java ================================================ package com.kunfei.bookshelf.utils; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; /** * Created by newbiechen on 17-5-11. */ public class IOUtils { public static void close(Closeable closeable) { if (closeable == null) return; try { closeable.close(); } catch (IOException e) { e.printStackTrace(); } } public static String toString(InputStream inputStream) { Scanner s = new Scanner(inputStream).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/IntentExtensions.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils import android.content.Intent fun Intent.putJson(key: String, any: Any?) { any?.let { putExtra(key, GSON.toJson(any)) } } inline fun Intent.getJsonObject(key: String): T? { val value = getStringExtra(key) return GSON.fromJsonObject(value) } inline fun Intent.getJsonArray(key: String): List? { val value = getStringExtra(key) return GSON.fromJsonArray(value) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ListUtil.java ================================================ package com.kunfei.bookshelf.utils; import java.util.ArrayList; import java.util.List; public class ListUtil { public static List filter(List list, ListLook hook) { ArrayList r = new ArrayList<>(); for (T t : list) { if (hook.test(t)) { r.add(t); } } r.trimToSize(); return r; } public interface ListLook { boolean test(T t); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/MD5Utils.java ================================================ package com.kunfei.bookshelf.utils; /** * Created by newbiechen on 2018/1/1. */ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * 将字符串转化为MD5 */ public class MD5Utils { public static String strToMd5By32(String str) { String reStr = null; try { MessageDigest md5 = MessageDigest.getInstance("MD5"); byte[] bytes = md5.digest(str.getBytes()); StringBuilder stringBuffer = new StringBuilder(); for (byte b : bytes) { int bt = b & 0xff; if (bt < 16) { stringBuffer.append(0); } stringBuffer.append(Integer.toHexString(bt)); } reStr = stringBuffer.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return reStr; } public static String strToMd5By16(String str) { String reStr = strToMd5By32(str); if (reStr != null) { reStr = reStr.substring(8, 24); } return reStr; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/MarkdownUtils.java ================================================ package com.kunfei.bookshelf.utils; import android.os.Build; import android.text.Html; import android.widget.TextView; import java.util.regex.Matcher; import java.util.regex.Pattern; public class MarkdownUtils { @SuppressWarnings("deprecation") public static CharSequence simpleMarkdownConverter(String text) { Pattern listPtn = Pattern.compile("^[\\-*] "); Pattern headPtn = Pattern.compile("^(#{1,6}) "); String strongemPtn = "\\*\\*\\*([^*]+)\\*\\*\\*"; String strongPtn = "\\*\\*([^*]+)\\*\\*"; String emPtn = "\\*([^*]+)\\*"; boolean isInList = false; StringBuilder builder = new StringBuilder(); for (String line : text.split("\\n")) { Matcher listMtc = listPtn.matcher(line); Matcher headMtc = headPtn.matcher(line); boolean isList = listMtc.find(); if (!isInList && isList) { builder.append("

    \n"); isInList = true; } else if (isInList && !isList) { builder.append("
\n"); isInList = false; } if (isList) { line = "
  • " + line.substring(2) + "
  • \n"; } else if (headMtc.find()) { final int level = headMtc.group(1).length(); line = "" + line.substring(level + 1) + "\n"; } else { line = "
    " + line + "
    \n"; } line = line.replaceAll(strongemPtn, "$1"); line = line.replaceAll(strongPtn, "$1"); line = line.replaceAll(emPtn, "$1"); builder.append(line); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Html.fromHtml(builder.toString(), Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM); } else { return Html.fromHtml(builder.toString()); } } public static void setText(TextView view, String text) { view.setText(simpleMarkdownConverter(text)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/MeUtils.java ================================================ package com.kunfei.bookshelf.utils; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; /** * by yangyxd * data: 2019.08.29 */ public class MeUtils { /** 获取 assets 中指定目录中的文件名称列表 */ public static CharSequence[] getAssetsFileList(AssetManager am, String path) throws IOException { final String[] fs = am.list(path); if (fs == null || fs.length == 0) return null; final CharSequence[] items = new CharSequence[fs.length]; for (int i=0; i reqWidth || options.outHeight > reqHeight) { int widthRatio = Math.round((float) options.outWidth / (float) reqWidth); int heightRatio = Math.round((float) options.outHeight / (float) reqHeight); inSampleSize = Math.min(widthRatio, heightRatio); } return inSampleSize; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/NetworkUtils.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.utils; import android.annotation.SuppressLint; import android.content.Context; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.os.Build; import android.text.TextUtils; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.URL; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; import retrofit2.Response; public class NetworkUtils { public static final Pattern headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE); public static final int SUCCESS = 10000; public static final int ERROR_CODE_NONET = 10001; public static final int ERROR_CODE_OUTTIME = 10002; public static final int ERROR_CODE_ANALY = 10003; @SuppressLint("UseSparseArrays") private static final Map errorMap = new HashMap<>(); static { errorMap.put(ERROR_CODE_NONET, MApplication.getInstance().getString(R.string.net_error_10001)); errorMap.put(ERROR_CODE_OUTTIME, MApplication.getInstance().getString(R.string.net_error_10002)); errorMap.put(ERROR_CODE_ANALY, MApplication.getInstance().getString(R.string.net_error_10003)); } public static String getErrorTip(int code) { return errorMap.get(code); } public static boolean isNetWorkAvailable() { ConnectivityManager cm = (ConnectivityManager) MApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE); if (Build.VERSION.SDK_INT < 23) { NetworkInfo mWiFiNetworkInfo = cm.getActiveNetworkInfo(); if (mWiFiNetworkInfo != null) { //移动数据 if (mWiFiNetworkInfo.getType() == ConnectivityManager.TYPE_WIFI) {//WIFI return true; } else return mWiFiNetworkInfo.getType() == ConnectivityManager.TYPE_MOBILE; } } else { Network network = cm.getActiveNetwork(); if (network != null) { NetworkCapabilities nc = cm.getNetworkCapabilities(network); if (nc != null) { //移动数据 if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {//WIFI return true; } else return nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); } } } return false; } public static String getUrl(Response response) { okhttp3.Response networkResponse = response.raw().networkResponse(); if (networkResponse != null) { return networkResponse.request().url().toString(); } else { return response.raw().request().url().toString(); } } /** * 获取绝对地址 */ public static String getAbsoluteURL(String baseURL, String relativePath) { if (TextUtils.isEmpty(relativePath)) return ""; if (TextUtils.isEmpty(baseURL)) return relativePath; String header = null; if (StringUtils.startWithIgnoreCase(relativePath, "@header:")) { header = relativePath.substring(0, relativePath.indexOf("}") + 1); relativePath = relativePath.substring(header.length()); } try { URL absoluteUrl = new URL(baseURL); URL parseUrl = new URL(absoluteUrl, relativePath); relativePath = parseUrl.toString(); if (header != null) { relativePath = header + relativePath; } return relativePath; } catch (Exception e) { e.printStackTrace(); } return relativePath; } public static String getAbsoluteURL(URL baseURL, String relativePath) { if (baseURL == null) return relativePath; String header = null; if (StringUtils.startWithIgnoreCase(relativePath, "@header:")) { header = relativePath.substring(0, relativePath.indexOf("}") + 1); relativePath = relativePath.substring(header.length()); } try { URL parseUrl = new URL(baseURL, relativePath); relativePath = parseUrl.toString(); if (header != null) { relativePath = header + relativePath; } return relativePath; } catch (Exception e) { e.printStackTrace(); } return relativePath; } public static boolean isUrl(String urlStr) { String regex = "^(https?)://.+$";//设置正则表达式 return urlStr.matches(regex); } /** * Ipv4 address check. */ private static final Pattern IPV4_PATTERN = Pattern.compile( "^(" + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}" + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"); /** * Check if valid IPV4 address. * * @param input the address string to check for validity. * @return True if the input parameter is a valid IPv4 address. */ public static boolean isIPv4Address(String input) { return IPV4_PATTERN.matcher(input).matches(); } /** * Get local Ip address. */ public static InetAddress getLocalIPAddress() { Enumeration enumeration = null; try { enumeration = NetworkInterface.getNetworkInterfaces(); } catch (SocketException e) { e.printStackTrace(); } if (enumeration != null) { while (enumeration.hasMoreElements()) { NetworkInterface nif = enumeration.nextElement(); Enumeration inetAddresses = nif.getInetAddresses(); if (inetAddresses != null) { while (inetAddresses.hasMoreElements()) { InetAddress inetAddress = inetAddresses.nextElement(); if (!inetAddress.isLoopbackAddress() && isIPv4Address(inetAddress.getHostAddress())) { return inetAddress; } } } } } return null; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ReadAssets.java ================================================ package com.kunfei.bookshelf.utils; import android.content.Context; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; public class ReadAssets { @SuppressWarnings("ResultOfMethodCallIgnored") public static String getText(Context context, String fileName) { try { //Return an AssetManager instance for your application's package InputStream is = context.getAssets().open(fileName); int size = is.available(); // Read the entire asset into a local byte buffer. byte[] buffer = new byte[size]; is.read(buffer); is.close(); // Convert the buffer into a string. // Finally stick the string into the text view. return new String(buffer, StandardCharsets.UTF_8); } catch (IOException e) { // Should never happen! e.printStackTrace(); } return "读取错误,请检查文件名"; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/RealPathUtil.kt ================================================ package com.kunfei.bookshelf.utils import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Build import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore import timber.log.Timber import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException @Suppress("unused") object RealPathUtil { /** * Method for return file path of Gallery image * @return path of the selected image file from gallery */ private var filePathUri: Uri? = null @Suppress("DEPRECATION") @JvmStatic fun getPath(context: Context, uri: Uri): String? { //check here to KITKAT or new version @SuppressLint("ObsoleteSdkInt") val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT filePathUri = uri // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { val docId = DocumentsContract.getDocumentId(uri) val split = docId.split(":").toTypedArray() val type = split[0] if ("primary".equals(type, ignoreCase = true)) { return Environment.getExternalStorageDirectory().toString() + "/" + split[1] } } else if (isDownloadsDocument(uri)) { val id = DocumentsContract.getDocumentId(uri) val contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) ) //return getDataColumn(context, uri, null, null); return getDataColumn(context, contentUri, null, null) } else if (isMediaDocument(uri)) { val docId = DocumentsContract.getDocumentId(uri) val split = docId.split(":").toTypedArray() val type = split[0] var contentUri: Uri? = null when (type) { "image" -> { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI } "video" -> { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI } "audio" -> { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI } } val selection = "_id=?" val selectionArgs = arrayOf( split[1] ) return getDataColumn(context, contentUri, selection, selectionArgs) } } else if ("content".equals( uri.scheme, ignoreCase = true ) ) { // Return the remote address return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( context, uri, null, null ) } else if ("file".equals(uri.scheme, ignoreCase = true)) { return uri.path } return null } /** * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ private fun getDataColumn( context: Context, uri: Uri?, selection: String?, selectionArgs: Array? ): String? { var cursor: Cursor? = null val column = "_data" val projection = arrayOf( column ) try { cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) if (cursor != null && cursor.moveToFirst()) { val index = cursor.getColumnIndexOrThrow(column) return cursor.getString(index) } } catch (e: IllegalArgumentException) { Timber.e(e) val file = File(context.cacheDir, "tmp") val filePath = file.absolutePath var input: FileInputStream? = null var output: FileOutputStream? = null try { val pfd = context.contentResolver.openFileDescriptor(filePathUri!!, "r") ?: return null val fd = pfd.fileDescriptor input = FileInputStream(fd) output = FileOutputStream(filePath) var read: Int val bytes = ByteArray(4096) while (input.read(bytes).also { read = it } != -1) { output.write(bytes, 0, read) } return File(filePath).absolutePath } catch (ignored: IOException) { Timber.e(ignored) } finally { input?.close() output?.close() } } finally { cursor?.close() } return null } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private fun isExternalStorageDocument(uri: Uri): Boolean { return "com.android.externalstorage.documents" == uri.authority } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private fun isDownloadsDocument(uri: Uri): Boolean { return "com.android.providers.downloads.documents" == uri.authority } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private fun isMediaDocument(uri: Uri): Boolean { return "com.android.providers.media.documents" == uri.authority } /** * @param uri The Uri to check. * @return Whether the Uri authority is Google Photos. */ private fun isGooglePhotosUri(uri: Uri): Boolean { return "com.google.android.apps.photos.content" == uri.authority } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/RxUtils.java ================================================ package com.kunfei.bookshelf.utils; import androidx.annotation.NonNull; import io.reactivex.Observable; import io.reactivex.ObservableSource; import io.reactivex.Single; import io.reactivex.SingleSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; /** * Created by newbiechen on 17-4-29. */ public class RxUtils { public static SingleSource toSimpleSingle(Single upstream) { return upstream.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } @NonNull public static ObservableSource toSimpleSingle(Observable upstream) { return upstream.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } public static TwoTuple twoTuple(T first, R second) { return new TwoTuple(first, second); } public static class TwoTuple { public final A first; public final B second; public TwoTuple(A a, B b) { this.first = a; this.second = b; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ScreenUtils.java ================================================ package com.kunfei.bookshelf.utils; import android.annotation.SuppressLint; import android.content.res.Resources; import android.util.DisplayMetrics; import android.view.View; import androidx.appcompat.app.AppCompatActivity; import com.kunfei.bookshelf.MApplication; import java.lang.reflect.Method; /** * Created by newbiechen on 17-5-1. */ @SuppressWarnings({"unused", "WeakerAccess"}) public class ScreenUtils { public static int dpToPx(int dp) { DisplayMetrics metrics = getDisplayMetrics(); return (int) (dp * metrics.density + 0.5f * (dp >= 0 ? 1 : -1)); } public static int pxToDp(int px) { DisplayMetrics metrics = getDisplayMetrics(); return (int) (px / metrics.density); } public static int spToPx(int sp) { float fontScale = getDisplayMetrics().scaledDensity; return (int) (sp * fontScale + 0.5f); } public static int pxToSp(int px) { DisplayMetrics metrics = getDisplayMetrics(); return (int) (px / metrics.scaledDensity); } /** * 获取手机显示App区域的大小(头部导航栏+ActionBar+根布局),不包括虚拟按钮 * * @return */ public static int[] getAppSize() { int[] size = new int[2]; DisplayMetrics metrics = getDisplayMetrics(); size[0] = metrics.widthPixels; size[1] = metrics.heightPixels; return size; } /** * 获取整个手机屏幕的大小(包括虚拟按钮) * 必须在onWindowFocus方法之后使用 * * @param activity * @return */ public static int[] getScreenSize(AppCompatActivity activity) { int[] size = new int[2]; View decorView = activity.getWindow().getDecorView(); size[0] = decorView.getWidth(); size[1] = decorView.getHeight(); return size; } /** * 获取状态栏的高度 */ public static int getStatusBarHeight() { Resources resources = MApplication.getInstance().getResources(); int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android"); return resources.getDimensionPixelSize(resourceId); } /** * 获取虚拟按键的高度 */ public static int getNavigationBarHeight() { int navigationBarHeight = 0; Resources rs = MApplication.getInstance().getResources(); int id = rs.getIdentifier("navigation_bar_height", "dimen", "android"); if (id > 0 && hasNavigationBar()) { navigationBarHeight = rs.getDimensionPixelSize(id); } return navigationBarHeight; } /** * 是否存在虚拟按键 * * @return */ @SuppressWarnings("unchecked") private static boolean hasNavigationBar() { boolean hasNavigationBar = false; Resources rs = MApplication.getInstance().getResources(); int id = rs.getIdentifier("config_showNavigationBar", "bool", "android"); if (id > 0) { hasNavigationBar = rs.getBoolean(id); } try { @SuppressLint("PrivateApi") Class systemPropertiesClass = Class.forName("android.os.SystemProperties"); Method m = systemPropertiesClass.getMethod("get", String.class); String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys"); if ("1".equals(navBarOverride)) { hasNavigationBar = false; } else if ("0".equals(navBarOverride)) { hasNavigationBar = true; } } catch (Exception ignored) { } return hasNavigationBar; } public static DisplayMetrics getDisplayMetrics() { return MApplication .getInstance() .getResources() .getDisplayMetrics(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/Selector.java ================================================ package com.kunfei.bookshelf.utils; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.StateListDrawable; import androidx.annotation.ColorInt; import androidx.annotation.Dimension; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.core.content.ContextCompat; public class Selector { public static ShapeSelector shapeBuild() { return new ShapeSelector(); } public static ColorSelector colorBuild() { return new ColorSelector(); } public static DrawableSelector drawableBuild() { return new DrawableSelector(); } /** * 形状ShapeSelector * * @author hjy * created at 2017/12/11 22:26 */ public static final class ShapeSelector { @IntDef({GradientDrawable.RECTANGLE, GradientDrawable.OVAL, GradientDrawable.LINE, GradientDrawable.RING}) private @interface Shape { } private int mShape; //the shape of background private int mDefaultBgColor; //default background color private int mDisabledBgColor; //state_enabled = false private int mPressedBgColor; //state_pressed = true private int mSelectedBgColor; //state_selected = true private int mFocusedBgColor; //state_focused = true private int mCheckedBgColor; //state_checked = true private int mStrokeWidth; //stroke width in pixel private int mDefaultStrokeColor; //default stroke color private int mDisabledStrokeColor; //state_enabled = false private int mPressedStrokeColor; //state_pressed = true private int mSelectedStrokeColor; //state_selected = true private int mFocusedStrokeColor; //state_focused = true private int mCheckedStrokeColor; //state_checked = true private int mCornerRadius; //corner radius private boolean hasSetDisabledBgColor = false; private boolean hasSetPressedBgColor = false; private boolean hasSetSelectedBgColor = false; private boolean hasSetFocusedBgColor = false; private boolean hasSetCheckedBgColor = false; private boolean hasSetDisabledStrokeColor = false; private boolean hasSetPressedStrokeColor = false; private boolean hasSetSelectedStrokeColor = false; private boolean hasSetFocusedStrokeColor = false; private boolean hasSetCheckedStrokeColor = false; public ShapeSelector() { //initialize default values mShape = GradientDrawable.RECTANGLE; mDefaultBgColor = Color.TRANSPARENT; mDisabledBgColor = Color.TRANSPARENT; mPressedBgColor = Color.TRANSPARENT; mSelectedBgColor = Color.TRANSPARENT; mFocusedBgColor = Color.TRANSPARENT; mStrokeWidth = 0; mDefaultStrokeColor = Color.TRANSPARENT; mDisabledStrokeColor = Color.TRANSPARENT; mPressedStrokeColor = Color.TRANSPARENT; mSelectedStrokeColor = Color.TRANSPARENT; mFocusedStrokeColor = Color.TRANSPARENT; mCornerRadius = 0; } public ShapeSelector setShape(@Shape int shape) { mShape = shape; return this; } public ShapeSelector setDefaultBgColor(@ColorInt int color) { mDefaultBgColor = color; if (!hasSetDisabledBgColor) mDisabledBgColor = color; if (!hasSetPressedBgColor) mPressedBgColor = color; if (!hasSetSelectedBgColor) mSelectedBgColor = color; if (!hasSetFocusedBgColor) mFocusedBgColor = color; return this; } public ShapeSelector setDisabledBgColor(@ColorInt int color) { mDisabledBgColor = color; hasSetDisabledBgColor = true; return this; } public ShapeSelector setPressedBgColor(@ColorInt int color) { mPressedBgColor = color; hasSetPressedBgColor = true; return this; } public ShapeSelector setSelectedBgColor(@ColorInt int color) { mSelectedBgColor = color; hasSetSelectedBgColor = true; return this; } public ShapeSelector setFocusedBgColor(@ColorInt int color) { mFocusedBgColor = color; hasSetPressedBgColor = true; return this; } public ShapeSelector setCheckedBgColor(@ColorInt int color) { mCheckedBgColor = color; hasSetCheckedBgColor = true; return this; } public ShapeSelector setStrokeWidth(@Dimension int width) { mStrokeWidth = width; return this; } public ShapeSelector setDefaultStrokeColor(@ColorInt int color) { mDefaultStrokeColor = color; if (!hasSetDisabledStrokeColor) mDisabledStrokeColor = color; if (!hasSetPressedStrokeColor) mPressedStrokeColor = color; if (!hasSetSelectedStrokeColor) mSelectedStrokeColor = color; if (!hasSetFocusedStrokeColor) mFocusedStrokeColor = color; return this; } public ShapeSelector setDisabledStrokeColor(@ColorInt int color) { mDisabledStrokeColor = color; hasSetDisabledStrokeColor = true; return this; } public ShapeSelector setPressedStrokeColor(@ColorInt int color) { mPressedStrokeColor = color; hasSetPressedStrokeColor = true; return this; } public ShapeSelector setSelectedStrokeColor(@ColorInt int color) { mSelectedStrokeColor = color; hasSetSelectedStrokeColor = true; return this; } public ShapeSelector setCheckedStrokeColor(@ColorInt int color) { mCheckedStrokeColor = color; hasSetCheckedStrokeColor = true; return this; } public ShapeSelector setFocusedStrokeColor(@ColorInt int color) { mFocusedStrokeColor = color; hasSetFocusedStrokeColor = true; return this; } public ShapeSelector setCornerRadius(@Dimension int radius) { mCornerRadius = radius; return this; } public StateListDrawable create() { StateListDrawable selector = new StateListDrawable(); //enabled = false if (hasSetDisabledBgColor || hasSetDisabledStrokeColor) { GradientDrawable disabledShape = getItemShape(mShape, mCornerRadius, mDisabledBgColor, mStrokeWidth, mDisabledStrokeColor); selector.addState(new int[]{-android.R.attr.state_enabled}, disabledShape); } //pressed = true if (hasSetPressedBgColor || hasSetPressedStrokeColor) { GradientDrawable pressedShape = getItemShape(mShape, mCornerRadius, mPressedBgColor, mStrokeWidth, mPressedStrokeColor); selector.addState(new int[]{android.R.attr.state_pressed}, pressedShape); } //selected = true if (hasSetSelectedBgColor || hasSetSelectedStrokeColor) { GradientDrawable selectedShape = getItemShape(mShape, mCornerRadius, mSelectedBgColor, mStrokeWidth, mSelectedStrokeColor); selector.addState(new int[]{android.R.attr.state_selected}, selectedShape); } //focused = true if (hasSetFocusedBgColor || hasSetFocusedStrokeColor) { GradientDrawable focusedShape = getItemShape(mShape, mCornerRadius, mFocusedBgColor, mStrokeWidth, mFocusedStrokeColor); selector.addState(new int[]{android.R.attr.state_focused}, focusedShape); } //checked = true if (hasSetCheckedBgColor || hasSetCheckedStrokeColor) { GradientDrawable checkedShape = getItemShape(mShape, mCornerRadius, mCheckedBgColor, mStrokeWidth, mCheckedStrokeColor); selector.addState(new int[]{android.R.attr.state_checked}, checkedShape); } //default GradientDrawable defaultShape = getItemShape(mShape, mCornerRadius, mDefaultBgColor, mStrokeWidth, mDefaultStrokeColor); selector.addState(new int[]{}, defaultShape); return selector; } private GradientDrawable getItemShape(int shape, int cornerRadius, int solidColor, int strokeWidth, int strokeColor) { GradientDrawable drawable = new GradientDrawable(); drawable.setShape(shape); drawable.setStroke(strokeWidth, strokeColor); drawable.setCornerRadius(cornerRadius); drawable.setColor(solidColor); return drawable; } } /** * 资源DrawableSelector * * @author hjy * created at 2017/12/11 22:34 */ public static final class DrawableSelector { private Drawable mDefaultDrawable; private Drawable mDisabledDrawable; private Drawable mPressedDrawable; private Drawable mSelectedDrawable; private Drawable mFocusedDrawable; private boolean hasSetDisabledDrawable = false; private boolean hasSetPressedDrawable = false; private boolean hasSetSelectedDrawable = false; private boolean hasSetFocusedDrawable = false; private DrawableSelector() { mDefaultDrawable = new ColorDrawable(Color.TRANSPARENT); } public DrawableSelector setDefaultDrawable(Drawable drawable) { mDefaultDrawable = drawable; if (!hasSetDisabledDrawable) mDisabledDrawable = drawable; if (!hasSetPressedDrawable) mPressedDrawable = drawable; if (!hasSetSelectedDrawable) mSelectedDrawable = drawable; if (!hasSetFocusedDrawable) mFocusedDrawable = drawable; return this; } public DrawableSelector setDisabledDrawable(Drawable drawable) { mDisabledDrawable = drawable; hasSetDisabledDrawable = true; return this; } public DrawableSelector setPressedDrawable(Drawable drawable) { mPressedDrawable = drawable; hasSetPressedDrawable = true; return this; } public DrawableSelector setSelectedDrawable(Drawable drawable) { mSelectedDrawable = drawable; hasSetSelectedDrawable = true; return this; } public DrawableSelector setFocusedDrawable(Drawable drawable) { mFocusedDrawable = drawable; hasSetFocusedDrawable = true; return this; } public StateListDrawable create() { StateListDrawable selector = new StateListDrawable(); if (hasSetDisabledDrawable) selector.addState(new int[]{-android.R.attr.state_enabled}, mDisabledDrawable); if (hasSetPressedDrawable) selector.addState(new int[]{android.R.attr.state_pressed}, mPressedDrawable); if (hasSetSelectedDrawable) selector.addState(new int[]{android.R.attr.state_selected}, mSelectedDrawable); if (hasSetFocusedDrawable) selector.addState(new int[]{android.R.attr.state_focused}, mFocusedDrawable); selector.addState(new int[]{}, mDefaultDrawable); return selector; } public DrawableSelector setDefaultDrawable(Context context, @DrawableRes int drawableRes) { return setDefaultDrawable(ContextCompat.getDrawable(context, drawableRes)); } public DrawableSelector setDisabledDrawable(Context context, @DrawableRes int drawableRes) { return setDisabledDrawable(ContextCompat.getDrawable(context, drawableRes)); } public DrawableSelector setPressedDrawable(Context context, @DrawableRes int drawableRes) { return setPressedDrawable(ContextCompat.getDrawable(context, drawableRes)); } public DrawableSelector setSelectedDrawable(Context context, @DrawableRes int drawableRes) { return setSelectedDrawable(ContextCompat.getDrawable(context, drawableRes)); } public DrawableSelector setFocusedDrawable(Context context, @DrawableRes int drawableRes) { return setFocusedDrawable(ContextCompat.getDrawable(context, drawableRes)); } } /** * 颜色ColorSelector * * @author hjy * created at 2017/12/11 22:26 */ public static final class ColorSelector { private int mDefaultColor; private int mDisabledColor; private int mPressedColor; private int mSelectedColor; private int mFocusedColor; private int mCheckedColor; private boolean hasSetDisabledColor = false; private boolean hasSetPressedColor = false; private boolean hasSetSelectedColor = false; private boolean hasSetFocusedColor = false; private boolean hasSetCheckedColor = false; private ColorSelector() { mDefaultColor = Color.BLACK; mDisabledColor = Color.GRAY; mPressedColor = Color.BLACK; mSelectedColor = Color.BLACK; mFocusedColor = Color.BLACK; } public ColorSelector setDefaultColor(@ColorInt int color) { mDefaultColor = color; if (!hasSetDisabledColor) mDisabledColor = color; if (!hasSetPressedColor) mPressedColor = color; if (!hasSetSelectedColor) mSelectedColor = color; if (!hasSetFocusedColor) mFocusedColor = color; return this; } public ColorSelector setDisabledColor(@ColorInt int color) { mDisabledColor = color; hasSetDisabledColor = true; return this; } public ColorSelector setPressedColor(@ColorInt int color) { mPressedColor = color; hasSetPressedColor = true; return this; } public ColorSelector setSelectedColor(@ColorInt int color) { mSelectedColor = color; hasSetSelectedColor = true; return this; } public ColorSelector setFocusedColor(@ColorInt int color) { mFocusedColor = color; hasSetFocusedColor = true; return this; } public ColorSelector setCheckedColor(@ColorInt int color) { mCheckedColor = color; hasSetCheckedColor = true; return this; } public ColorStateList create() { int[] colors = new int[]{ hasSetDisabledColor ? mDisabledColor : mDefaultColor, hasSetPressedColor ? mPressedColor : mDefaultColor, hasSetSelectedColor ? mSelectedColor : mDefaultColor, hasSetFocusedColor ? mFocusedColor : mDefaultColor, hasSetCheckedColor ? mCheckedColor : mDefaultColor, mDefaultColor }; int[][] states = new int[6][]; states[0] = new int[]{-android.R.attr.state_enabled}; states[1] = new int[]{android.R.attr.state_pressed}; states[2] = new int[]{android.R.attr.state_selected}; states[3] = new int[]{android.R.attr.state_focused}; states[4] = new int[]{android.R.attr.state_checked}; states[5] = new int[]{}; return new ColorStateList(states, colors); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/SoftInputUtil.java ================================================ package com.kunfei.bookshelf.utils; import android.app.Activity; import android.content.Context; import android.graphics.Rect; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import com.kunfei.bookshelf.MApplication; public class SoftInputUtil { //隐藏输入法 public static void hideIMM(View view) { InputMethodManager imm = (InputMethodManager) MApplication.getInstance().getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null && view != null) { imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } } public static void resetBoxPosition(Activity activity, View prentView, int viewId) { final View decorView = (activity).getWindow().getDecorView(); decorView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { try { Rect rect = new Rect(); decorView.getWindowVisibleDisplayFrame(rect); int screenHeight = getScreenHeight(activity); int heightDifference = screenHeight - rect.bottom; //计算软键盘占有的高度 = 屏幕高度 - 视图可见高度 View view = prentView.findViewById(viewId); ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); layoutParams.bottomMargin = heightDifference; //设置rlContent的marginBottom的值为软键盘占有的高度即可 view.setLayoutParams(layoutParams); view.requestLayout(); } catch (Exception e) { e.printStackTrace(); } }); } public static int getScreenHeight(Activity activity) { WindowManager manager = (activity).getWindowManager(); DisplayMetrics outMetrics = new DisplayMetrics(); manager.getDefaultDisplay().getMetrics(outMetrics); return outMetrics.heightPixels; } public static int getScreenWidth(Activity activity) { WindowManager manager = (activity).getWindowManager(); DisplayMetrics outMetrics = new DisplayMetrics(); manager.getDefaultDisplay().getMetrics(outMetrics); return outMetrics.widthPixels; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/StringExtensions.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils import android.icu.text.Collator import android.icu.util.ULocale import android.net.Uri import java.io.File import java.util.* fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true fun String.parseToUri(): Uri { return if (isContentScheme()) { Uri.parse(this) } else { Uri.fromFile(File(this)) } } fun String?.isAbsUrl() = this?.let { it.startsWith("http://", true) || it.startsWith("https://", true) } ?: false fun String?.isJson(): Boolean = this?.run { val str = this.trim() when { str.startsWith("{") && str.endsWith("}") -> true str.startsWith("[") && str.endsWith("]") -> true else -> false } } ?: false fun String?.isJsonObject(): Boolean = this?.run { val str = this.trim() str.startsWith("{") && str.endsWith("}") } ?: false fun String?.isJsonArray(): Boolean = this?.run { val str = this.trim() str.startsWith("[") && str.endsWith("]") } ?: false fun String.splitNotBlank(vararg delimiter: String): Array = run { this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() } fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = run { this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() } fun String.cnCompare(other: String): Int { return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { Collator.getInstance(ULocale.SIMPLIFIED_CHINESE).compare(this, other) } else { java.text.Collator.getInstance(Locale.CHINA).compare(this, other) } } /** * 将字符串拆分为单个字符,包含emoji */ fun String.toStringArray(): Array { var codePointIndex = 0 return try { Array(codePointCount(0, length)) { val start = codePointIndex codePointIndex = offsetByCodePoints(start, 1) substring(start, codePointIndex) } } catch (e: Exception) { split("").toTypedArray() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/StringJoiner.java ================================================ package com.kunfei.bookshelf.utils; import androidx.annotation.NonNull; import java.util.Objects; public class StringJoiner { private String emptyValue; // 前缀 private final String prefix; // 分隔符 private final String delimiter; // 后缀 private final String suffix; // 值 private StringBuilder value; /** * 构造器 */ public StringJoiner(CharSequence delimiter) { this(delimiter, "", ""); } public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) { Objects.requireNonNull(prefix, "The prefix must not be null"); Objects.requireNonNull(delimiter, "The delimiter must not be null"); Objects.requireNonNull(suffix, "The suffix must not be null"); // make defensive copies of arguments this.prefix = prefix.toString(); this.delimiter = delimiter.toString(); this.suffix = suffix.toString(); this.emptyValue = this.prefix + this.suffix; } // 拼接 public StringJoiner add(CharSequence newElement) { prepareBuilder().append(newElement); return this; } // 预拼接value private StringBuilder prepareBuilder() { // value已加前缀 if (value != null) { // 此时添加分隔符 value.append(delimiter); } else { // value未加前缀时需要先添加前缀 value = new StringBuilder().append(prefix); } return value; } //重写了toString 方法 @NonNull @Override public String toString() { if (value == null) { // value未进行任何字符拼接时反悔emptyValue return emptyValue; } else { // 后缀为""字符时,直接返回value if (suffix.equals("")) { return value.toString(); } else { // 获取value未拼接后缀的长度 int initialLength = value.length(); String result = value.append(suffix).toString(); // reset value to pre-append initialLength // 此处是为了保证value.toString()为未拼接后缀前的字符串 value.setLength(initialLength); return result; } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/StringUtils.java ================================================ package com.kunfei.bookshelf.utils; import android.annotation.SuppressLint; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import androidx.annotation.StringRes; import com.kunfei.bookshelf.MApplication; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.Inflater; import static android.text.TextUtils.isEmpty; @SuppressWarnings({"unused", "WeakerAccess"}) public class StringUtils { private static final String TAG = "StringUtils"; private static final int HOUR_OF_DAY = 24; private static final int DAY_OF_YESTERDAY = 2; private static final int TIME_UNIT = 60; private final static HashMap ChnMap = getChnMap(); //将时间转换成日期 public static String dateConvert(long time, String pattern) { Date date = new Date(time); @SuppressLint("SimpleDateFormat") SimpleDateFormat format = new SimpleDateFormat(pattern); return format.format(date); } //将日期转换成昨天、今天、明天 public static String dateConvert(String source, String pattern) { @SuppressLint("SimpleDateFormat") DateFormat format = new SimpleDateFormat(pattern); Calendar calendar = Calendar.getInstance(); try { Date date = format.parse(source); long curTime = calendar.getTimeInMillis(); calendar.setTime(date); //将MISC 转换成 sec long difSec = Math.abs((curTime - date.getTime()) / 1000); long difMin = difSec / 60; long difHour = difMin / 60; long difDate = difHour / 60; int oldHour = calendar.get(Calendar.HOUR); //如果没有时间 if (oldHour == 0) { //比日期:昨天今天和明天 if (difDate == 0) { return "今天"; } else if (difDate < DAY_OF_YESTERDAY) { return "昨天"; } else { @SuppressLint("SimpleDateFormat") DateFormat convertFormat = new SimpleDateFormat("yyyy-MM-dd"); return convertFormat.format(date); } } if (difSec < TIME_UNIT) { return difSec + "秒前"; } else if (difMin < TIME_UNIT) { return difMin + "分钟前"; } else if (difHour < HOUR_OF_DAY) { return difHour + "小时前"; } else if (difDate < DAY_OF_YESTERDAY) { return "昨天"; } else { @SuppressLint("SimpleDateFormat") DateFormat convertFormat = new SimpleDateFormat("yyyy-MM-dd"); return convertFormat.format(date); } } catch (ParseException e) { e.printStackTrace(); } return ""; } public static String toFirstCapital(String str) { return str.substring(0, 1).toUpperCase() + str.substring(1); } public static String getString(@StringRes int id) { return MApplication.getInstance().getResources().getString(id); } public static String getString(@StringRes int id, Object... formatArgs) { return MApplication.getInstance().getString(id, formatArgs); } /** * 将文本中的半角字符,转换成全角字符 */ public static String halfToFull(String input) { char[] c = input.toCharArray(); for (int i = 0; i < c.length; i++) { if (c[i] == 32) //半角空格 { c[i] = (char) 12288; continue; } //根据实际情况,过滤不需要转换的符号 //if (c[i] == 46) //半角点号,不转换 // continue; if (c[i] > 32 && c[i] < 127) //其他符号都转换为全角 c[i] = (char) (c[i] + 65248); } return new String(c); } //功能:字符串全角转换为半角 public static String fullToHalf(String input) { char[] c = input.toCharArray(); for (int i = 0; i < c.length; i++) { if (c[i] == 12288) //全角空格 { c[i] = (char) 32; continue; } if (c[i] > 65280 && c[i] < 65375) c[i] = (char) (c[i] - 65248); } return new String(c); } private static HashMap getChnMap() { HashMap map = new HashMap<>(); String cnStr = "零一二三四五六七八九十"; char[] c = cnStr.toCharArray(); for (int i = 0; i <= 10; i++) { map.put(c[i], i); } cnStr = "〇壹贰叁肆伍陆柒捌玖拾"; c = cnStr.toCharArray(); for (int i = 0; i <= 10; i++) { map.put(c[i], i); } map.put('两', 2); map.put('百', 100); map.put('佰', 100); map.put('千', 1000); map.put('仟', 1000); map.put('万', 10000); map.put('亿', 100000000); return map; } @SuppressWarnings("ConstantConditions") public static int chineseNumToInt(String chNum) { int result = 0; int tmp = 0; int billion = 0; char[] cn = chNum.toCharArray(); // "一零二五" 形式 if (cn.length > 1 && chNum.matches("^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$")) { for (int i = 0; i < cn.length; i++) { cn[i] = (char) (48 + ChnMap.get(cn[i])); } return Integer.parseInt(new String(cn)); } // "一千零二十五", "一千二" 形式 try { for (int i = 0; i < cn.length; i++) { int tmpNum = ChnMap.get(cn[i]); if (tmpNum == 100000000) { result += tmp; result *= tmpNum; billion = billion * 100000000 + result; result = 0; tmp = 0; } else if (tmpNum == 10000) { result += tmp; result *= tmpNum; tmp = 0; } else if (tmpNum >= 10) { if (tmp == 0) tmp = 1; result += tmpNum * tmp; tmp = 0; } else { if (i >= 2 && i == cn.length - 1 && ChnMap.get(cn[i - 1]) > 10) tmp = tmpNum * ChnMap.get(cn[i - 1]) / 10; else tmp = tmp * 10 + tmpNum; } } result += tmp + billion; return result; } catch (Exception e) { return -1; } } public static int stringToInt(String str) { if (str != null) { String num = fullToHalf(str).replaceAll("\\s", ""); try { return Integer.parseInt(num); } catch (Exception e) { return chineseNumToInt(num); } } return -1; } public static String base64Decode(String str) { byte[] bytes = Base64.decode(str, Base64.DEFAULT); try { return new String(bytes, StandardCharsets.UTF_8); } catch (Exception e) { return new String(bytes); } } public static String escape(String src) { int i; char j; StringBuilder tmp = new StringBuilder(); tmp.ensureCapacity(src.length() * 6); for (i = 0; i < src.length(); i++) { j = src.charAt(i); if (Character.isDigit(j) || Character.isLowerCase(j) || Character.isUpperCase(j)) tmp.append(j); else if (j < 256) { tmp.append("%"); if (j < 16) tmp.append("0"); tmp.append(Integer.toString(j, 16)); } else { tmp.append("%u"); tmp.append(Integer.toString(j, 16)); } } return tmp.toString(); } public static boolean isJsonType(String str) { boolean result = false; if (!TextUtils.isEmpty(str)) { str = str.trim(); if (str.startsWith("{") && str.endsWith("}")) { result = true; } else if (str.startsWith("[") && str.endsWith("]")) { result = true; } } return result; } public static boolean isCompressJsonType(String str) { if (!TextUtils.isEmpty(str)) { if (str.replaceAll("(\\s|\n)*", "").matches("^\\{.*[^}]$")) { return true; } } return false; } public static String unCompressJson(String str) { if (TextUtils.isEmpty(str)) return ""; // 如果是未压缩的json if (str.replaceAll("(\\s|\n)*", "").matches("^\\{.*\\}$")) return str; // if (str.replaceAll("(\\s|\n)*","").matches("^\\{.*[^}]$")) String string = null; str = str.trim(); try { if (str.charAt(0) == '{') string = unzipString(str.substring(1)); else string = unzipString(str); if (string.charAt(string.length() - 1) == '}') return "{" + string; } catch (Exception e) { e.printStackTrace(); } return str; } public static boolean isJsonObject(String text) { boolean result = false; if (!TextUtils.isEmpty(text)) { text = text.trim(); if (text.startsWith("{") && text.endsWith("}")) { result = true; } } return result; } public static boolean isJsonArray(String text) { boolean result = false; if (!TextUtils.isEmpty(text)) { text = text.trim(); if (text.startsWith("[") && text.endsWith("]")) { result = true; } } return result; } public static boolean isTrimEmpty(String text) { if (text == null) return true; if (text.length() == 0) return true; return text.trim().length() == 0; } public static boolean startWithIgnoreCase(String src, String obj) { if (src == null || obj == null) return false; if (obj.length() > src.length()) return false; return src.substring(0, obj.length()).equalsIgnoreCase(obj); } public static boolean endWithIgnoreCase(String src, String obj) { if (src == null || obj == null) return false; if (obj.length() > src.length()) return false; return src.substring(src.length() - obj.length()).equalsIgnoreCase(obj); } public static boolean isContainNumber(String company) { Pattern p = Pattern.compile("[0-9]"); Matcher m = p.matcher(company); return m.find(); } public static boolean isNumeric(String str) { Pattern pattern = Pattern.compile("[0-9]*"); Matcher isNum = pattern.matcher(str); return isNum.matches(); } public static String getBaseUrl(String url) { if (url == null || !url.startsWith("http")) return null; int index = url.indexOf("/", 9); if (index == -1) { return url; } return url.substring(0, index); } // 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格) public static String trim(String s) { if (isEmpty(s)) return ""; int start = 0, len = s.length(); int end = len - 1; while ((start < end) && ((s.charAt(start) <= 0x20) || (s.charAt(start) == ' '))) { ++start; } while ((start < end) && ((s.charAt(end) <= 0x20) || (s.charAt(end) == ' '))) { --end; } if (end < len) ++end; return ((start > 0) || (end < len)) ? s.substring(start, end) : s; } public static String repeat(String str, int n) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < n; i++) { stringBuilder.append(str); } return stringBuilder.toString(); } public static String removeUTFCharacters(String data) { if (data == null) return null; Pattern p = Pattern.compile("\\\\u(\\p{XDigit}{4})"); Matcher m = p.matcher(data); StringBuffer buf = new StringBuffer(data.length()); while (m.find()) { String ch = String.valueOf((char) Integer.parseInt(m.group(1), 16)); m.appendReplacement(buf, Matcher.quoteReplacement(ch)); } m.appendTail(buf); return buf.toString(); } public static String formatHtml(String html) { if (TextUtils.isEmpty(html)) return ""; return html.replaceAll("(?i)<(br[\\s/]*|/?p[^>]*|/?div[^>]*)>", "\n")// 替换特定标签为换行符 //.replaceAll("<(script[^>]*>)?[^>]*>| ", "")// 删除script标签对和空格转义符 .replaceAll("]*>", "")// 删除标签对 .replaceAll("\\s*\\n+\\s*", "\n  ")// 移除空行,并增加段前缩进2个汉字 .replaceAll("^[\\n\\s]+", "  ")//移除开头空行,并增加段前缩进2个汉字 .replaceAll("[\\n\\s]+$", "");//移除尾部空行 } public static String formatHtml2Intor(String html) { if (TextUtils.isEmpty(html)) return ""; return "  " + html.replaceAll("(?i)<(br[\\s/]*|/?p[^>]*|/?div[^>]*)>", "\n")// 替换特定标签为换行符 .replaceAll("]*>", "")// 删除标签对 .replaceAll("\\s*\\n+\\s*", "\n  ")// 移除空行,并增加段前缩进2个汉字 .trim(); } /** * 压缩 */ public static String zipString(String unzipString) { /* * https://www.yiibai.com/javazip/javazip_deflater.html#article-start * 0 ~ 9 压缩等级 低到高 * public static final int BEST_COMPRESSION = 9; 最佳压缩的压缩级别。 * public static final int BEST_SPEED = 1; 压缩级别最快的压缩。 * public static final int DEFAULT_COMPRESSION = -1; 默认压缩级别。 * public static final int DEFAULT_STRATEGY = 0; 默认压缩策略。 * public static final int DEFLATED = 8; 压缩算法的压缩方法(目前唯一支持的压缩方法)。 * public static final int FILTERED = 1; 压缩策略最适用于大部分数值较小且数据分布随机分布的数据。 * public static final int FULL_FLUSH = 3; 压缩刷新模式,用于清除所有待处理的输出并重置拆卸器。 * public static final int HUFFMAN_ONLY = 2; 仅用于霍夫曼编码的压缩策略。 * public static final int NO_COMPRESSION = 0; 不压缩的压缩级别。 * public static final int NO_FLUSH = 0; 用于实现最佳压缩结果的压缩刷新模式。 * public static final int SYNC_FLUSH = 2; 用于清除所有未决输出的压缩刷新模式; 可能会降低某些压缩算法的压缩率。 */ try { //使用指定的压缩级别创建一个新的压缩器。 Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); //设置压缩输入数据。 deflater.setInput(unzipString.getBytes(StandardCharsets.UTF_8)); //当被调用时,表示压缩应该以输入缓冲区的当前内容结束。 deflater.finish(); final byte[] bytes = new byte[512]; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512); while (!deflater.finished()) { //压缩输入数据并用压缩数据填充指定的缓冲区。 int length = deflater.deflate(bytes); outputStream.write(bytes, 0, length); } //关闭压缩器并丢弃任何未处理的输入。 deflater.end(); String zipString = new String(Base64.encode(outputStream.toByteArray(), Base64.DEFAULT), StandardCharsets.UTF_8); Log.d("zipString()压缩比", "char:" + zipString.length() + "/" + unzipString.length() + "=" + zipString.length() /(float)(unzipString.length()) + "\tbyte:" + zipString.getBytes("utf-8").length + "/" + unzipString.getBytes("utf-8").length + "=" + zipString.getBytes("UTF-8").length /(float) unzipString.getBytes("utf-8").length); return zipString.trim(); } catch (Exception e) { e.printStackTrace(); } return ""; //处理回车符 // return zipString.replaceAll("[\r\n]", ""); } /** * 解压缩 */ public static String unzipString(String zipString) { byte[] decode //= Base64.decodeBase64(zipString); = Base64.decode(zipString, Base64.DEFAULT); //创建一个新的解压缩器 https://www.yiibai.com/javazip/javazip_inflater.html Inflater inflater = new Inflater(); //设置解压缩的输入数据。 inflater.setInput(decode); final byte[] bytes = new byte[512]; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512); try { //finished() 如果已到达压缩数据流的末尾,则返回true。 while (!inflater.finished()) { //将字节解压缩到指定的缓冲区中。 int length = inflater.inflate(bytes); outputStream.write(bytes, 0, length); } } catch (DataFormatException e) { e.printStackTrace(); return null; } finally { //关闭解压缩器并丢弃任何未处理的输入。 inflater.end(); } try { return outputStream.toString("UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/SystemUtil.java ================================================ package com.kunfei.bookshelf.utils; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.PowerManager; import android.provider.Settings; import static android.content.Context.POWER_SERVICE; public class SystemUtil { public static int getScreenOffTime(Context context) { int screenOffTime = 0; try { screenOffTime = Settings.System.getInt(context.getContentResolver(), Settings.System.SCREEN_OFF_TIMEOUT); } catch (Exception e) { e.printStackTrace(); } return screenOffTime; } public static void ignoreBatteryOptimization(Activity activity) { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return; PowerManager powerManager = (PowerManager) activity.getSystemService(POWER_SERVICE); boolean hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName()); // 判断当前APP是否有加入电池优化的白名单,如果没有,弹出加入电池优化的白名单的设置对话框。 if (!hasIgnored) { try { @SuppressLint("BatteryLife") Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:" + activity.getPackageName())); activity.startActivity(intent); } catch (Throwable ignored) { } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/TimeUtils.java ================================================ package com.kunfei.bookshelf.utils; import androidx.annotation.NonNull; import com.kunfei.bookshelf.constant.TimeConstants; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; @SuppressWarnings({"unused", "WeakerAccess"}) public final class TimeUtils { private static final ThreadLocal SDF_THREAD_LOCAL = new ThreadLocal<>(); private static SimpleDateFormat getDefaultFormat() { SimpleDateFormat simpleDateFormat = SDF_THREAD_LOCAL.get(); if (simpleDateFormat == null) { simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); SDF_THREAD_LOCAL.set(simpleDateFormat); } return simpleDateFormat; } private TimeUtils() { throw new UnsupportedOperationException("u can't instantiate me..."); } /** * Milliseconds to the formatted time string. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param millis The milliseconds. * @return the formatted time string */ public static String millis2String(final long millis) { return millis2String(millis, getDefaultFormat()); } /** * Milliseconds to the formatted time string. * * @param millis The milliseconds. * @param format The format. * @return the formatted time string */ public static String millis2String(final long millis, @NonNull final DateFormat format) { return format.format(new Date(millis)); } /** * Formatted time string to the milliseconds. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return the milliseconds */ public static long string2Millis(final String time) { return string2Millis(time, getDefaultFormat()); } /** * Formatted time string to the milliseconds. * * @param time The formatted time string. * @param format The format. * @return the milliseconds */ public static long string2Millis(final String time, @NonNull final DateFormat format) { try { return format.parse(time).getTime(); } catch (ParseException e) { e.printStackTrace(); } return -1; } /** * Formatted time string to the date. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return the date */ public static Date string2Date(final String time) { return string2Date(time, getDefaultFormat()); } /** * Formatted time string to the date. * * @param time The formatted time string. * @param format The format. * @return the date */ public static Date string2Date(final String time, @NonNull final DateFormat format) { try { return format.parse(time); } catch (ParseException e) { e.printStackTrace(); } return null; } /** * Date to the formatted time string. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param date The date. * @return the formatted time string */ public static String date2String(final Date date) { return date2String(date, getDefaultFormat()); } /** * Date to the formatted time string. * * @param date The date. * @param format The format. * @return the formatted time string */ public static String date2String(final Date date, @NonNull final DateFormat format) { return format.format(date); } /** * Date to the milliseconds. * * @param date The date. * @return the milliseconds */ public static long date2Millis(final Date date) { return date.getTime(); } /** * Milliseconds to the date. * * @param millis The milliseconds. * @return the date */ public static Date millis2Date(final long millis) { return new Date(millis); } /** * Return the time span, in unit. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time1 The first formatted time string. * @param time2 The second formatted time string. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span, in unit */ public static long getTimeSpan(final String time1, final String time2, @TimeConstants.Unit final int unit) { return getTimeSpan(time1, time2, getDefaultFormat(), unit); } /** * Return the time span, in unit. * * @param time1 The first formatted time string. * @param time2 The second formatted time string. * @param format The format. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span, in unit */ public static long getTimeSpan(final String time1, final String time2, @NonNull final DateFormat format, @TimeConstants.Unit final int unit) { return millis2TimeSpan(string2Millis(time1, format) - string2Millis(time2, format), unit); } /** * Return the time span, in unit. * * @param date1 The first date. * @param date2 The second date. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span, in unit */ public static long getTimeSpan(final Date date1, final Date date2, @TimeConstants.Unit final int unit) { return millis2TimeSpan(date2Millis(date1) - date2Millis(date2), unit); } /** * Return the time span, in unit. * * @param millis1 The first milliseconds. * @param millis2 The second milliseconds. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span, in unit */ public static long getTimeSpan(final long millis1, final long millis2, @TimeConstants.Unit final int unit) { return millis2TimeSpan(millis1 - millis2, unit); } /** * Return the fit time span. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time1 The first formatted time string. * @param time2 The second formatted time string. * @param precision The precision of time span. *
      *
    • precision = 0, return null
    • *
    • precision = 1, return 天
    • *
    • precision = 2, return 天, 小时
    • *
    • precision = 3, return 天, 小时, 分钟
    • *
    • precision = 4, return 天, 小时, 分钟, 秒
    • *
    • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
    • *
    * @return the fit time span */ public static String getFitTimeSpan(final String time1, final String time2, final int precision) { long delta = string2Millis(time1, getDefaultFormat()) - string2Millis(time2, getDefaultFormat()); return millis2FitTimeSpan(delta, precision); } /** * Return the fit time span. * * @param time1 The first formatted time string. * @param time2 The second formatted time string. * @param format The format. * @param precision The precision of time span. *
      *
    • precision = 0, return null
    • *
    • precision = 1, return 天
    • *
    • precision = 2, return 天, 小时
    • *
    • precision = 3, return 天, 小时, 分钟
    • *
    • precision = 4, return 天, 小时, 分钟, 秒
    • *
    • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
    • *
    * @return the fit time span */ public static String getFitTimeSpan(final String time1, final String time2, @NonNull final DateFormat format, final int precision) { long delta = string2Millis(time1, format) - string2Millis(time2, format); return millis2FitTimeSpan(delta, precision); } /** * Return the fit time span. * * @param date1 The first date. * @param date2 The second date. * @param precision The precision of time span. *
      *
    • precision = 0, return null
    • *
    • precision = 1, return 天
    • *
    • precision = 2, return 天, 小时
    • *
    • precision = 3, return 天, 小时, 分钟
    • *
    • precision = 4, return 天, 小时, 分钟, 秒
    • *
    • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
    • *
    * @return the fit time span */ public static String getFitTimeSpan(final Date date1, final Date date2, final int precision) { return millis2FitTimeSpan(date2Millis(date1) - date2Millis(date2), precision); } /** * Return the fit time span. * * @param millis1 The first milliseconds. * @param millis2 The second milliseconds. * @param precision The precision of time span. *
      *
    • precision = 0, return null
    • *
    • precision = 1, return 天
    • *
    • precision = 2, return 天, 小时
    • *
    • precision = 3, return 天, 小时, 分钟
    • *
    • precision = 4, return 天, 小时, 分钟, 秒
    • *
    • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
    • *
    * @return the fit time span */ public static String getFitTimeSpan(final long millis1, final long millis2, final int precision) { return millis2FitTimeSpan(millis1 - millis2, precision); } /** * Return the current time in milliseconds. * * @return the current time in milliseconds */ public static long getNowMills() { return System.currentTimeMillis(); } /** * Return the current formatted time string. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @return the current formatted time string */ public static String getNowString() { return millis2String(System.currentTimeMillis(), getDefaultFormat()); } /** * Return the current formatted time string. * * @param format The format. * @return the current formatted time string */ public static String getNowString(@NonNull final DateFormat format) { return millis2String(System.currentTimeMillis(), format); } /** * Return the current date. * * @return the current date */ public static Date getNowDate() { return new Date(); } /** * Return the time span by now, in unit. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span by now, in unit */ public static long getTimeSpanByNow(final String time, @TimeConstants.Unit final int unit) { return getTimeSpan(time, getNowString(), getDefaultFormat(), unit); } /** * Return the time span by now, in unit. * * @param time The formatted time string. * @param format The format. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span by now, in unit */ public static long getTimeSpanByNow(final String time, @NonNull final DateFormat format, @TimeConstants.Unit final int unit) { return getTimeSpan(time, getNowString(format), format, unit); } /** * Return the time span by now, in unit. * * @param date The date. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span by now, in unit */ public static long getTimeSpanByNow(final Date date, @TimeConstants.Unit final int unit) { return getTimeSpan(date, new Date(), unit); } /** * Return the time span by now, in unit. * * @param millis The milliseconds. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the time span by now, in unit */ public static long getTimeSpanByNow(final long millis, @TimeConstants.Unit final int unit) { return getTimeSpan(millis, System.currentTimeMillis(), unit); } /** * Return the fit time span by now. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @param precision The precision of time span. *
      *
    • precision = 0,返回 null
    • *
    • precision = 1,返回天
    • *
    • precision = 2,返回天和小时
    • *
    • precision = 3,返回天、小时和分钟
    • *
    • precision = 4,返回天、小时、分钟和秒
    • *
    • precision >= 5,返回天、小时、分钟、秒和毫秒
    • *
    * @return the fit time span by now */ public static String getFitTimeSpanByNow(final String time, final int precision) { return getFitTimeSpan(time, getNowString(), getDefaultFormat(), precision); } /** * Return the fit time span by now. * * @param time The formatted time string. * @param format The format. * @param precision The precision of time span. *
      *
    • precision = 0,返回 null
    • *
    • precision = 1,返回天
    • *
    • precision = 2,返回天和小时
    • *
    • precision = 3,返回天、小时和分钟
    • *
    • precision = 4,返回天、小时、分钟和秒
    • *
    • precision >= 5,返回天、小时、分钟、秒和毫秒
    • *
    * @return the fit time span by now */ public static String getFitTimeSpanByNow(final String time, @NonNull final DateFormat format, final int precision) { return getFitTimeSpan(time, getNowString(format), format, precision); } /** * Return the fit time span by now. * * @param date The date. * @param precision The precision of time span. *
      *
    • precision = 0,返回 null
    • *
    • precision = 1,返回天
    • *
    • precision = 2,返回天和小时
    • *
    • precision = 3,返回天、小时和分钟
    • *
    • precision = 4,返回天、小时、分钟和秒
    • *
    • precision >= 5,返回天、小时、分钟、秒和毫秒
    • *
    * @return the fit time span by now */ public static String getFitTimeSpanByNow(final Date date, final int precision) { return getFitTimeSpan(date, getNowDate(), precision); } /** * Return the fit time span by now. * * @param millis The milliseconds. * @param precision The precision of time span. *
      *
    • precision = 0,返回 null
    • *
    • precision = 1,返回天
    • *
    • precision = 2,返回天和小时
    • *
    • precision = 3,返回天、小时和分钟
    • *
    • precision = 4,返回天、小时、分钟和秒
    • *
    • precision >= 5,返回天、小时、分钟、秒和毫秒
    • *
    * @return the fit time span by now */ public static String getFitTimeSpanByNow(final long millis, final int precision) { return getFitTimeSpan(millis, System.currentTimeMillis(), precision); } /** * Return the friendly time span by now. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return the friendly time span by now *
      *
    • 如果小于 1 秒钟内,显示刚刚
    • *
    • 如果在 1 分钟内,显示 XXX秒前
    • *
    • 如果在 1 小时内,显示 XXX分钟前
    • *
    • 如果在 1 小时外的今天内,显示今天15:32
    • *
    • 如果是昨天的,显示昨天15:32
    • *
    • 其余显示,2016-10-15
    • *
    • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
    • *
    */ public static String getFriendlyTimeSpanByNow(final String time) { return getFriendlyTimeSpanByNow(time, getDefaultFormat()); } /** * Return the friendly time span by now. * * @param time The formatted time string. * @param format The format. * @return the friendly time span by now *
      *
    • 如果小于 1 秒钟内,显示刚刚
    • *
    • 如果在 1 分钟内,显示 XXX秒前
    • *
    • 如果在 1 小时内,显示 XXX分钟前
    • *
    • 如果在 1 小时外的今天内,显示今天15:32
    • *
    • 如果是昨天的,显示昨天15:32
    • *
    • 其余显示,2016-10-15
    • *
    • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
    • *
    */ public static String getFriendlyTimeSpanByNow(final String time, @NonNull final DateFormat format) { return getFriendlyTimeSpanByNow(string2Millis(time, format)); } /** * Return the friendly time span by now. * * @param date The date. * @return the friendly time span by now *
      *
    • 如果小于 1 秒钟内,显示刚刚
    • *
    • 如果在 1 分钟内,显示 XXX秒前
    • *
    • 如果在 1 小时内,显示 XXX分钟前
    • *
    • 如果在 1 小时外的今天内,显示今天15:32
    • *
    • 如果是昨天的,显示昨天15:32
    • *
    • 其余显示,2016-10-15
    • *
    • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
    • *
    */ public static String getFriendlyTimeSpanByNow(final Date date) { return getFriendlyTimeSpanByNow(date.getTime()); } /** * Return the friendly time span by now. * * @param millis The milliseconds. * @return the friendly time span by now *
      *
    • 如果小于 1 秒钟内,显示刚刚
    • *
    • 如果在 1 分钟内,显示 XXX秒前
    • *
    • 如果在 1 小时内,显示 XXX分钟前
    • *
    • 如果在 1 小时外的今天内,显示今天15:32
    • *
    • 如果是昨天的,显示昨天15:32
    • *
    • 其余显示,2016-10-15
    • *
    • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
    • *
    */ public static String getFriendlyTimeSpanByNow(final long millis) { long now = System.currentTimeMillis(); long span = now - millis; if (span < 0) // U can read http://www.apihome.cn/api/java/Formatter.html to understand it. return String.format("%tc", millis); if (span < 1000) { return "刚刚"; } else if (span < TimeConstants.MIN) { return String.format(Locale.getDefault(), "%d秒前", span / TimeConstants.SEC); } else if (span < TimeConstants.HOUR) { return String.format(Locale.getDefault(), "%d分钟前", span / TimeConstants.MIN); } // 获取当天 00:00 long wee = getWeeOfToday(); if (millis >= wee) { return String.format("今天%tR", millis); } else if (millis >= wee - TimeConstants.DAY) { return String.format("昨天%tR", millis); } else { return String.format("%tF", millis); } } private static long getWeeOfToday() { Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.MILLISECOND, 0); return cal.getTimeInMillis(); } /** * Return the milliseconds differ time span. * * @param millis The milliseconds. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the milliseconds differ time span */ public static long getMillis(final long millis, final long timeSpan, @TimeConstants.Unit final int unit) { return millis + timeSpan2Millis(timeSpan, unit); } /** * Return the milliseconds differ time span. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the milliseconds differ time span */ public static long getMillis(final String time, final long timeSpan, @TimeConstants.Unit final int unit) { return getMillis(time, getDefaultFormat(), timeSpan, unit); } /** * Return the milliseconds differ time span. * * @param time The formatted time string. * @param format The format. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the milliseconds differ time span. */ public static long getMillis(final String time, @NonNull final DateFormat format, final long timeSpan, @TimeConstants.Unit final int unit) { return string2Millis(time, format) + timeSpan2Millis(timeSpan, unit); } /** * Return the milliseconds differ time span. * * @param date The date. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the milliseconds differ time span. */ public static long getMillis(final Date date, final long timeSpan, @TimeConstants.Unit final int unit) { return date2Millis(date) + timeSpan2Millis(timeSpan, unit); } /** * Return the formatted time string differ time span. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param millis The milliseconds. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span */ public static String getString(final long millis, final long timeSpan, @TimeConstants.Unit final int unit) { return getString(millis, getDefaultFormat(), timeSpan, unit); } /** * Return the formatted time string differ time span. * * @param millis The milliseconds. * @param format The format. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span */ public static String getString(final long millis, @NonNull final DateFormat format, final long timeSpan, @TimeConstants.Unit final int unit) { return millis2String(millis + timeSpan2Millis(timeSpan, unit), format); } /** * Return the formatted time string differ time span. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span */ public static String getString(final String time, final long timeSpan, @TimeConstants.Unit final int unit) { return getString(time, getDefaultFormat(), timeSpan, unit); } /** * Return the formatted time string differ time span. * * @param time The formatted time string. * @param format The format. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span */ public static String getString(final String time, @NonNull final DateFormat format, final long timeSpan, @TimeConstants.Unit final int unit) { return millis2String(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit), format); } /** * Return the formatted time string differ time span. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param date The date. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span */ public static String getString(final Date date, final long timeSpan, @TimeConstants.Unit final int unit) { return getString(date, getDefaultFormat(), timeSpan, unit); } /** * Return the formatted time string differ time span. * * @param date The date. * @param format The format. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span */ public static String getString(final Date date, @NonNull final DateFormat format, final long timeSpan, @TimeConstants.Unit final int unit) { return millis2String(date2Millis(date) + timeSpan2Millis(timeSpan, unit), format); } /** * Return the date differ time span. * * @param millis The milliseconds. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the date differ time span */ public static Date getDate(final long millis, final long timeSpan, @TimeConstants.Unit final int unit) { return millis2Date(millis + timeSpan2Millis(timeSpan, unit)); } /** * Return the date differ time span. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the date differ time span */ public static Date getDate(final String time, final long timeSpan, @TimeConstants.Unit final int unit) { return getDate(time, getDefaultFormat(), timeSpan, unit); } /** * Return the date differ time span. * * @param time The formatted time string. * @param format The format. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the date differ time span */ public static Date getDate(final String time, @NonNull final DateFormat format, final long timeSpan, @TimeConstants.Unit final int unit) { return millis2Date(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit)); } /** * Return the date differ time span. * * @param date The date. * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the date differ time span */ public static Date getDate(final Date date, final long timeSpan, @TimeConstants.Unit final int unit) { return millis2Date(date2Millis(date) + timeSpan2Millis(timeSpan, unit)); } /** * Return the milliseconds differ time span by now. * * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the milliseconds differ time span by now */ public static long getMillisByNow(final long timeSpan, @TimeConstants.Unit final int unit) { return getMillis(getNowMills(), timeSpan, unit); } /** * Return the formatted time string differ time span by now. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span by now */ public static String getStringByNow(final long timeSpan, @TimeConstants.Unit final int unit) { return getStringByNow(timeSpan, getDefaultFormat(), unit); } /** * Return the formatted time string differ time span by now. * * @param timeSpan The time span. * @param format The format. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the formatted time string differ time span by now */ public static String getStringByNow(final long timeSpan, @NonNull final DateFormat format, @TimeConstants.Unit final int unit) { return getString(getNowMills(), format, timeSpan, unit); } /** * Return the date differ time span by now. * * @param timeSpan The time span. * @param unit The unit of time span. *
      *
    • {@link TimeConstants#MSEC}
    • *
    • {@link TimeConstants#SEC }
    • *
    • {@link TimeConstants#MIN }
    • *
    • {@link TimeConstants#HOUR}
    • *
    • {@link TimeConstants#DAY }
    • *
    * @return the date differ time span by now */ public static Date getDateByNow(final long timeSpan, @TimeConstants.Unit final int unit) { return getDate(getNowMills(), timeSpan, unit); } /** * Return whether it is today. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return {@code true}: yes
    {@code false}: no */ public static boolean isToday(final String time) { return isToday(string2Millis(time, getDefaultFormat())); } /** * Return whether it is today. * * @param time The formatted time string. * @param format The format. * @return {@code true}: yes
    {@code false}: no */ public static boolean isToday(final String time, @NonNull final DateFormat format) { return isToday(string2Millis(time, format)); } /** * Return whether it is today. * * @param date The date. * @return {@code true}: yes
    {@code false}: no */ public static boolean isToday(final Date date) { return isToday(date.getTime()); } /** * Return whether it is today. * * @param millis The milliseconds. * @return {@code true}: yes
    {@code false}: no */ public static boolean isToday(final long millis) { long wee = getWeeOfToday(); return millis >= wee && millis < wee + TimeConstants.DAY; } /** * Return whether it is leap year. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return {@code true}: yes
    {@code false}: no */ public static boolean isLeapYear(final String time) { return isLeapYear(string2Date(time, getDefaultFormat())); } /** * Return whether it is leap year. * * @param time The formatted time string. * @param format The format. * @return {@code true}: yes
    {@code false}: no */ public static boolean isLeapYear(final String time, @NonNull final DateFormat format) { return isLeapYear(string2Date(time, format)); } /** * Return whether it is leap year. * * @param date The date. * @return {@code true}: yes
    {@code false}: no */ public static boolean isLeapYear(final Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); int year = cal.get(Calendar.YEAR); return isLeapYear(year); } /** * Return whether it is leap year. * * @param millis The milliseconds. * @return {@code true}: yes
    {@code false}: no */ public static boolean isLeapYear(final long millis) { return isLeapYear(millis2Date(millis)); } /** * Return whether it is leap year. * * @param year The year. * @return {@code true}: yes
    {@code false}: no */ public static boolean isLeapYear(final int year) { return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; } /** * Return the day of week in Chinese. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return the day of week in Chinese */ public static String getChineseWeek(final String time) { return getChineseWeek(string2Date(time, getDefaultFormat())); } /** * Return the day of week in Chinese. * * @param time The formatted time string. * @param format The format. * @return the day of week in Chinese */ public static String getChineseWeek(final String time, @NonNull final DateFormat format) { return getChineseWeek(string2Date(time, format)); } /** * Return the day of week in Chinese. * * @param date The date. * @return the day of week in Chinese */ public static String getChineseWeek(final Date date) { return new SimpleDateFormat("E", Locale.CHINA).format(date); } /** * Return the day of week in Chinese. * * @param millis The milliseconds. * @return the day of week in Chinese */ public static String getChineseWeek(final long millis) { return getChineseWeek(new Date(millis)); } /** * Return the day of week in US. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return the day of week in US */ public static String getUSWeek(final String time) { return getUSWeek(string2Date(time, getDefaultFormat())); } /** * Return the day of week in US. * * @param time The formatted time string. * @param format The format. * @return the day of week in US */ public static String getUSWeek(final String time, @NonNull final DateFormat format) { return getUSWeek(string2Date(time, format)); } /** * Return the day of week in US. * * @param date The date. * @return the day of week in US */ public static String getUSWeek(final Date date) { return new SimpleDateFormat("EEEE", Locale.US).format(date); } /** * Return the day of week in US. * * @param millis The milliseconds. * @return the day of week in US */ public static String getUSWeek(final long millis) { return getUSWeek(new Date(millis)); } /** * Returns the value of the given calendar field. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @param field The given calendar field. *
      *
    • {@link Calendar#ERA}
    • *
    • {@link Calendar#YEAR}
    • *
    • {@link Calendar#MONTH}
    • *
    • ...
    • *
    • {@link Calendar#DST_OFFSET}
    • *
    * @return the value of the given calendar field */ public static int getValueByCalendarField(final String time, final int field) { return getValueByCalendarField(string2Date(time, getDefaultFormat()), field); } /** * Returns the value of the given calendar field. * * @param time The formatted time string. * @param format The format. * @param field The given calendar field. *
      *
    • {@link Calendar#ERA}
    • *
    • {@link Calendar#YEAR}
    • *
    • {@link Calendar#MONTH}
    • *
    • ...
    • *
    • {@link Calendar#DST_OFFSET}
    • *
    * @return the value of the given calendar field */ public static int getValueByCalendarField(final String time, @NonNull final DateFormat format, final int field) { return getValueByCalendarField(string2Date(time, format), field); } /** * Returns the value of the given calendar field. * * @param date The date. * @param field The given calendar field. *
      *
    • {@link Calendar#ERA}
    • *
    • {@link Calendar#YEAR}
    • *
    • {@link Calendar#MONTH}
    • *
    • ...
    • *
    • {@link Calendar#DST_OFFSET}
    • *
    * @return the value of the given calendar field */ public static int getValueByCalendarField(final Date date, final int field) { Calendar cal = Calendar.getInstance(); cal.setTime(date); return cal.get(field); } /** * Returns the value of the given calendar field. * * @param millis The milliseconds. * @param field The given calendar field. *
      *
    • {@link Calendar#ERA}
    • *
    • {@link Calendar#YEAR}
    • *
    • {@link Calendar#MONTH}
    • *
    • ...
    • *
    • {@link Calendar#DST_OFFSET}
    • *
    * @return the value of the given calendar field */ public static int getValueByCalendarField(final long millis, final int field) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(millis); return cal.get(field); } private static final String[] CHINESE_ZODIAC = {"猴", "鸡", "狗", "猪", "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊"}; /** * Return the Chinese zodiac. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return the Chinese zodiac */ public static String getChineseZodiac(final String time) { return getChineseZodiac(string2Date(time, getDefaultFormat())); } /** * Return the Chinese zodiac. * * @param time The formatted time string. * @param format The format. * @return the Chinese zodiac */ public static String getChineseZodiac(final String time, @NonNull final DateFormat format) { return getChineseZodiac(string2Date(time, format)); } /** * Return the Chinese zodiac. * * @param date The date. * @return the Chinese zodiac */ public static String getChineseZodiac(final Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); return CHINESE_ZODIAC[cal.get(Calendar.YEAR) % 12]; } /** * Return the Chinese zodiac. * * @param millis The milliseconds. * @return the Chinese zodiac */ public static String getChineseZodiac(final long millis) { return getChineseZodiac(millis2Date(millis)); } /** * Return the Chinese zodiac. * * @param year The year. * @return the Chinese zodiac */ public static String getChineseZodiac(final int year) { return CHINESE_ZODIAC[year % 12]; } private static final int[] ZODIAC_FLAGS = {20, 19, 21, 21, 21, 22, 23, 23, 23, 24, 23, 22}; private static final String[] ZODIAC = { "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "魔羯座" }; /** * Return the zodiac. *

    The pattern is {@code yyyy-MM-dd HH:mm:ss}.

    * * @param time The formatted time string. * @return the zodiac */ public static String getZodiac(final String time) { return getZodiac(string2Date(time, getDefaultFormat())); } /** * Return the zodiac. * * @param time The formatted time string. * @param format The format. * @return the zodiac */ public static String getZodiac(final String time, @NonNull final DateFormat format) { return getZodiac(string2Date(time, format)); } /** * Return the zodiac. * * @param date The date. * @return the zodiac */ public static String getZodiac(final Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); return getZodiac(month, day); } /** * Return the zodiac. * * @param millis The milliseconds. * @return the zodiac */ public static String getZodiac(final long millis) { return getZodiac(millis2Date(millis)); } /** * Return the zodiac. * * @param month The month. * @param day The day. * @return the zodiac */ public static String getZodiac(final int month, final int day) { return ZODIAC[day >= ZODIAC_FLAGS[month - 1] ? month - 1 : (month + 10) % 12]; } private static long timeSpan2Millis(final long timeSpan, @TimeConstants.Unit final int unit) { return timeSpan * unit; } private static long millis2TimeSpan(final long millis, @TimeConstants.Unit final int unit) { return millis / unit; } private static String millis2FitTimeSpan(long millis, int precision) { if (precision <= 0) return null; precision = Math.min(precision, 5); String[] units = {"天", "小时", "分钟", "秒", "毫秒"}; if (millis == 0) return 0 + units[precision - 1]; StringBuilder sb = new StringBuilder(); if (millis < 0) { sb.append("-"); millis = -millis; } int[] unitLen = {86400000, 3600000, 60000, 1000, 1}; for (int i = 0; i < precision; i++) { if (millis >= unitLen[i]) { long mode = millis / unitLen[i]; millis -= mode * unitLen[i]; sb.append(mode).append(units[i]); } } return sb.toString(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/Toasts.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils import android.content.Context import android.widget.Toast import androidx.fragment.app.Fragment private var toast: Toast? = null fun Context.toastOnUi(message: Int) { runOnUI { if (toast == null) { toast = Toast.makeText(this, message, Toast.LENGTH_SHORT) } else { toast?.setText(message) toast?.duration = Toast.LENGTH_SHORT } toast?.show() } } fun Context.toastOnUi(message: CharSequence?) { runOnUI { if (toast == null) { toast = Toast.makeText(this, message, Toast.LENGTH_SHORT) } else { toast?.setText(message) toast?.duration = Toast.LENGTH_SHORT } toast?.show() } } fun Context.longToastOnUi(message: Int) { runOnUI { if (toast == null) { toast = Toast.makeText(this, message, Toast.LENGTH_LONG) } else { toast?.setText(message) toast?.duration = Toast.LENGTH_LONG } toast?.show() } } fun Context.longToastOnUi(message: CharSequence?) { runOnUI { if (toast == null) { toast = Toast.makeText(this, message, Toast.LENGTH_LONG) } else { toast?.setText(message) toast?.duration = Toast.LENGTH_LONG } toast?.show() } } /** * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. * * @param message the message text resource. */ fun Fragment.toastOnUi(message: Int) = requireActivity().toastOnUi(message) /** * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. * * @param message the message text. */ fun Fragment.toastOnUi(message: CharSequence) = requireActivity().toastOnUi(message) /** * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. * * @param message the message text resource. */ fun Fragment.longToast(message: Int) = requireContext().longToastOnUi(message) /** * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. * * @param message the message text. */ fun Fragment.longToast(message: CharSequence) = requireContext().longToastOnUi(message) ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/UriExtensions.kt ================================================ package com.kunfei.bookshelf.utils import android.content.Context import android.net.Uri import androidx.appcompat.app.AppCompatActivity import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import com.kunfei.bookshelf.R import com.kunfei.bookshelf.help.permission.Permissions import com.kunfei.bookshelf.help.permission.PermissionsCompat import timber.log.Timber import java.io.File import java.io.IOException fun Uri.isContentScheme() = this.scheme == "content" /** * 读取URI */ fun AppCompatActivity.readUri(uri: Uri?, success: (name: String, bytes: ByteArray) -> Unit) { uri ?: return try { if (uri.isContentScheme()) { val doc = DocumentFile.fromSingleUri(this, uri) doc ?: throw IOException("未获取到文件") val name = doc.name ?: throw IOException("未获取到文件名") val fileBytes = DocumentUtils.readBytes(this, doc.uri) success.invoke(name, fileBytes) } else { PermissionsCompat.Builder(this) .addPermissions( Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE ) .rationale(R.string.bg_image_per) .onGranted { RealPathUtil.getPath(this, uri)?.let { path -> val imgFile = File(path) success.invoke(imgFile.name, imgFile.readBytes()) } } .request() } } catch (e: Exception) { Timber.e(e) toastOnUi(e.localizedMessage ?: "read uri error") } } /** * 读取URI */ fun Fragment.readUri(uri: Uri?, success: (name: String, bytes: ByteArray) -> Unit) { uri ?: return try { if (uri.isContentScheme()) { val doc = DocumentFile.fromSingleUri(requireContext(), uri) doc ?: throw IOException("未获取到文件") val name = doc.name ?: throw IOException("未获取到文件名") val fileBytes = DocumentUtils.readBytes(requireContext(), doc.uri) success.invoke(name, fileBytes) } else { PermissionsCompat.Builder(this) .addPermissions( Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE ) .rationale(R.string.bg_image_per) .onGranted { RealPathUtil.getPath(requireContext(), uri)?.let { path -> val imgFile = File(path) success.invoke(imgFile.name, imgFile.readBytes()) } } .request() } } catch (e: Exception) { Timber.e(e) toastOnUi(e.localizedMessage ?: "read uri error") } } @Throws(Exception::class) fun Uri.readBytes(context: Context): ByteArray { return if (this.isContentScheme()) { DocumentUtils.readBytes(context, this) } else { val path = RealPathUtil.getPath(context, this) if (path?.isNotEmpty() == true) { File(path).readBytes() } else { throw IOException("获取文件真实地址失败\n${this.path}") } } } @Throws(Exception::class) fun Uri.readText(context: Context): String { readBytes(context).let { return String(it) } } @Throws(Exception::class) fun Uri.writeBytes( context: Context, byteArray: ByteArray ): Boolean { if (this.isContentScheme()) { return DocumentUtils.writeBytes(context, byteArray, this) } else { val path = RealPathUtil.getPath(context, this) if (path?.isNotEmpty() == true) { File(path).writeBytes(byteArray) return true } } return false } @Throws(Exception::class) fun Uri.writeText(context: Context, text: String): Boolean { return writeBytes(context, text.toByteArray()) } fun Uri.writeBytes( context: Context, fileName: String, byteArray: ByteArray ): Boolean { if (this.isContentScheme()) { DocumentFile.fromTreeUri(context, this)?.let { pDoc -> DocumentUtils.createFileIfNotExist(pDoc, fileName)?.let { return it.uri.writeBytes(context, byteArray) } } } else { FileUtils.createFileWithReplace(path + File.separatorChar + fileName) .writeBytes(byteArray) return true } return false } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/UrlEncoderUtils.java ================================================ package com.kunfei.bookshelf.utils; import java.util.BitSet; /** * 这里会有误差,比如输入一个字符串 123+456,它到底是原文就是123+456还是123 456做了urlEncode后的内容呢?
    * 其实问题是一样的,比如遇到123%2B456,它到底是原文即使如此,还是123+456 urlEncode后的呢?
    * 在这里,我认为只要符合urlEncode规范的,就当作已经urlEncode过了
    * 毕竟这个方法的初衷就是判断string是否urlEncode过
    */ public class UrlEncoderUtils { private static BitSet dontNeedEncoding; static { dontNeedEncoding = new BitSet(256); int i; for (i = 'a'; i <= 'z'; i++) { dontNeedEncoding.set(i); } for (i = 'A'; i <= 'Z'; i++) { dontNeedEncoding.set(i); } for (i = '0'; i <= '9'; i++) { dontNeedEncoding.set(i); } dontNeedEncoding.set('+'); dontNeedEncoding.set('-'); dontNeedEncoding.set('_'); dontNeedEncoding.set('.'); dontNeedEncoding.set('$'); dontNeedEncoding.set(':'); dontNeedEncoding.set('('); dontNeedEncoding.set(')'); dontNeedEncoding.set('!'); dontNeedEncoding.set('*'); dontNeedEncoding.set('@'); dontNeedEncoding.set('&'); dontNeedEncoding.set('#'); dontNeedEncoding.set(','); dontNeedEncoding.set('['); dontNeedEncoding.set(']'); } /** * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' * 0-9a-zA-Z保留
    * ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留 * 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F] */ public static boolean hasUrlEncoded(String str) { boolean needEncode = false; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (dontNeedEncoding.get((int) c)) { continue; } if (c == '%' && (i + 2) < str.length()) { // 判断是否符合urlEncode规范 char c1 = str.charAt(++i); char c2 = str.charAt(++i); if (isDigit16Char(c1) && isDigit16Char(c2)) { continue; } } // 其他字符,肯定需要urlEncode needEncode = true; break; } return !needEncode; } /** * 判断c是否是16进制的字符 */ private static boolean isDigit16Char(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/XmlUtils.java ================================================ package com.kunfei.bookshelf.utils; /* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.net.Uri; import android.util.Base64; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ProtocolException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @SuppressWarnings({"unused", "WeakerAccess"}) public class XmlUtils { public static void skipCurrentTag(XmlPullParser parser) throws XmlPullParserException, IOException { int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { } } public static int convertValueToList(CharSequence value, String[] options, int defaultValue) { if (null != value) { for (int i = 0; i < options.length; i++) { if (value.equals(options[i])) return i; } } return defaultValue; } public static boolean convertValueToBoolean(CharSequence value, boolean defaultValue) { boolean result = false; if (null == value) return defaultValue; if (value.equals("1") || value.equals("true") || value.equals("TRUE")) result = true; return result; } public static int convertValueToInt(CharSequence charSeq, int defaultValue) { if (null == charSeq) return defaultValue; String nm = charSeq.toString(); // XXX This code is copied from Integer.decode() so we don't // have to instantiate an Integer! int value; int sign = 1; int index = 0; int len = nm.length(); int base = 10; if ('-' == nm.charAt(0)) { sign = -1; index++; } if ('0' == nm.charAt(index)) { // Quick check for a zero by itself if (index == (len - 1)) return 0; char c = nm.charAt(index + 1); if ('x' == c || 'X' == c) { index += 2; base = 16; } else { index++; base = 8; } } else if ('#' == nm.charAt(index)) { index++; base = 16; } return Integer.parseInt(nm.substring(index), base) * sign; } public static int convertValueToUnsignedInt(String value, int defaultValue) { if (null == value) { return defaultValue; } return parseUnsignedIntAttribute(value); } public static int parseUnsignedIntAttribute(CharSequence charSeq) { String value = charSeq.toString(); long bits; int index = 0; int len = value.length(); int base = 10; if ('0' == value.charAt(index)) { // Quick check for zero by itself if (index == (len - 1)) return 0; char c = value.charAt(index + 1); if ('x' == c || 'X' == c) { // check for hex index += 2; base = 16; } else { // check for octal index++; base = 8; } } else if ('#' == value.charAt(index)) { index++; base = 16; } return (int) Long.parseLong(value.substring(index), base); } /** * Flatten a Map into an output stream as XML. The map can later be * read back with readMapXml(). * * @param val The map to be flattened. * @param out Where to write the XML data. * @see #writeMapXml(Map, String, XmlSerializer) * @see #writeListXml * @see #writeValueXml * @see #readMapXml */ public static void writeMapXml(Map val, OutputStream out) throws XmlPullParserException, java.io.IOException { XmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(out, "utf-8"); serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); writeMapXml(val, null, serializer); serializer.endDocument(); } /** * Flatten a List into an output stream as XML. The list can later be * read back with readListXml(). * * @param val The list to be flattened. * @param out Where to write the XML data. * @see #writeListXml(List, String, XmlSerializer) * @see #writeMapXml * @see #writeValueXml * @see #readListXml */ public static void writeListXml(List val, OutputStream out) throws XmlPullParserException, java.io.IOException { XmlSerializer serializer = Xml.newSerializer(); serializer.setOutput(out, "utf-8"); serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); writeListXml(val, null, serializer); serializer.endDocument(); } /** * Flatten a Map into an XmlSerializer. The map can later be read back * with readThisMapXml(). * * @param val The map to be flattened. * @param name Name attribute to include with this list's tag, or null for * none. * @param out XmlSerializer to write the map into. * @see #writeMapXml(Map, OutputStream) * @see #writeListXml * @see #writeValueXml * @see #readMapXml */ public static void writeMapXml(Map val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { writeMapXml(val, name, out, null); } /** * Flatten a Map into an XmlSerializer. The map can later be read back * with readThisMapXml(). * * @param val The map to be flattened. * @param name Name attribute to include with this list's tag, or null for * none. * @param out XmlSerializer to write the map into. * @param callback Method to call when an Object type is not recognized. * @hide * @see #writeMapXml(Map, OutputStream) * @see #writeListXml * @see #writeValueXml * @see #readMapXml */ public static void writeMapXml(Map val, String name, XmlSerializer out, WriteMapCallback callback) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "map"); if (name != null) { out.attribute(null, "name", name); } writeMapXml(val, out, callback); out.endTag(null, "map"); } /** * Flatten a Map into an XmlSerializer. The map can later be read back * with readThisMapXml(). This method presumes that the start tag and * name attribute have already been written and does not write an end tag. * * @param val The map to be flattened. * @param out XmlSerializer to write the map into. * @hide * @see #writeMapXml(Map, OutputStream) * @see #writeListXml * @see #writeValueXml * @see #readMapXml */ public static void writeMapXml(Map val, XmlSerializer out, WriteMapCallback callback) throws XmlPullParserException, java.io.IOException { if (val == null) { return; } Set s = val.entrySet(); for (Object o : s) { Map.Entry e = (Map.Entry) o; writeValueXml(e.getValue(), (String) e.getKey(), out, callback); } } /** * Flatten a List into an XmlSerializer. The list can later be read back * with readThisListXml(). * * @param val The list to be flattened. * @param name Name attribute to include with this list's tag, or null for * none. * @param out XmlSerializer to write the list into. * @see #writeListXml(List, OutputStream) * @see #writeMapXml * @see #writeValueXml * @see #readListXml */ public static void writeListXml(List val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "list"); if (name != null) { out.attribute(null, "name", name); } int N = val.size(); int i = 0; while (i < N) { writeValueXml(val.get(i), null, out); i++; } out.endTag(null, "list"); } public static void writeSetXml(Set val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "set"); if (name != null) { out.attribute(null, "name", name); } for (Object v : val) { writeValueXml(v, null, out); } out.endTag(null, "set"); } /** * Flatten a byte[] into an XmlSerializer. The list can later be read back * with readThisByteArrayXml(). * * @param val The byte array to be flattened. * @param name Name attribute to include with this array's tag, or null for * none. * @param out XmlSerializer to write the array into. * @see #writeMapXml * @see #writeValueXml */ public static void writeByteArrayXml(byte[] val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "byte-array"); if (name != null) { out.attribute(null, "name", name); } final int N = val.length; out.attribute(null, "num", Integer.toString(N)); StringBuilder sb = new StringBuilder(val.length * 2); for (int b : val) { int h = b >> 4; sb.append(h >= 10 ? ('a' + h - 10) : ('0' + h)); h = b & 0xff; sb.append(h >= 10 ? ('a' + h - 10) : ('0' + h)); } out.text(sb.toString()); out.endTag(null, "byte-array"); } /** * Flatten an int[] into an XmlSerializer. The list can later be read back * with readThisIntArrayXml(). * * @param val The int array to be flattened. * @param name Name attribute to include with this array's tag, or null for * none. * @param out XmlSerializer to write the array into. * @see #writeMapXml * @see #writeValueXml * @see #readThisIntArrayXml */ public static void writeIntArrayXml(int[] val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "int-array"); if (name != null) { out.attribute(null, "name", name); } final int N = val.length; out.attribute(null, "num", Integer.toString(N)); for (int i1 : val) { out.startTag(null, "item"); out.attribute(null, "value", Integer.toString(i1)); out.endTag(null, "item"); } out.endTag(null, "int-array"); } /** * Flatten a long[] into an XmlSerializer. The list can later be read back * with readThisLongArrayXml(). * * @param val The long array to be flattened. * @param name Name attribute to include with this array's tag, or null for * none. * @param out XmlSerializer to write the array into. * @see #writeMapXml * @see #writeValueXml * @see #readThisIntArrayXml */ public static void writeLongArrayXml(long[] val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "long-array"); if (name != null) { out.attribute(null, "name", name); } final int N = val.length; out.attribute(null, "num", Integer.toString(N)); for (long l : val) { out.startTag(null, "item"); out.attribute(null, "value", Long.toString(l)); out.endTag(null, "item"); } out.endTag(null, "long-array"); } /** * Flatten a double[] into an XmlSerializer. The list can later be read back * with readThisDoubleArrayXml(). * * @param val The double array to be flattened. * @param name Name attribute to include with this array's tag, or null for * none. * @param out XmlSerializer to write the array into. * @see #writeMapXml * @see #writeValueXml * @see #readThisIntArrayXml */ public static void writeDoubleArrayXml(double[] val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "double-array"); if (name != null) { out.attribute(null, "name", name); } final int N = val.length; out.attribute(null, "num", Integer.toString(N)); for (double v : val) { out.startTag(null, "item"); out.attribute(null, "value", Double.toString(v)); out.endTag(null, "item"); } out.endTag(null, "double-array"); } /** * Flatten a String[] into an XmlSerializer. The list can later be read back * with readThisStringArrayXml(). * * @param val The long array to be flattened. * @param name Name attribute to include with this array's tag, or null for * none. * @param out XmlSerializer to write the array into. * @see #writeMapXml * @see #writeValueXml * @see #readThisIntArrayXml */ public static void writeStringArrayXml(String[] val, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } out.startTag(null, "string-array"); if (name != null) { out.attribute(null, "name", name); } final int N = val.length; out.attribute(null, "num", Integer.toString(N)); for (String s : val) { out.startTag(null, "item"); out.attribute(null, "value", s); out.endTag(null, "item"); } out.endTag(null, "string-array"); } /** * Flatten an object's value into an XmlSerializer. The value can later * be read back with readThisValueXml(). *

    * Currently supported value types are: null, String, Integer, Long, * Float, Double Boolean, Map, List. * * @param v The object to be flattened. * @param name Name attribute to include with this value's tag, or null * for none. * @param out XmlSerializer to write the object into. * @see #writeMapXml * @see #writeListXml * @see #readValueXml */ public static void writeValueXml(Object v, String name, XmlSerializer out) throws XmlPullParserException, java.io.IOException { writeValueXml(v, name, out, null); } /** * Flatten an object's value into an XmlSerializer. The value can later * be read back with readThisValueXml(). *

    * Currently supported value types are: null, String, Integer, Long, * Float, Double Boolean, Map, List. * * @param v The object to be flattened. * @param name Name attribute to include with this value's tag, or null * for none. * @param out XmlSerializer to write the object into. * @param callback Handler for Object types not recognized. * @see #writeMapXml * @see #writeListXml * @see #readValueXml */ private static void writeValueXml(Object v, String name, XmlSerializer out, WriteMapCallback callback) throws XmlPullParserException, java.io.IOException { String typeStr; if (v == null) { out.startTag(null, "null"); if (name != null) { out.attribute(null, "name", name); } out.endTag(null, "null"); return; } else if (v instanceof String) { out.startTag(null, "string"); if (name != null) { out.attribute(null, "name", name); } out.text(v.toString()); out.endTag(null, "string"); return; } else if (v instanceof Integer) { typeStr = "int"; } else if (v instanceof Long) { typeStr = "long"; } else if (v instanceof Float) { typeStr = "float"; } else if (v instanceof Double) { typeStr = "double"; } else if (v instanceof Boolean) { typeStr = "boolean"; } else if (v instanceof byte[]) { writeByteArrayXml((byte[]) v, name, out); return; } else if (v instanceof int[]) { writeIntArrayXml((int[]) v, name, out); return; } else if (v instanceof long[]) { writeLongArrayXml((long[]) v, name, out); return; } else if (v instanceof double[]) { writeDoubleArrayXml((double[]) v, name, out); return; } else if (v instanceof String[]) { writeStringArrayXml((String[]) v, name, out); return; } else if (v instanceof Map) { writeMapXml((Map) v, name, out); return; } else if (v instanceof List) { writeListXml((List) v, name, out); return; } else if (v instanceof Set) { writeSetXml((Set) v, name, out); return; } else if (v instanceof CharSequence) { // XXX This is to allow us to at least write something if // we encounter styled text... but it means we will drop all // of the styling information. :( out.startTag(null, "string"); if (name != null) { out.attribute(null, "name", name); } out.text(v.toString()); out.endTag(null, "string"); return; } else if (callback != null) { callback.writeUnknownObject(v, name, out); return; } else { throw new RuntimeException("writeValueXml: unable to write value " + v); } out.startTag(null, typeStr); if (name != null) { out.attribute(null, "name", name); } out.attribute(null, "value", v.toString()); out.endTag(null, typeStr); } /** * Read a HashMap from an InputStream containing XML. The stream can * previously have been written by writeMapXml(). * * @param in The InputStream from which to read. * @return HashMap The resulting map. * @see #readListXml * @see #readValueXml * @see #readThisMapXml * #see #writeMapXml */ @SuppressWarnings("unchecked") public static HashMap readMapXml(InputStream in) throws XmlPullParserException, java.io.IOException { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); return (HashMap) readValueXml(parser, new String[1]); } /** * Read an ArrayList from an InputStream containing XML. The stream can * previously have been written by writeListXml(). * * @param in The InputStream from which to read. * @return ArrayList The resulting list. * @see #readMapXml * @see #readValueXml * @see #readThisListXml * @see #writeListXml */ public static ArrayList readListXml(InputStream in) throws XmlPullParserException, java.io.IOException { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); return (ArrayList) readValueXml(parser, new String[1]); } /** * Read a HashSet from an InputStream containing XML. The stream can * previously have been written by writeSetXml(). * * @param in The InputStream from which to read. * @return HashSet The resulting set. * @throws XmlPullParserException * @throws java.io.IOException * @see #readValueXml * @see #readThisSetXml * @see #writeSetXml */ public static HashSet readSetXml(InputStream in) throws XmlPullParserException, java.io.IOException { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); return (HashSet) readValueXml(parser, new String[1]); } /** * Read a HashMap object from an XmlPullParser. The XML data could * previously have been generated by writeMapXml(). The XmlPullParser * must be positioned after the tag that begins the map. * * @param parser The XmlPullParser from which to read the map data. * @param endTag Name of the tag that will end the map, usually "map". * @param name An array of one string, used to return the name attribute * of the map's tag. * @return HashMap The newly generated map. * @see #readMapXml */ public static HashMap readThisMapXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { return readThisMapXml(parser, endTag, name, null); } /** * Read a HashMap object from an XmlPullParser. The XML data could * previously have been generated by writeMapXml(). The XmlPullParser * must be positioned after the tag that begins the map. * * @param parser The XmlPullParser from which to read the map data. * @param endTag Name of the tag that will end the map, usually "map". * @param name An array of one string, used to return the name attribute * of the map's tag. * @return HashMap The newly generated map. * @hide * @see #readMapXml */ public static HashMap readThisMapXml(XmlPullParser parser, String endTag, String[] name, ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { HashMap map = new HashMap<>(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { Object val = readThisValueXml(parser, name, callback); map.put(name[0], val); } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return map; } throw new XmlPullParserException( "Expected " + endTag + " end tag at: " + parser.getName()); } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); } /** * Read an ArrayList object from an XmlPullParser. The XML data could * previously have been generated by writeListXml(). The XmlPullParser * must be positioned after the tag that begins the list. * * @param parser The XmlPullParser from which to read the list data. * @param endTag Name of the tag that will end the list, usually "list". * @param name An array of one string, used to return the name attribute * of the list's tag. * @return HashMap The newly generated list. * @see #readListXml */ public static ArrayList readThisListXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { return readThisListXml(parser, endTag, name, null); } /** * Read an ArrayList object from an XmlPullParser. The XML data could * previously have been generated by writeListXml(). The XmlPullParser * must be positioned after the tag that begins the list. * * @param parser The XmlPullParser from which to read the list data. * @param endTag Name of the tag that will end the list, usually "list". * @param name An array of one string, used to return the name attribute * of the list's tag. * @return HashMap The newly generated list. * @see #readListXml */ private static ArrayList readThisListXml(XmlPullParser parser, String endTag, String[] name, ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { ArrayList list = new ArrayList<>(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { Object val = readThisValueXml(parser, name, callback); list.add(val); //System.out.println("Adding to list: " + val); } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return list; } throw new XmlPullParserException( "Expected " + endTag + " end tag at: " + parser.getName()); } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); } /** * Read a HashSet object from an XmlPullParser. The XML data could previously * have been generated by writeSetXml(). The XmlPullParser must be positioned * after the tag that begins the set. * * @param parser The XmlPullParser from which to read the set data. * @param endTag Name of the tag that will end the set, usually "set". * @param name An array of one string, used to return the name attribute * of the set's tag. * @return HashSet The newly generated set. * @throws XmlPullParserException * @throws java.io.IOException * @see #readSetXml */ public static HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { return readThisSetXml(parser, endTag, name, null); } /** * Read a HashSet object from an XmlPullParser. The XML data could previously * have been generated by writeSetXml(). The XmlPullParser must be positioned * after the tag that begins the set. * * @param parser The XmlPullParser from which to read the set data. * @param endTag Name of the tag that will end the set, usually "set". * @param name An array of one string, used to return the name attribute * of the set's tag. * @return HashSet The newly generated set. * @throws XmlPullParserException * @throws java.io.IOException * @hide * @see #readSetXml */ private static HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name, ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { HashSet set = new HashSet<>(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { Object val = readThisValueXml(parser, name, callback); set.add(val); //System.out.println("Adding to set: " + val); } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return set; } throw new XmlPullParserException( "Expected " + endTag + " end tag at: " + parser.getName()); } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); } /** * Read an int[] object from an XmlPullParser. The XML data could * previously have been generated by writeIntArrayXml(). The XmlPullParser * must be positioned after the tag that begins the list. * * @param parser The XmlPullParser from which to read the list data. * @param endTag Name of the tag that will end the list, usually "list". * @param name An array of one string, used to return the name attribute * of the list's tag. * @return Returns a newly generated int[]. * @see #readListXml */ public static int[] readThisIntArrayXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { int num; try { num = Integer.parseInt(parser.getAttributeValue(null, "num")); } catch (NullPointerException e) { throw new XmlPullParserException( "Need num attribute in byte-array"); } catch (NumberFormatException e) { throw new XmlPullParserException( "Not a number in num attribute in byte-array"); } parser.next(); int[] array = new int[num]; int i = 0; int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { if (parser.getName().equals("item")) { try { array[i] = Integer.parseInt( parser.getAttributeValue(null, "value")); } catch (NullPointerException e) { throw new XmlPullParserException( "Need value attribute in item"); } catch (NumberFormatException e) { throw new XmlPullParserException( "Not a number in value attribute in item"); } } else { throw new XmlPullParserException( "Expected item tag at: " + parser.getName()); } } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return array; } else if (parser.getName().equals("item")) { i++; } else { throw new XmlPullParserException( "Expected " + endTag + " end tag at: " + parser.getName()); } } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); } /** * Read a long[] object from an XmlPullParser. The XML data could * previously have been generated by writeLongArrayXml(). The XmlPullParser * must be positioned after the tag that begins the list. * * @param parser The XmlPullParser from which to read the list data. * @param endTag Name of the tag that will end the list, usually "list". * @param name An array of one string, used to return the name attribute * of the list's tag. * @return Returns a newly generated long[]. * @see #readListXml */ public static long[] readThisLongArrayXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { int num; try { num = Integer.parseInt(parser.getAttributeValue(null, "num")); } catch (NullPointerException e) { throw new XmlPullParserException("Need num attribute in long-array"); } catch (NumberFormatException e) { throw new XmlPullParserException("Not a number in num attribute in long-array"); } parser.next(); long[] array = new long[num]; int i = 0; int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { if (parser.getName().equals("item")) { try { array[i] = Long.parseLong(parser.getAttributeValue(null, "value")); } catch (NullPointerException e) { throw new XmlPullParserException("Need value attribute in item"); } catch (NumberFormatException e) { throw new XmlPullParserException("Not a number in value attribute in item"); } } else { throw new XmlPullParserException("Expected item tag at: " + parser.getName()); } } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return array; } else if (parser.getName().equals("item")) { i++; } else { throw new XmlPullParserException("Expected " + endTag + " end tag at: " + parser.getName()); } } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException("Document ended before " + endTag + " end tag"); } /** * Read a double[] object from an XmlPullParser. The XML data could * previously have been generated by writeDoubleArrayXml(). The XmlPullParser * must be positioned after the tag that begins the list. * * @param parser The XmlPullParser from which to read the list data. * @param endTag Name of the tag that will end the list, usually "double-array". * @param name An array of one string, used to return the name attribute * of the list's tag. * @return Returns a newly generated double[]. * @see #readListXml */ public static double[] readThisDoubleArrayXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { int num; try { num = Integer.parseInt(parser.getAttributeValue(null, "num")); } catch (NullPointerException e) { throw new XmlPullParserException("Need num attribute in double-array"); } catch (NumberFormatException e) { throw new XmlPullParserException("Not a number in num attribute in double-array"); } parser.next(); double[] array = new double[num]; int i = 0; int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { if (parser.getName().equals("item")) { try { array[i] = Double.parseDouble(parser.getAttributeValue(null, "value")); } catch (NullPointerException e) { throw new XmlPullParserException("Need value attribute in item"); } catch (NumberFormatException e) { throw new XmlPullParserException("Not a number in value attribute in item"); } } else { throw new XmlPullParserException("Expected item tag at: " + parser.getName()); } } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return array; } else if (parser.getName().equals("item")) { i++; } else { throw new XmlPullParserException("Expected " + endTag + " end tag at: " + parser.getName()); } } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException("Document ended before " + endTag + " end tag"); } /** * Read a String[] object from an XmlPullParser. The XML data could * previously have been generated by writeStringArrayXml(). The XmlPullParser * must be positioned after the tag that begins the list. * * @param parser The XmlPullParser from which to read the list data. * @param endTag Name of the tag that will end the list, usually "string-array". * @param name An array of one string, used to return the name attribute * of the list's tag. * @return Returns a newly generated String[]. * @see #readListXml */ public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag, String[] name) throws XmlPullParserException, java.io.IOException { int num; try { num = Integer.parseInt(parser.getAttributeValue(null, "num")); } catch (NullPointerException e) { throw new XmlPullParserException("Need num attribute in string-array"); } catch (NumberFormatException e) { throw new XmlPullParserException("Not a number in num attribute in string-array"); } parser.next(); String[] array = new String[num]; int i = 0; int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { if (parser.getName().equals("item")) { try { array[i] = parser.getAttributeValue(null, "value"); } catch (NullPointerException e) { throw new XmlPullParserException("Need value attribute in item"); } catch (NumberFormatException e) { throw new XmlPullParserException("Not a number in value attribute in item"); } } else { throw new XmlPullParserException("Expected item tag at: " + parser.getName()); } } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { return array; } else if (parser.getName().equals("item")) { i++; } else { throw new XmlPullParserException("Expected " + endTag + " end tag at: " + parser.getName()); } } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException("Document ended before " + endTag + " end tag"); } /** * Read a flattened object from an XmlPullParser. The XML data could * previously have been written with writeMapXml(), writeListXml(), or * writeValueXml(). The XmlPullParser must be positioned at the * tag that defines the value. * * @param parser The XmlPullParser from which to read the object. * @param name An array of one string, used to return the name attribute * of the value's tag. * @return Object The newly generated value object. * @see #readMapXml * @see #readListXml * @see #writeValueXml */ public static Object readValueXml(XmlPullParser parser, String[] name) throws XmlPullParserException, java.io.IOException { int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { return readThisValueXml(parser, name, null); } else if (eventType == parser.END_TAG) { throw new XmlPullParserException( "Unexpected end tag at: " + parser.getName()); } else if (eventType == parser.TEXT) { throw new XmlPullParserException( "Unexpected text: " + parser.getText()); } eventType = parser.next(); } while (eventType != parser.END_DOCUMENT); throw new XmlPullParserException( "Unexpected end of document"); } private static Object readThisValueXml(XmlPullParser parser, String[] name, ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { final String valueName = parser.getAttributeValue(null, "name"); final String tagName = parser.getName(); //System.out.println("Reading this value tag: " + tagName + ", name=" + valueName); Object res; if (tagName.equals("null")) { res = null; } else if (tagName.equals("string")) { String value = ""; int eventType; while ((eventType = parser.next()) != parser.END_DOCUMENT) { if (eventType == parser.END_TAG) { if (parser.getName().equals("string")) { name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + value); return value; } throw new XmlPullParserException( "Unexpected end tag in : " + parser.getName()); } else if (eventType == parser.TEXT) { value += parser.getText(); } else if (eventType == parser.START_TAG) { throw new XmlPullParserException( "Unexpected start tag in : " + parser.getName()); } } throw new XmlPullParserException( "Unexpected end of document in "); } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) { // all work already done by readThisPrimitiveValueXml } else if (tagName.equals("int-array")) { res = readThisIntArrayXml(parser, "int-array", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("long-array")) { res = readThisLongArrayXml(parser, "long-array", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("double-array")) { res = readThisDoubleArrayXml(parser, "double-array", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("string-array")) { res = readThisStringArrayXml(parser, "string-array", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("map")) { parser.next(); res = readThisMapXml(parser, "map", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("list")) { parser.next(); res = readThisListXml(parser, "list", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (tagName.equals("set")) { parser.next(); res = readThisSetXml(parser, "set", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } else if (callback != null) { res = callback.readThisUnknownObjectXml(parser, tagName); name[0] = valueName; return res; } else { throw new XmlPullParserException("Unknown tag: " + tagName); } // Skip through to end tag. int eventType; while ((eventType = parser.next()) != parser.END_DOCUMENT) { if (eventType == parser.END_TAG) { if (parser.getName().equals(tagName)) { name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; } throw new XmlPullParserException( "Unexpected end tag in <" + tagName + ">: " + parser.getName()); } else if (eventType == parser.TEXT) { throw new XmlPullParserException( "Unexpected text in <" + tagName + ">: " + parser.getName()); } else if (eventType == parser.START_TAG) { throw new XmlPullParserException( "Unexpected start tag in <" + tagName + ">: " + parser.getName()); } } throw new XmlPullParserException( "Unexpected end of document in <" + tagName + ">"); } private static Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName) throws XmlPullParserException, java.io.IOException { try { switch (tagName) { case "int": return Integer.parseInt(parser.getAttributeValue(null, "value")); case "long": return Long.valueOf(parser.getAttributeValue(null, "value")); case "float": return Float.valueOf(parser.getAttributeValue(null, "value")); case "double": return Double.valueOf(parser.getAttributeValue(null, "value")); case "boolean": return Boolean.valueOf(parser.getAttributeValue(null, "value")); default: return null; } } catch (NullPointerException e) { throw new XmlPullParserException("Need value attribute in <" + tagName + ">"); } catch (NumberFormatException e) { throw new XmlPullParserException( "Not a number in value attribute in <" + tagName + ">"); } } public static void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException { int type; while ((type = parser.next()) != parser.START_TAG && type != parser.END_DOCUMENT) { } if (type != parser.START_TAG) { throw new XmlPullParserException("No start tag found"); } if (!parser.getName().equals(firstElementName)) { throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + ", expected " + firstElementName); } } public static void nextElement(XmlPullParser parser) throws XmlPullParserException, IOException { int type; while ((type = parser.next()) != parser.START_TAG && type != parser.END_DOCUMENT) { } } public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) throws IOException, XmlPullParserException { for (; ; ) { int type = parser.next(); if (type == XmlPullParser.END_DOCUMENT || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { return false; } if (type == XmlPullParser.START_TAG && parser.getDepth() == outerDepth + 1) { return true; } } } public static int readIntAttribute(XmlPullParser in, String name, int defaultValue) { final String value = in.getAttributeValue(null, name); try { return Integer.parseInt(value); } catch (NumberFormatException e) { return defaultValue; } } public static int readIntAttribute(XmlPullParser in, String name) throws IOException { final String value = in.getAttributeValue(null, name); try { return Integer.parseInt(value); } catch (NumberFormatException e) { throw new ProtocolException("problem parsing " + name + "=" + value + " as int"); } } public static void writeIntAttribute(XmlSerializer out, String name, int value) throws IOException { out.attribute(null, name, Integer.toString(value)); } public static long readLongAttribute(XmlPullParser in, String name, long defaultValue) { final String value = in.getAttributeValue(null, name); try { return Long.parseLong(value); } catch (NumberFormatException e) { return defaultValue; } } public static long readLongAttribute(XmlPullParser in, String name) throws IOException { final String value = in.getAttributeValue(null, name); try { return Long.parseLong(value); } catch (NumberFormatException e) { throw new ProtocolException("problem parsing " + name + "=" + value + " as long"); } } public static void writeLongAttribute(XmlSerializer out, String name, long value) throws IOException { out.attribute(null, name, Long.toString(value)); } public static float readFloatAttribute(XmlPullParser in, String name) throws IOException { final String value = in.getAttributeValue(null, name); try { return Float.parseFloat(value); } catch (NumberFormatException e) { throw new ProtocolException("problem parsing " + name + "=" + value + " as long"); } } public static void writeFloatAttribute(XmlSerializer out, String name, float value) throws IOException { out.attribute(null, name, Float.toString(value)); } public static boolean readBooleanAttribute(XmlPullParser in, String name) { final String value = in.getAttributeValue(null, name); return Boolean.parseBoolean(value); } public static boolean readBooleanAttribute(XmlPullParser in, String name, boolean defaultValue) { final String value = in.getAttributeValue(null, name); if (value == null) { return defaultValue; } else { return Boolean.parseBoolean(value); } } public static void writeBooleanAttribute(XmlSerializer out, String name, boolean value) throws IOException { out.attribute(null, name, Boolean.toString(value)); } public static Uri readUriAttribute(XmlPullParser in, String name) { final String value = in.getAttributeValue(null, name); return (value != null) ? Uri.parse(value) : null; } public static void writeUriAttribute(XmlSerializer out, String name, Uri value) throws IOException { if (value != null) { out.attribute(null, name, value.toString()); } } public static String readStringAttribute(XmlPullParser in, String name) { return in.getAttributeValue(null, name); } public static void writeStringAttribute(XmlSerializer out, String name, String value) throws IOException { if (value != null) { out.attribute(null, name, value); } } public static byte[] readByteArrayAttribute(XmlPullParser in, String name) { final String value = in.getAttributeValue(null, name); if (value != null) { return Base64.decode(value, Base64.DEFAULT); } else { return null; } } public static void writeByteArrayAttribute(XmlSerializer out, String name, byte[] value) throws IOException { if (value != null) { out.attribute(null, name, Base64.encodeToString(value, Base64.DEFAULT)); } } public static Bitmap readBitmapAttribute(XmlPullParser in, String name) { final byte[] value = readByteArrayAttribute(in, name); if (value != null) { return BitmapFactory.decodeByteArray(value, 0, value.length); } else { return null; } } @Deprecated public static void writeBitmapAttribute(XmlSerializer out, String name, Bitmap value) throws IOException { if (value != null) { final ByteArrayOutputStream os = new ByteArrayOutputStream(); value.compress(CompressFormat.PNG, 90, os); writeByteArrayAttribute(out, name, os.toByteArray()); } } /** * @hide */ public interface WriteMapCallback { /** * Called from writeMapXml when an Object type is not recognized. The implementer * must write out the entire element including start and end tags. * * @param v The object to be written out * @param name The mapping key for v. Must be written into the "name" attribute of the * start tag. * @param out The XML output stream. * @throws XmlPullParserException on unrecognized Object type. * @throws IOException on XmlSerializer serialization errors. * @hide */ void writeUnknownObject(Object v, String name, XmlSerializer out) throws XmlPullParserException, IOException; } /** * @hide */ public interface ReadMapCallback { /** * Called from readThisMapXml when a START_TAG is not recognized. The input stream * is positioned within the start tag so that attributes can be read using in.getAttribute. * * @param in the XML input stream * @param tag the START_TAG that was not recognized. * @return the Object parsed from the stream which will be put into the map. * @throws XmlPullParserException if the START_TAG is not recognized. * @throws IOException on XmlPullParser serialization errors. * @hide */ Object readThisUnknownObjectXml(XmlPullParser in, String tag) throws XmlPullParserException, IOException; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/ZipUtils.java ================================================ package com.kunfei.bookshelf.utils; import android.util.Log; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; /** *
     *     author: Blankj
     *     blog  : http://blankj.com
     *     time  : 2016/08/27
     *     desc  : utils about zip
     * 
    */ @SuppressWarnings({"unused", "WeakerAccess"}) public final class ZipUtils { private static final int BUFFER_LEN = 8192; private ZipUtils() { throw new UnsupportedOperationException("u can't instantiate me..."); } /** * Zip the files. * * @param srcFiles The source of files. * @param zipFilePath The path of ZIP file. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFiles(final Collection srcFiles, final String zipFilePath) throws IOException { return zipFiles(srcFiles, zipFilePath, null); } /** * Zip the files. * * @param srcFilePaths The paths of source files. * @param zipFilePath The path of ZIP file. * @param comment The comment. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFiles(final Collection srcFilePaths, final String zipFilePath, final String comment) throws IOException { if (srcFilePaths == null || zipFilePath == null) return false; ZipOutputStream zos = null; try { zos = new ZipOutputStream(new FileOutputStream(zipFilePath)); for (String srcFile : srcFilePaths) { if (!zipFile(getFileByPath(srcFile), "", zos, comment)) return false; } return true; } finally { if (zos != null) { zos.finish(); zos.close(); } } } /** * Zip the files. * * @param srcFiles The source of files. * @param zipFile The ZIP file. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFiles(final Collection srcFiles, final File zipFile) throws IOException { return zipFiles(srcFiles, zipFile, null); } /** * Zip the files. * * @param srcFiles The source of files. * @param zipFile The ZIP file. * @param comment The comment. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFiles(final Collection srcFiles, final File zipFile, final String comment) throws IOException { if (srcFiles == null || zipFile == null) return false; ZipOutputStream zos = null; try { zos = new ZipOutputStream(new FileOutputStream(zipFile)); for (File srcFile : srcFiles) { if (!zipFile(srcFile, "", zos, comment)) return false; } return true; } finally { if (zos != null) { zos.finish(); zos.close(); } } } /** * Zip the file. * * @param srcFilePath The path of source file. * @param zipFilePath The path of ZIP file. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFile(final String srcFilePath, final String zipFilePath) throws IOException { return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), null); } /** * Zip the file. * * @param srcFilePath The path of source file. * @param zipFilePath The path of ZIP file. * @param comment The comment. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFile(final String srcFilePath, final String zipFilePath, final String comment) throws IOException { return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), comment); } /** * Zip the file. * * @param srcFile The source of file. * @param zipFile The ZIP file. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFile(final File srcFile, final File zipFile) throws IOException { return zipFile(srcFile, zipFile, null); } /** * Zip the file. * * @param srcFile The source of file. * @param zipFile The ZIP file. * @param comment The comment. * @return {@code true}: success
    {@code false}: fail * @throws IOException if an I/O error has occurred */ public static boolean zipFile(final File srcFile, final File zipFile, final String comment) throws IOException { if (srcFile == null || zipFile == null) return false; try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { return zipFile(srcFile, "", zos, comment); } } private static boolean zipFile(final File srcFile, String rootPath, final ZipOutputStream zos, final String comment) throws IOException { if (!srcFile.exists()) return true; rootPath = rootPath + (isSpace(rootPath) ? "" : File.separator) + srcFile.getName(); if (srcFile.isDirectory()) { File[] fileList = srcFile.listFiles(); if (fileList == null || fileList.length <= 0) { ZipEntry entry = new ZipEntry(rootPath + '/'); entry.setComment(comment); zos.putNextEntry(entry); zos.closeEntry(); } else { for (File file : fileList) { if (!zipFile(file, rootPath, zos, comment)) return false; } } } else { try (InputStream is = new BufferedInputStream(new FileInputStream(srcFile))) { ZipEntry entry = new ZipEntry(rootPath); entry.setComment(comment); zos.putNextEntry(entry); byte buffer[] = new byte[BUFFER_LEN]; int len; while ((len = is.read(buffer, 0, BUFFER_LEN)) != -1) { zos.write(buffer, 0, len); } zos.closeEntry(); } } return true; } /** * Unzip the file. * * @param zipFilePath The path of ZIP file. * @param destDirPath The path of destination directory. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ public static List unzipFile(final String zipFilePath, final String destDirPath) throws IOException { return unzipFileByKeyword(zipFilePath, destDirPath, null); } /** * Unzip the file. * * @param zipFile The ZIP file. * @param destDir The destination directory. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ public static List unzipFile(final File zipFile, final File destDir) throws IOException { return unzipFileByKeyword(zipFile, destDir, null); } /** * Unzip the file by keyword. * * @param zipFilePath The path of ZIP file. * @param destDirPath The path of destination directory. * @param keyword The keyboard. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ public static List unzipFileByKeyword(final String zipFilePath, final String destDirPath, final String keyword) throws IOException { return unzipFileByKeyword(getFileByPath(zipFilePath), getFileByPath(destDirPath), keyword); } /** * Unzip the file by keyword. * * @param zipFile The ZIP file. * @param destDir The destination directory. * @param keyword The keyboard. * @return the unzipped files * @throws IOException if unzip unsuccessfully */ public static List unzipFileByKeyword(final File zipFile, final File destDir, final String keyword) throws IOException { if (zipFile == null || destDir == null) return null; List files = new ArrayList<>(); ZipFile zip = new ZipFile(zipFile); Enumeration entries = zip.entries(); try { if (isSpace(keyword)) { while (entries.hasMoreElements()) { ZipEntry entry = ((ZipEntry) entries.nextElement()); String entryName = entry.getName(); if (entryName.contains("../")) { Log.e("ZipUtils", "entryName: " + entryName + " is dangerous!"); continue; } if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files; } } else { while (entries.hasMoreElements()) { ZipEntry entry = ((ZipEntry) entries.nextElement()); String entryName = entry.getName(); if (entryName.contains("../")) { Log.e("ZipUtils", "entryName: " + entryName + " is dangerous!"); continue; } if (entryName.contains(keyword)) { if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files; } } } } finally { zip.close(); } return files; } private static boolean unzipChildFile(final File destDir, final List files, final ZipFile zip, final ZipEntry entry, final String name) throws IOException { File file = new File(destDir, name); files.add(file); if (entry.isDirectory()) { return createOrExistsDir(file); } else { if (!createOrExistsFile(file)) return false; try (InputStream in = new BufferedInputStream(zip.getInputStream(entry)); OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) { byte buffer[] = new byte[BUFFER_LEN]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } } return true; } /** * Return the files' path in ZIP file. * * @param zipFilePath The path of ZIP file. * @return the files' path in ZIP file * @throws IOException if an I/O error has occurred */ public static List getFilesPath(final String zipFilePath) throws IOException { return getFilesPath(getFileByPath(zipFilePath)); } /** * Return the files' path in ZIP file. * * @param zipFile The ZIP file. * @return the files' path in ZIP file * @throws IOException if an I/O error has occurred */ public static List getFilesPath(final File zipFile) throws IOException { if (zipFile == null) return null; List paths = new ArrayList<>(); ZipFile zip = new ZipFile(zipFile); Enumeration entries = zip.entries(); while (entries.hasMoreElements()) { String entryName = ((ZipEntry) entries.nextElement()).getName(); if (entryName.contains("../")) { Log.e("ZipUtils", "entryName: " + entryName + " is dangerous!"); paths.add(entryName); } else { paths.add(entryName); } } zip.close(); return paths; } /** * Return the files' comment in ZIP file. * * @param zipFilePath The path of ZIP file. * @return the files' comment in ZIP file * @throws IOException if an I/O error has occurred */ public static List getComments(final String zipFilePath) throws IOException { return getComments(getFileByPath(zipFilePath)); } /** * Return the files' comment in ZIP file. * * @param zipFile The ZIP file. * @return the files' comment in ZIP file * @throws IOException if an I/O error has occurred */ public static List getComments(final File zipFile) throws IOException { if (zipFile == null) return null; List comments = new ArrayList<>(); ZipFile zip = new ZipFile(zipFile); Enumeration entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = ((ZipEntry) entries.nextElement()); comments.add(entry.getComment()); } zip.close(); return comments; } private static boolean createOrExistsDir(final File file) { return file != null && (file.exists() ? file.isDirectory() : file.mkdirs()); } private static boolean createOrExistsFile(final File file) { if (file == null) return false; if (file.exists()) return file.isFile(); if (!createOrExistsDir(file.getParentFile())) return false; try { return file.createNewFile(); } catch (IOException e) { e.printStackTrace(); return false; } } private static File getFileByPath(final String filePath) { return isSpace(filePath) ? null : new File(filePath); } private static boolean isSpace(final String s) { if (s == null) return true; for (int i = 0, len = s.length(); i < len; ++i) { if (!Character.isWhitespace(s.charAt(i))) { return false; } } return true; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AlertBuilder.kt ================================================ @file:Suppress("NOTHING_TO_INLINE", "unused") package com.kunfei.bookshelf.utils.dialogs import android.annotation.SuppressLint import android.content.Context import android.content.DialogInterface import android.graphics.drawable.Drawable import android.view.KeyEvent import android.view.View import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.kunfei.bookshelf.R @SuppressLint("SupportAnnotationUsage") interface AlertBuilder { val ctx: Context fun setTitle(title: CharSequence) fun setTitle(titleResource: Int) fun setMessage(message: CharSequence) fun setMessage(messageResource: Int) fun setIcon(icon: Drawable) fun setIcon(@DrawableRes iconResource: Int) fun setCustomTitle(customTitle: View) fun setCustomView(customView: View) fun setCancelable(isCancelable: Boolean) fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) fun positiveButton( @StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null ) fun negativeButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) fun negativeButton( @StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null ) fun neutralButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) fun neutralButton( @StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null ) fun onCancelled(handler: (dialog: DialogInterface) -> Unit) fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) fun onDismiss(handler: (dialog: DialogInterface) -> Unit) fun items( items: List, onItemSelected: (dialog: DialogInterface, index: Int) -> Unit ) fun items( items: List, onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit ) fun multiChoiceItems( items: Array, checkedItems: BooleanArray, onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit ) fun singleChoiceItems( items: Array, checkedItem: Int = 0, onClick: ((dialog: DialogInterface, which: Int) -> Unit)? = null ) fun build(): D fun show(): D fun customTitle(view: () -> View) { setCustomTitle(view()) } fun customView(view: () -> View) { setCustomView(view()) } fun okButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = positiveButton(android.R.string.ok, handler) fun cancelButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = negativeButton(android.R.string.cancel, handler) fun yesButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = positiveButton(R.string.yes, handler) fun noButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = negativeButton(R.string.no, handler) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AndroidAlertBuilder.kt ================================================ package com.kunfei.bookshelf.utils.dialogs import android.content.Context import android.content.DialogInterface import android.graphics.drawable.Drawable import android.view.KeyEvent import android.view.View import androidx.appcompat.app.AlertDialog import com.kunfei.bookshelf.utils.applyTint internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder { private val builder = AlertDialog.Builder(ctx) override fun setTitle(title: CharSequence) { builder.setTitle(title) } override fun setTitle(titleResource: Int) { builder.setTitle(titleResource) } override fun setMessage(message: CharSequence) { builder.setMessage(message) } override fun setMessage(messageResource: Int) { builder.setMessage(messageResource) } override fun setIcon(icon: Drawable) { builder.setIcon(icon) } override fun setIcon(iconResource: Int) { builder.setIcon(iconResource) } override fun setCustomTitle(customTitle: View) { builder.setCustomTitle(customTitle) } override fun setCustomView(customView: View) { builder.setView(customView) } override fun setCancelable(isCancelable: Boolean) { builder.setCancelable(isCancelable) } override fun onCancelled(handler: (DialogInterface) -> Unit) { builder.setOnCancelListener(handler) } override fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) { builder.setOnKeyListener(handler) } override fun positiveButton( buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? ) { builder.setPositiveButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } } override fun positiveButton( buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? ) { builder.setPositiveButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } } override fun negativeButton( buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? ) { builder.setNegativeButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } } override fun negativeButton( buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? ) { builder.setNegativeButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } } override fun neutralButton( buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? ) { builder.setNeutralButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } } override fun neutralButton( buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? ) { builder.setNeutralButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } } override fun onDismiss(handler: (dialog: DialogInterface) -> Unit) { builder.setOnDismissListener(handler) } override fun items( items: List, onItemSelected: (dialog: DialogInterface, index: Int) -> Unit ) { builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which -> onItemSelected(dialog, which) } } override fun items( items: List, onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit ) { builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which -> onItemSelected(dialog, items[which], which) } } override fun multiChoiceItems( items: Array, checkedItems: BooleanArray, onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit ) { builder.setMultiChoiceItems(items, checkedItems) { dialog, which, isChecked -> onClick(dialog, which, isChecked) } } override fun singleChoiceItems( items: Array, checkedItem: Int, onClick: ((dialog: DialogInterface, which: Int) -> Unit)? ) { builder.setSingleChoiceItems(items, checkedItem) { dialog, which -> onClick?.invoke(dialog, which) } } override fun build(): AlertDialog = builder.create() override fun show(): AlertDialog = builder.show().applyTint() } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AndroidDialogs.kt ================================================ @file:Suppress("NOTHING_TO_INLINE", "unused", "DEPRECATION") package io.legado.app.lib.dialogs import android.app.ProgressDialog import android.content.Context import android.content.DialogInterface import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import com.kunfei.bookshelf.utils.dialogs.AlertBuilder import com.kunfei.bookshelf.utils.dialogs.AndroidAlertBuilder fun Context.alert( title: CharSequence? = null, message: CharSequence? = null, init: (AlertBuilder.() -> Unit)? = null ): AlertDialog { return AndroidAlertBuilder(this).apply { if (title != null) { this.setTitle(title) } if (message != null) { this.setMessage(message) } if (init != null) init() }.show() } inline fun Fragment.alert( title: CharSequence? = null, message: CharSequence? = null, noinline init: (AlertBuilder.() -> Unit)? = null ) = requireActivity().alert(title, message, init) fun Context.alert( titleResource: Int? = null, messageResource: Int? = null, init: (AlertBuilder.() -> Unit)? = null ): AlertDialog { return AndroidAlertBuilder(this).apply { if (titleResource != null) { this.setTitle(titleResource) } if (messageResource != null) { this.setMessage(messageResource) } if (init != null) init() }.show() } inline fun Fragment.alert( titleResource: Int? = null, message: Int? = null, noinline init: (AlertBuilder.() -> Unit)? = null ) = requireActivity().alert(titleResource, message, init) fun Context.alert(init: AlertBuilder.() -> Unit): AlertDialog = AndroidAlertBuilder(this).apply { init() }.show() inline fun Fragment.alert(noinline init: AlertBuilder.() -> Unit) = requireContext().alert(init) inline fun Fragment.progressDialog( title: Int? = null, message: Int? = null, noinline init: (ProgressDialog.() -> Unit)? = null ) = requireActivity().progressDialog(title, message, init) fun Context.progressDialog( title: Int? = null, message: Int? = null, init: (ProgressDialog.() -> Unit)? = null ) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, false, init) inline fun Fragment.indeterminateProgressDialog( title: Int? = null, message: Int? = null, noinline init: (ProgressDialog.() -> Unit)? = null ) = requireActivity().indeterminateProgressDialog(title, message, init) fun Context.indeterminateProgressDialog( title: Int? = null, message: Int? = null, init: (ProgressDialog.() -> Unit)? = null ) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, true, init) inline fun Fragment.progressDialog( title: CharSequence? = null, message: CharSequence? = null, noinline init: (ProgressDialog.() -> Unit)? = null ) = requireActivity().progressDialog(title, message, init) fun Context.progressDialog( title: CharSequence? = null, message: CharSequence? = null, init: (ProgressDialog.() -> Unit)? = null ) = progressDialog(title, message, false, init) inline fun Fragment.indeterminateProgressDialog( title: CharSequence? = null, message: CharSequence? = null, noinline init: (ProgressDialog.() -> Unit)? = null ) = requireActivity().indeterminateProgressDialog(title, message, init) fun Context.indeterminateProgressDialog( title: CharSequence? = null, message: CharSequence? = null, init: (ProgressDialog.() -> Unit)? = null ) = progressDialog(title, message, true, init) private fun Context.progressDialog( title: CharSequence? = null, message: CharSequence? = null, indeterminate: Boolean, init: (ProgressDialog.() -> Unit)? = null ) = ProgressDialog(this).apply { isIndeterminate = indeterminate if (!indeterminate) setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) if (message != null) setMessage(message) if (title != null) setTitle(title) if (init != null) init() show() } typealias AlertBuilderFactory = (Context) -> AlertBuilder inline fun Fragment.alert( noinline factory: AlertBuilderFactory, title: String? = null, message: String? = null, noinline init: (AlertBuilder.() -> Unit)? = null ) = activity?.alert(factory, title, message, init) fun Context.alert( factory: AlertBuilderFactory, title: String? = null, message: String? = null, init: (AlertBuilder.() -> Unit)? = null ): AlertBuilder { return factory(this).apply { if (title != null) { this.setTitle(title) } if (message != null) { this.setMessage(message) } if (init != null) init() } } inline fun Fragment.alert( noinline factory: AlertBuilderFactory, titleResource: Int? = null, messageResource: Int? = null, noinline init: (AlertBuilder.() -> Unit)? = null ) = requireActivity().alert(factory, titleResource, messageResource, init) fun Context.alert( factory: AlertBuilderFactory, titleResource: Int? = null, messageResource: Int? = null, init: (AlertBuilder.() -> Unit)? = null ): AlertBuilder { return factory(this).apply { if (titleResource != null) { this.setTitle(titleResource) } if (messageResource != null) { this.setMessage(messageResource) } if (init != null) init() } } inline fun Fragment.alert( noinline factory: AlertBuilderFactory, noinline init: AlertBuilder.() -> Unit ) = requireActivity().alert(factory, init) fun Context.alert( factory: AlertBuilderFactory, init: AlertBuilder.() -> Unit ): AlertBuilder = factory(this).apply { init() } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AndroidSelectors.kt ================================================ /* * Copyright 2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ @file:Suppress("NOTHING_TO_INLINE", "unused") package io.legado.app.lib.dialogs import android.content.Context import android.content.DialogInterface import com.kunfei.bookshelf.utils.dialogs.AndroidAlertBuilder fun Context.selector( items: List, onClick: (DialogInterface, Int) -> Unit ) { with(AndroidAlertBuilder(this)) { items(items, onClick) show() } } fun Context.selector( items: List, onClick: (DialogInterface, T, Int) -> Unit ) { with(AndroidAlertBuilder(this)) { items(items, onClick) show() } } fun Context.selector( title: CharSequence, items: List, onClick: (DialogInterface, Int) -> Unit ) { with(AndroidAlertBuilder(this)) { this.setTitle(title) items(items, onClick) show() } } fun Context.selector( title: CharSequence, items: List, onClick: (DialogInterface, T, Int) -> Unit ) { with(AndroidAlertBuilder(this)) { this.setTitle(title) items(items, onClick) show() } } fun Context.selector( titleSource: Int, items: List, onClick: (DialogInterface, Int) -> Unit ) { with(AndroidAlertBuilder(this)) { this.setTitle(titleSource) items(items, onClick) show() } } fun Context.selector( titleSource: Int, items: List, onClick: (DialogInterface, T, Int) -> Unit ) { with(AndroidAlertBuilder(this)) { this.setTitle(titleSource) items(items, onClick) show() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/dialogs/SelectItem.kt ================================================ package com.kunfei.bookshelf.utils.dialogs @Suppress("unused") data class SelectItem( val title: String, val value: T ) { override fun toString(): String { return title } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/download/DownloadUtils.java ================================================ package com.kunfei.bookshelf.utils.download; import androidx.annotation.NonNull; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.TimeUnit; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; public class DownloadUtils { private static final String TAG = "DownloadUtils"; private static final int DEFAULT_TIMEOUT = 15; private Retrofit retrofit; private JsDownloadListener listener; public DownloadUtils(String baseUrl, JsDownloadListener listener) { this.listener = listener; JsDownloadInterceptor mInterceptor = new JsDownloadInterceptor(listener); OkHttpClient httpClient = new OkHttpClient.Builder() .addInterceptor(mInterceptor) .retryOnConnectionFailure(true) .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) .build(); retrofit = new Retrofit.Builder() .baseUrl(baseUrl) .client(httpClient) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); } /** * 开始下载 */ public void download(@NonNull String url, final File file, Observer subscriber) { retrofit.create(Service.class) .download(url) .subscribeOn(Schedulers.io()) .unsubscribeOn(Schedulers.io()) .map(ResponseBody::byteStream) .observeOn(Schedulers.computation()) // 用于计算任务 .doOnNext(inputStream -> writeFile(inputStream, file)) .observeOn(AndroidSchedulers.mainThread()) .subscribe(subscriber); } /** * 将输入流写入文件 */ private void writeFile(InputStream inputString, File file) { if (file.exists()) { //noinspection ResultOfMethodCallIgnored file.delete(); } FileOutputStream fos = null; try { fos = new FileOutputStream(file); byte[] b = new byte[1024]; int len; while ((len = inputString.read(b)) != -1) { fos.write(b, 0, len); } inputString.close(); fos.close(); } catch (FileNotFoundException e) { listener.onFail("FileNotFoundException"); } catch (IOException e) { listener.onFail("IOException"); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/download/JsDownloadInterceptor.java ================================================ package com.kunfei.bookshelf.utils.download; import java.io.IOException; import okhttp3.Interceptor; import okhttp3.Response; public class JsDownloadInterceptor implements Interceptor { private JsDownloadListener downloadListener; public JsDownloadInterceptor(JsDownloadListener downloadListener) { this.downloadListener = downloadListener; } @Override public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(chain.request()); return response.newBuilder().body( new JsResponseBody(response.body(), downloadListener)).build(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/download/JsDownloadListener.java ================================================ package com.kunfei.bookshelf.utils.download; public interface JsDownloadListener { void onStartDownload(long length); void onProgress(int progress); void onFail(String errorInfo); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/download/JsResponseBody.java ================================================ package com.kunfei.bookshelf.utils.download; import java.io.IOException; import okhttp3.MediaType; import okhttp3.ResponseBody; import okio.Buffer; import okio.BufferedSource; import okio.ForwardingSource; import okio.Okio; import okio.Source; public class JsResponseBody extends ResponseBody { private ResponseBody responseBody; private JsDownloadListener downloadListener; // BufferedSource 是okio库中的输入流,这里就当作inputStream来使用。 private BufferedSource bufferedSource; public JsResponseBody(ResponseBody responseBody, JsDownloadListener downloadListener) { this.responseBody = responseBody; this.downloadListener = downloadListener; downloadListener.onStartDownload(responseBody.contentLength()); } @Override public MediaType contentType() { return responseBody.contentType(); } @Override public long contentLength() { return responseBody.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(source(responseBody.source())); } return bufferedSource; } private Source source(Source source) { return new ForwardingSource(source) { long totalBytesRead = 0L; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); totalBytesRead += bytesRead != -1 ? bytesRead : 0; if (null != downloadListener) { if (bytesRead != -1) { downloadListener.onProgress((int) (totalBytesRead * 100 / responseBody.contentLength())); } } return bytesRead; } }; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/download/Service.java ================================================ package com.kunfei.bookshelf.utils.download; import io.reactivex.Observable; import okhttp3.ResponseBody; import retrofit2.http.GET; import retrofit2.http.Streaming; import retrofit2.http.Url; public interface Service { @Streaming @GET Observable download(@Url String url); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/ATH.java ================================================ package com.kunfei.bookshelf.utils.theme; import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.content.res.ColorStateList; import android.os.Build; import android.view.View; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.Selector; /** * @author Karim Abou Zeid (kabouzeid) */ public final class ATH { @SuppressLint("CommitPrefEdits") public static boolean didThemeValuesChange(@NonNull Context context, long since) { return ThemeStore.isConfigured(context) && ThemeStore.prefs(context).getLong(ThemeStore.VALUES_CHANGED, -1) > since; } public static void setStatusbarColorAuto(Activity activity) { setStatusbarColor(activity, ThemeStore.statusBarColor(activity)); } public static void setStatusbarColor(Activity activity, int color) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { activity.getWindow().setStatusBarColor(color); setLightStatusbarAuto(activity, color); } } public static void setLightStatusbarAuto(Activity activity, int bgColor) { setLightStatusbar(activity, ColorUtils.isColorLight(bgColor)); } public static void setLightStatusbar(Activity activity, boolean enabled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final View decorView = activity.getWindow().getDecorView(); final int systemUiVisibility = decorView.getSystemUiVisibility(); if (enabled) { decorView.setSystemUiVisibility(systemUiVisibility | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); } else { decorView.setSystemUiVisibility(systemUiVisibility & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); } } } public static void setLightNavigationbar(Activity activity, boolean enabled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { final View decorView = activity.getWindow().getDecorView(); int systemUiVisibility = decorView.getSystemUiVisibility(); if (enabled) { systemUiVisibility |= SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } else { systemUiVisibility &= ~SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; } decorView.setSystemUiVisibility(systemUiVisibility); } } public static void setLightNavigationbarAuto(Activity activity, int bgColor) { setLightNavigationbar(activity, ColorUtils.isColorLight(bgColor)); } public static void setNavigationbarColorAuto(Activity activity) { setNavigationbarColor(activity, ThemeStore.navigationBarColor(activity)); } public static void setNavigationbarColor(Activity activity, int color) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { activity.getWindow().setNavigationBarColor(color); setLightNavigationbarAuto(activity, color); } } public static void setTaskDescriptionColorAuto(@NonNull Activity activity) { setTaskDescriptionColor(activity, ThemeStore.primaryColor(activity)); } public static void setTaskDescriptionColor(@NonNull Activity activity, @ColorInt int color) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Task description requires fully opaque color color = ColorUtils.stripAlpha(color); // Sets color of entry in the system recents page activity.setTaskDescription(new ActivityManager.TaskDescription((String) activity.getTitle(), null, color)); } } public static void setTint(@NonNull View view, @ColorInt int color) { TintHelper.setTintAuto(view, color, false); } public static void setBackgroundTint(@NonNull View view, @ColorInt int color) { TintHelper.setTintAuto(view, color, true); } public static AlertDialog setAlertDialogTint(@NonNull AlertDialog dialog) { ColorStateList colorStateList = Selector.colorBuild() .setDefaultColor(ThemeStore.accentColor(dialog.getContext())) .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(dialog.getContext()))) .create(); if (dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE) != null) { dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList); } if (dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE) != null) { dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList); } return dialog; } private ATH() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/ATHUtil.java ================================================ package com.kunfei.bookshelf.utils.theme; import android.content.Context; import android.content.res.TypedArray; import androidx.annotation.AttrRes; import com.kunfei.bookshelf.utils.ColorUtils; /** * @author Aidan Follestad (afollestad) */ public final class ATHUtil { public static boolean isWindowBackgroundDark(Context context) { return !ColorUtils.isColorLight(ATHUtil.resolveColor(context, android.R.attr.windowBackground)); } public static int resolveColor(Context context, @AttrRes int attr) { return resolveColor(context, attr, 0); } public static int resolveColor(Context context, @AttrRes int attr, int fallback) { TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); try { return a.getColor(0, fallback); } finally { a.recycle(); } } private ATHUtil() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/MaterialValueHelper.java ================================================ package com.kunfei.bookshelf.utils.theme; import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.ColorInt; import androidx.core.content.ContextCompat; import com.kunfei.bookshelf.R; /** * @author Karim Abou Zeid (kabouzeid) */ public final class MaterialValueHelper { @SuppressLint("PrivateResource") @ColorInt public static int getPrimaryTextColor(final Context context, boolean dark) { if (dark) { return ContextCompat.getColor(context, R.color.primary_text_default_material_light); } return ContextCompat.getColor(context, R.color.primary_text_default_material_dark); } @SuppressLint("PrivateResource") @ColorInt public static int getSecondaryTextColor(final Context context, boolean dark) { if (dark) { return ContextCompat.getColor(context, R.color.secondary_text_default_material_light); } return ContextCompat.getColor(context, R.color.secondary_text_default_material_dark); } @SuppressLint("PrivateResource") @ColorInt public static int getPrimaryDisabledTextColor(final Context context, boolean dark) { if (dark) { return ContextCompat.getColor(context, R.color.primary_text_disabled_material_light); } return ContextCompat.getColor(context, R.color.primary_text_disabled_material_dark); } @SuppressLint("PrivateResource") @ColorInt public static int getSecondaryDisabledTextColor(final Context context, boolean dark) { if (dark) { return ContextCompat.getColor(context, R.color.secondary_text_disabled_material_light); } return ContextCompat.getColor(context, R.color.secondary_text_disabled_material_dark); } private MaterialValueHelper() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/MaterialValueHelper.kt ================================================ @file:Suppress("unused") package com.kunfei.bookshelf.utils.theme import android.content.Context import android.graphics.drawable.GradientDrawable import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.kunfei.bookshelf.R import com.kunfei.bookshelf.utils.ColorUtils import com.kunfei.bookshelf.utils.dp /** * @author Karim Abou Zeid (kabouzeid) */ @ColorInt fun Context.getPrimaryTextColor(dark: Boolean): Int { return if (dark) { ContextCompat.getColor(this, R.color.primary_text_default_material_light) } else ContextCompat.getColor(this, R.color.primary_text_default_material_dark) } @ColorInt fun Context.getSecondaryTextColor(dark: Boolean): Int { return if (dark) { ContextCompat.getColor(this, R.color.secondary_text_default_material_light) } else { ContextCompat.getColor(this, R.color.secondary_text_default_material_dark) } } @ColorInt fun Context.getPrimaryDisabledTextColor(dark: Boolean): Int { return if (dark) { ContextCompat.getColor(this, R.color.primary_text_disabled_material_light) } else { ContextCompat.getColor(this, R.color.primary_text_disabled_material_dark) } } @ColorInt fun Context.getSecondaryDisabledTextColor(dark: Boolean): Int { return if (dark) { ContextCompat.getColor(this, R.color.secondary_text_disabled_material_light) } else { ContextCompat.getColor(this, R.color.secondary_text_disabled_material_dark) } } val Context.primaryColor: Int get() = ThemeStore.primaryColor(this) val Context.primaryColorDark: Int get() = ThemeStore.primaryColorDark(this) val Context.accentColor: Int get() = ThemeStore.accentColor(this) val Context.backgroundColor: Int get() = ThemeStore.backgroundColor(this) val Context.primaryTextColor: Int get() = getPrimaryTextColor(isDarkTheme) val Context.secondaryTextColor: Int get() = getSecondaryTextColor(isDarkTheme) val Context.primaryDisabledTextColor: Int get() = getPrimaryDisabledTextColor(isDarkTheme) val Context.secondaryDisabledTextColor: Int get() = getSecondaryDisabledTextColor(isDarkTheme) val Fragment.primaryColor: Int get() = ThemeStore.primaryColor(requireContext()) val Fragment.primaryColorDark: Int get() = ThemeStore.primaryColorDark(requireContext()) val Fragment.accentColor: Int get() = ThemeStore.accentColor(requireContext()) val Fragment.backgroundColor: Int get() = ThemeStore.backgroundColor(requireContext()) val Fragment.primaryTextColor: Int get() = requireContext().getPrimaryTextColor(isDarkTheme) val Fragment.secondaryTextColor: Int get() = requireContext().getSecondaryTextColor(isDarkTheme) val Fragment.primaryDisabledTextColor: Int get() = requireContext().getPrimaryDisabledTextColor(isDarkTheme) val Fragment.secondaryDisabledTextColor: Int get() = requireContext().getSecondaryDisabledTextColor(isDarkTheme) val Context.buttonDisabledColor: Int get() = if (isDarkTheme) { ContextCompat.getColor(this, R.color.md_dark_disabled) } else { ContextCompat.getColor(this, R.color.md_light_disabled) } val Context.isDarkTheme: Boolean get() = ColorUtils.isColorLight(ThemeStore.primaryColor(this)) val Fragment.isDarkTheme: Boolean get() = requireContext().isDarkTheme val Context.filletBackground: GradientDrawable get() { val background = GradientDrawable() background.cornerRadius = 3F.dp background.setColor(backgroundColor) return background } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/NavigationViewUtil.java ================================================ package com.kunfei.bookshelf.utils.theme; import android.content.res.ColorStateList; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import com.google.android.material.internal.NavigationMenuView; import com.google.android.material.navigation.NavigationView; /** * @author Karim Abou Zeid (kabouzeid) */ public final class NavigationViewUtil { public static void setItemIconColors(@NonNull NavigationView navigationView, @ColorInt int normalColor, @ColorInt int selectedColor) { final ColorStateList iconSl = new ColorStateList( new int[][]{ new int[]{-android.R.attr.state_checked}, new int[]{android.R.attr.state_checked} }, new int[]{ normalColor, selectedColor }); navigationView.setItemIconTintList(iconSl); } public static void setItemTextColors(@NonNull NavigationView navigationView, @ColorInt int normalColor, @ColorInt int selectedColor) { final ColorStateList textSl = new ColorStateList( new int[][]{ new int[]{-android.R.attr.state_checked}, new int[]{android.R.attr.state_checked} }, new int[]{ normalColor, selectedColor }); navigationView.setItemTextColor(textSl); } /** * 去掉navigationView的滚动条 * * @param navigationView NavigationView */ public static void disableScrollbar(NavigationView navigationView) { if (navigationView != null) { NavigationMenuView navigationMenuView = (NavigationMenuView) navigationView.getChildAt(0); if (navigationMenuView != null) { navigationMenuView.setVerticalScrollBarEnabled(false); } } } private NavigationViewUtil() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/ThemeStore.java ================================================ package com.kunfei.bookshelf.utils.theme; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; import androidx.annotation.AttrRes; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.ColorUtils; /** * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) */ public final class ThemeStore implements ThemeStorePrefKeys, ThemeStoreInterface { private final Context mContext; private final SharedPreferences.Editor mEditor; public static ThemeStore editTheme(@NonNull Context context) { return new ThemeStore(context); } @SuppressLint("CommitPrefEdits") private ThemeStore(@NonNull Context context) { mContext = context; mEditor = prefs(context).edit(); } @Override public ThemeStore primaryColor(@ColorInt int color) { mEditor.putInt(KEY_PRIMARY_COLOR, color); if (autoGeneratePrimaryDark(mContext)) primaryColorDark(ColorUtils.darkenColor(color)); return this; } @Override public ThemeStore primaryColorRes(@ColorRes int colorRes) { return primaryColor(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore primaryColorAttr(@AttrRes int colorAttr) { return primaryColor(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore primaryColorDark(@ColorInt int color) { mEditor.putInt(KEY_PRIMARY_COLOR_DARK, color); return this; } @Override public ThemeStore primaryColorDarkRes(@ColorRes int colorRes) { return primaryColorDark(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore primaryColorDarkAttr(@AttrRes int colorAttr) { return primaryColorDark(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore accentColor(@ColorInt int color) { mEditor.putInt(KEY_ACCENT_COLOR, color); return this; } @Override public ThemeStore accentColorRes(@ColorRes int colorRes) { return accentColor(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore accentColorAttr(@AttrRes int colorAttr) { return accentColor(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore statusBarColor(@ColorInt int color) { mEditor.putInt(KEY_STATUS_BAR_COLOR, color); return this; } @Override public ThemeStore statusBarColorRes(@ColorRes int colorRes) { return statusBarColor(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore statusBarColorAttr(@AttrRes int colorAttr) { return statusBarColor(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore navigationBarColor(@ColorInt int color) { mEditor.putInt(KEY_NAVIGATION_BAR_COLOR, color); return this; } @Override public ThemeStore navigationBarColorRes(@ColorRes int colorRes) { return navigationBarColor(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore navigationBarColorAttr(@AttrRes int colorAttr) { return navigationBarColor(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore textColorPrimary(@ColorInt int color) { mEditor.putInt(KEY_TEXT_COLOR_PRIMARY, color); return this; } @Override public ThemeStore textColorPrimaryRes(@ColorRes int colorRes) { return textColorPrimary(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore textColorPrimaryAttr(@AttrRes int colorAttr) { return textColorPrimary(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore textColorPrimaryInverse(@ColorInt int color) { mEditor.putInt(KEY_TEXT_COLOR_PRIMARY_INVERSE, color); return this; } @Override public ThemeStore textColorPrimaryInverseRes(@ColorRes int colorRes) { return textColorPrimaryInverse(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore textColorPrimaryInverseAttr(@AttrRes int colorAttr) { return textColorPrimaryInverse(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore textColorSecondary(@ColorInt int color) { mEditor.putInt(KEY_TEXT_COLOR_SECONDARY, color); return this; } @Override public ThemeStore textColorSecondaryRes(@ColorRes int colorRes) { return textColorSecondary(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore textColorSecondaryAttr(@AttrRes int colorAttr) { return textColorSecondary(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore textColorSecondaryInverse(@ColorInt int color) { mEditor.putInt(KEY_TEXT_COLOR_SECONDARY_INVERSE, color); return this; } @Override public ThemeStore textColorSecondaryInverseRes(@ColorRes int colorRes) { return textColorSecondaryInverse(ContextCompat.getColor(mContext, colorRes)); } @Override public ThemeStore textColorSecondaryInverseAttr(@AttrRes int colorAttr) { return textColorSecondaryInverse(ATHUtil.resolveColor(mContext, colorAttr)); } @Override public ThemeStore backgroundColor(int color) { mEditor.putInt(KEY_BACKGROUND_COLOR, color); return this; } @Override public ThemeStore coloredStatusBar(boolean colored) { mEditor.putBoolean(KEY_APPLY_PRIMARYDARK_STATUSBAR, colored); return this; } @Override public ThemeStore coloredNavigationBar(boolean applyToNavBar) { mEditor.putBoolean(KEY_APPLY_PRIMARY_NAVBAR, applyToNavBar); return this; } @Override public ThemeStore autoGeneratePrimaryDark(boolean autoGenerate) { mEditor.putBoolean(KEY_AUTO_GENERATE_PRIMARYDARK, autoGenerate); return this; } // Commit method @SuppressWarnings("unchecked") @Override public void apply() { mEditor.putLong(VALUES_CHANGED, System.currentTimeMillis()) .putBoolean(IS_CONFIGURED_KEY, true) .apply(); } // Static getters @CheckResult @NonNull protected static SharedPreferences prefs(@NonNull Context context) { return context.getSharedPreferences(CONFIG_PREFS_KEY_DEFAULT, Context.MODE_PRIVATE); } public static void markChanged(@NonNull Context context) { new ThemeStore(context).apply(); } @CheckResult @ColorInt public static int primaryColor(@NonNull Context context) { return prefs(context).getInt(KEY_PRIMARY_COLOR, ATHUtil.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64"))); } @CheckResult @ColorInt public static int primaryColorDark(@NonNull Context context) { return prefs(context).getInt(KEY_PRIMARY_COLOR_DARK, ATHUtil.resolveColor(context, R.attr.colorPrimaryDark, Color.parseColor("#37474F"))); } @CheckResult @ColorInt public static int accentColor(@NonNull Context context) { return prefs(context).getInt(KEY_ACCENT_COLOR, ATHUtil.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238"))); } @CheckResult @ColorInt public static int statusBarColor(@NonNull Context context) { if (!coloredStatusBar(context)) { return Color.BLACK; } return prefs(context).getInt(KEY_STATUS_BAR_COLOR, primaryColorDark(context)); } @CheckResult @ColorInt public static int navigationBarColor(@NonNull Context context) { if (!coloredNavigationBar(context)) { return Color.BLACK; } return prefs(context).getInt(KEY_NAVIGATION_BAR_COLOR, primaryColor(context)); } @CheckResult @ColorInt public static int textColorPrimary(@NonNull Context context) { return prefs(context).getInt(KEY_TEXT_COLOR_PRIMARY, ATHUtil.resolveColor(context, android.R.attr.textColorPrimary)); } @CheckResult @ColorInt public static int textColorPrimaryInverse(@NonNull Context context) { return prefs(context).getInt(KEY_TEXT_COLOR_PRIMARY_INVERSE, ATHUtil.resolveColor(context, android.R.attr.textColorPrimaryInverse)); } @CheckResult @ColorInt public static int textColorSecondary(@NonNull Context context) { return prefs(context).getInt(KEY_TEXT_COLOR_SECONDARY, ATHUtil.resolveColor(context, android.R.attr.textColorSecondary)); } @CheckResult @ColorInt public static int textColorSecondaryInverse(@NonNull Context context) { return prefs(context).getInt(KEY_TEXT_COLOR_SECONDARY_INVERSE, ATHUtil.resolveColor(context, android.R.attr.textColorSecondaryInverse)); } @CheckResult @ColorInt public static int backgroundColor(@NonNull Context context) { return prefs(context).getInt(KEY_BACKGROUND_COLOR, ATHUtil.resolveColor(context, android.R.attr.colorBackground)); } @CheckResult public static boolean coloredStatusBar(@NonNull Context context) { return prefs(context).getBoolean(KEY_APPLY_PRIMARYDARK_STATUSBAR, true); } @CheckResult public static boolean coloredNavigationBar(@NonNull Context context) { return prefs(context).getBoolean(KEY_APPLY_PRIMARY_NAVBAR, false); } @CheckResult public static boolean autoGeneratePrimaryDark(@NonNull Context context) { return prefs(context).getBoolean(KEY_AUTO_GENERATE_PRIMARYDARK, true); } @CheckResult public static boolean isConfigured(Context context) { return prefs(context).getBoolean(IS_CONFIGURED_KEY, false); } @SuppressLint("CommitPrefEdits") public static boolean isConfigured(Context context, @IntRange(from = 0, to = Integer.MAX_VALUE) int version) { final SharedPreferences prefs = prefs(context); final int lastVersion = prefs.getInt(IS_CONFIGURED_VERSION_KEY, -1); if (version > lastVersion) { prefs.edit().putInt(IS_CONFIGURED_VERSION_KEY, version).apply(); return false; } return true; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/ThemeStoreInterface.java ================================================ package com.kunfei.bookshelf.utils.theme; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; /** * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) */ interface ThemeStoreInterface { // Primary colors ThemeStore primaryColor(@ColorInt int color); ThemeStore primaryColorRes(@ColorRes int colorRes); ThemeStore primaryColorAttr(@AttrRes int colorAttr); ThemeStore autoGeneratePrimaryDark(boolean autoGenerate); ThemeStore primaryColorDark(@ColorInt int color); ThemeStore primaryColorDarkRes(@ColorRes int colorRes); ThemeStore primaryColorDarkAttr(@AttrRes int colorAttr); // Accent colors ThemeStore accentColor(@ColorInt int color); ThemeStore accentColorRes(@ColorRes int colorRes); ThemeStore accentColorAttr(@AttrRes int colorAttr); // Status bar color ThemeStore statusBarColor(@ColorInt int color); ThemeStore statusBarColorRes(@ColorRes int colorRes); ThemeStore statusBarColorAttr(@AttrRes int colorAttr); // Navigation bar color ThemeStore navigationBarColor(@ColorInt int color); ThemeStore navigationBarColorRes(@ColorRes int colorRes); ThemeStore navigationBarColorAttr(@AttrRes int colorAttr); // Primary text color ThemeStore textColorPrimary(@ColorInt int color); ThemeStore textColorPrimaryRes(@ColorRes int colorRes); ThemeStore textColorPrimaryAttr(@AttrRes int colorAttr); ThemeStore textColorPrimaryInverse(@ColorInt int color); ThemeStore textColorPrimaryInverseRes(@ColorRes int colorRes); ThemeStore textColorPrimaryInverseAttr(@AttrRes int colorAttr); // Secondary text color ThemeStore textColorSecondary(@ColorInt int color); ThemeStore textColorSecondaryRes(@ColorRes int colorRes); ThemeStore textColorSecondaryAttr(@AttrRes int colorAttr); ThemeStore textColorSecondaryInverse(@ColorInt int color); ThemeStore textColorSecondaryInverseRes(@ColorRes int colorRes); ThemeStore textColorSecondaryInverseAttr(@AttrRes int colorAttr); ThemeStore backgroundColor(@ColorInt int color); // Toggle configurations ThemeStore coloredStatusBar(boolean colored); ThemeStore coloredNavigationBar(boolean applyToNavBar); // Commit/apply void apply(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/ThemeStorePrefKeys.java ================================================ package com.kunfei.bookshelf.utils.theme; /** * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) */ interface ThemeStorePrefKeys { String CONFIG_PREFS_KEY_DEFAULT = "[[kabouzeid_app-theme-helper]]"; String IS_CONFIGURED_KEY = "is_configured"; String IS_CONFIGURED_VERSION_KEY = "is_configured_version"; String VALUES_CHANGED = "values_changed"; String KEY_PRIMARY_COLOR = "primary_color"; String KEY_PRIMARY_COLOR_DARK = "primary_color_dark"; String KEY_ACCENT_COLOR = "accent_color"; String KEY_STATUS_BAR_COLOR = "status_bar_color"; String KEY_NAVIGATION_BAR_COLOR = "navigation_bar_color"; String KEY_TEXT_COLOR_PRIMARY = "text_color_primary"; String KEY_TEXT_COLOR_PRIMARY_INVERSE = "text_color_primary_inverse"; String KEY_TEXT_COLOR_SECONDARY = "text_color_secondary"; String KEY_TEXT_COLOR_SECONDARY_INVERSE = "text_color_secondary_inverse"; String KEY_BACKGROUND_COLOR = "backgroundColor"; String KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar"; String KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar"; String KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark"; } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/TintHelper.java ================================================ package com.kunfei.bookshelf.utils.theme; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RadioButton; import android.widget.SeekBar; import android.widget.Switch; import android.widget.TextView; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SwitchCompat; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.ColorUtils; import java.lang.reflect.Field; /** * @author afollestad, plusCubed */ public final class TintHelper { @SuppressLint("PrivateResource") @ColorInt private static int getDefaultRippleColor(@NonNull Context context, boolean useDarkRipple) { // Light ripple is actually translucent black, and vice versa return ContextCompat.getColor(context, useDarkRipple ? R.color.ripple_material_light : R.color.ripple_material_dark); } @NonNull private static ColorStateList getDisabledColorStateList(@ColorInt int normal, @ColorInt int disabled) { return new ColorStateList(new int[][]{ new int[]{-android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled} }, new int[]{ disabled, normal }); } @SuppressWarnings("deprecation") public static void setTintSelector(@NonNull View view, @ColorInt final int color, final boolean darker, final boolean useDarkTheme) { final boolean isColorLight = ColorUtils.isColorLight(color); final int disabled = ContextCompat.getColor(view.getContext(), useDarkTheme ? R.color.ate_button_disabled_dark : R.color.ate_button_disabled_light); final int pressed = ColorUtils.shiftColor(color, darker ? 0.9f : 1.1f); final int activated = ColorUtils.shiftColor(color, darker ? 1.1f : 0.9f); final int rippleColor = getDefaultRippleColor(view.getContext(), isColorLight); final int textColor = ContextCompat.getColor(view.getContext(), isColorLight ? R.color.ate_primary_text_light : R.color.ate_primary_text_dark); final ColorStateList sl; if (view instanceof Button) { sl = getDisabledColorStateList(color, disabled); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && view.getBackground() instanceof RippleDrawable) { RippleDrawable rd = (RippleDrawable) view.getBackground(); rd.setColor(ColorStateList.valueOf(rippleColor)); } // Disabled text color state for buttons, may get overridden later by ATE tags final Button button = (Button) view; button.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(), useDarkTheme ? R.color.ate_button_text_disabled_dark : R.color.ate_button_text_disabled_light))); } else if (view instanceof FloatingActionButton) { // FloatingActionButton doesn't support disabled state? sl = new ColorStateList(new int[][]{ new int[]{-android.R.attr.state_pressed}, new int[]{android.R.attr.state_pressed} }, new int[]{ color, pressed }); final FloatingActionButton fab = (FloatingActionButton) view; fab.setRippleColor(rippleColor); fab.setBackgroundTintList(sl); if (fab.getDrawable() != null) fab.setImageDrawable(createTintedDrawable(fab.getDrawable(), textColor)); return; } else { sl = new ColorStateList( new int[][]{ new int[]{-android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, new int[]{android.R.attr.state_enabled, android.R.attr.state_activated}, new int[]{android.R.attr.state_enabled, android.R.attr.state_checked} }, new int[]{ disabled, color, pressed, activated, activated } ); } Drawable drawable = view.getBackground(); if (drawable != null) { drawable = createTintedDrawable(drawable, sl); ViewUtil.setBackgroundCompat(view, drawable); } if (view instanceof TextView && !(view instanceof Button)) { final TextView tv = (TextView) view; tv.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(), isColorLight ? R.color.ate_text_disabled_light : R.color.ate_text_disabled_dark))); } } public static void setTintAuto(final @NonNull View view, final @ColorInt int color, boolean background) { setTintAuto(view, color, background, ATHUtil.isWindowBackgroundDark(view.getContext())); } @SuppressWarnings("deprecation") public static void setTintAuto(final @NonNull View view, final @ColorInt int color, boolean background, final boolean isDark) { if (!background) { if (view instanceof RadioButton) setTint((RadioButton) view, color, isDark); else if (view instanceof SeekBar) setTint((SeekBar) view, color, isDark); else if (view instanceof ProgressBar) setTint((ProgressBar) view, color); else if (view instanceof AppCompatEditText) setTint((AppCompatEditText) view, color, isDark); else if (view instanceof CheckBox) setTint((CheckBox) view, color, isDark); else if (view instanceof ImageView) setTint((ImageView) view, color); else if (view instanceof Switch) setTint((Switch) view, color, isDark); else if (view instanceof SwitchCompat) setTint((SwitchCompat) view, color, isDark); else if (view instanceof SearchView) { int iconIdS[] = new int[]{androidx.appcompat.R.id.search_button, androidx.appcompat.R.id.search_close_btn,}; for (int iconId : iconIdS) { ImageView icon = view.findViewById(iconId); if (icon != null) { setTint(icon, color); } } } else { background = true; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !background && view.getBackground() instanceof RippleDrawable) { // Ripples for the above views (e.g. when you tap and hold a switch or checkbox) RippleDrawable rd = (RippleDrawable) view.getBackground(); @SuppressLint("PrivateResource") final int unchecked = ContextCompat.getColor(view.getContext(), isDark ? R.color.ripple_material_dark : R.color.ripple_material_light); final int checked = ColorUtils.adjustAlpha(color, 0.4f); final ColorStateList sl = new ColorStateList( new int[][]{ new int[]{-android.R.attr.state_activated, -android.R.attr.state_checked}, new int[]{android.R.attr.state_activated}, new int[]{android.R.attr.state_checked} }, new int[]{ unchecked, checked, checked } ); rd.setColor(sl); } } if (background) { // Need to tint the background of a view if (view instanceof FloatingActionButton || view instanceof Button) { setTintSelector(view, color, false, isDark); } else if (view.getBackground() != null) { Drawable drawable = view.getBackground(); if (drawable != null) { drawable = createTintedDrawable(drawable, color); ViewUtil.setBackgroundCompat(view, drawable); } } } } public static void setTint(@NonNull RadioButton radioButton, @ColorInt int color, boolean useDarker) { ColorStateList sl = new ColorStateList(new int[][]{ new int[]{-android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, new int[]{android.R.attr.state_enabled, android.R.attr.state_checked} }, new int[]{ // Radio button includes own alpha for disabled state ColorUtils.stripAlpha(ContextCompat.getColor(radioButton.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light)), ContextCompat.getColor(radioButton.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light), color }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { radioButton.setButtonTintList(sl); } else { Drawable d = createTintedDrawable(ContextCompat.getDrawable(radioButton.getContext(), R.drawable.abc_btn_radio_material), sl); radioButton.setButtonDrawable(d); } } public static void setTint(@NonNull SeekBar seekBar, @ColorInt int color, boolean useDarker) { final ColorStateList s1 = getDisabledColorStateList(color, ContextCompat.getColor(seekBar.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { seekBar.setThumbTintList(s1); seekBar.setProgressTintList(s1); } else { Drawable progressDrawable = createTintedDrawable(seekBar.getProgressDrawable(), s1); seekBar.setProgressDrawable(progressDrawable); Drawable thumbDrawable = createTintedDrawable(seekBar.getThumb(), s1); seekBar.setThumb(thumbDrawable); } } public static void setTint(@NonNull ProgressBar progressBar, @ColorInt int color) { setTint(progressBar, color, false); } public static void setTint(@NonNull ProgressBar progressBar, @ColorInt int color, boolean skipIndeterminate) { ColorStateList sl = ColorStateList.valueOf(color); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { progressBar.setProgressTintList(sl); progressBar.setSecondaryProgressTintList(sl); if (!skipIndeterminate) progressBar.setIndeterminateTintList(sl); } else { PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; if (!skipIndeterminate && progressBar.getIndeterminateDrawable() != null) progressBar.getIndeterminateDrawable().setColorFilter(color, mode); if (progressBar.getProgressDrawable() != null) progressBar.getProgressDrawable().setColorFilter(color, mode); } } @SuppressLint("RestrictedApi") public static void setTint(@NonNull AppCompatEditText editText, @ColorInt int color, boolean useDarker) { final ColorStateList editTextColorStateList = new ColorStateList(new int[][]{ new int[]{-android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled, -android.R.attr.state_pressed, -android.R.attr.state_focused}, new int[]{} }, new int[]{ ContextCompat.getColor(editText.getContext(), useDarker ? R.color.ate_text_disabled_dark : R.color.ate_text_disabled_light), ContextCompat.getColor(editText.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light), color }); editText.setSupportBackgroundTintList(editTextColorStateList); setCursorTint(editText, color); } public static void setTint(@NonNull CheckBox box, @ColorInt int color, boolean useDarker) { ColorStateList sl = new ColorStateList(new int[][]{ new int[]{-android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, new int[]{android.R.attr.state_enabled, android.R.attr.state_checked} }, new int[]{ ContextCompat.getColor(box.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light), ContextCompat.getColor(box.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light), color }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { box.setButtonTintList(sl); } else { Drawable drawable = createTintedDrawable(ContextCompat.getDrawable(box.getContext(), R.drawable.abc_btn_check_material), sl); box.setButtonDrawable(drawable); } } public static void setTint(@NonNull ImageView image, @ColorInt int color) { image.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); } private static Drawable modifySwitchDrawable(@NonNull Context context, @NonNull Drawable from, @ColorInt int tint, boolean thumb, boolean compatSwitch, boolean useDarker) { if (useDarker) { tint = ColorUtils.shiftColor(tint, 1.1f); } tint = ColorUtils.adjustAlpha(tint, (compatSwitch && !thumb) ? 0.5f : 1.0f); int disabled; int normal; if (thumb) { disabled = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_thumb_disabled_dark : R.color.ate_switch_thumb_disabled_light); normal = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_thumb_normal_dark : R.color.ate_switch_thumb_normal_light); } else { disabled = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_track_disabled_dark : R.color.ate_switch_track_disabled_light); normal = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_track_normal_dark : R.color.ate_switch_track_normal_light); } // Stock switch includes its own alpha if (!compatSwitch) { normal = ColorUtils.stripAlpha(normal); } final ColorStateList sl = new ColorStateList( new int[][]{ new int[]{-android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled, -android.R.attr.state_activated, -android.R.attr.state_checked}, new int[]{android.R.attr.state_enabled, android.R.attr.state_activated}, new int[]{android.R.attr.state_enabled, android.R.attr.state_checked} }, new int[]{ disabled, normal, tint, tint } ); return createTintedDrawable(from, sl); } public static void setTint(@NonNull Switch switchView, @ColorInt int color, boolean useDarker) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return; if (switchView.getTrackDrawable() != null) { switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(), switchView.getTrackDrawable(), color, false, false, useDarker)); } if (switchView.getThumbDrawable() != null) { switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(), switchView.getThumbDrawable(), color, true, false, useDarker)); } } public static void setTint(@NonNull SwitchCompat switchView, @ColorInt int color, boolean useDarker) { if (switchView.getTrackDrawable() != null) { switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(), switchView.getTrackDrawable(), color, false, true, useDarker)); } if (switchView.getThumbDrawable() != null) { switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(), switchView.getThumbDrawable(), color, true, true, useDarker)); } } // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise. @CheckResult @Nullable public static Drawable createTintedDrawable(@Nullable Drawable drawable, @ColorInt int color) { if (drawable == null) return null; drawable = DrawableCompat.wrap(drawable.mutate()); DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN); DrawableCompat.setTint(drawable, color); return drawable; } // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise. @CheckResult @Nullable public static Drawable createTintedDrawable(@Nullable Drawable drawable, @NonNull ColorStateList sl) { if (drawable == null) return null; drawable = DrawableCompat.wrap(drawable.mutate()); DrawableCompat.setTintList(drawable, sl); return drawable; } public static void setCursorTint(@NonNull EditText editText, @ColorInt int color) { try { Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); fCursorDrawableRes.setAccessible(true); int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); Field fEditor = TextView.class.getDeclaredField("mEditor"); fEditor.setAccessible(true); Object editor = fEditor.get(editText); Class clazz = editor.getClass(); Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); fCursorDrawable.setAccessible(true); Drawable[] drawables = new Drawable[2]; drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); drawables[0] = createTintedDrawable(drawables[0], color); drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); drawables[1] = createTintedDrawable(drawables[1], color); fCursorDrawable.set(editor, drawables); } catch (Exception ignored) { } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/theme/ViewUtil.java ================================================ package com.kunfei.bookshelf.utils.theme; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.view.View; import android.view.ViewTreeObserver; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.kunfei.bookshelf.utils.DrawableUtil; /** * @author Karim Abou Zeid (kabouzeid) */ public final class ViewUtil { @SuppressWarnings("deprecation") public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener) { v.getViewTreeObserver().removeOnGlobalLayoutListener(listener); } @SuppressWarnings("deprecation") public static void setBackgroundCompat(@NonNull View view, @Nullable Drawable drawable) { view.setBackground(drawable); } public static TransitionDrawable setBackgroundTransition(@NonNull View view, @NonNull Drawable newDrawable) { TransitionDrawable transition = DrawableUtil.createTransitionDrawable(view.getBackground(), newDrawable); setBackgroundCompat(view, transition); return transition; } public static TransitionDrawable setBackgroundColorTransition(@NonNull View view, @ColorInt int newColor) { final Drawable oldColor = view.getBackground(); Drawable start = oldColor != null ? oldColor : new ColorDrawable(view.getSolidColor()); Drawable end = new ColorDrawable(newColor); TransitionDrawable transition = DrawableUtil.createTransitionDrawable(start, end); setBackgroundCompat(view, transition); return transition; } private ViewUtil() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/viewbindingdelegate/ActivityViewBindings.kt ================================================ @file:Suppress("RedundantVisibilityModifier", "unused") package com.kunfei.bookshelf.utils.viewbindingdelegate import android.view.LayoutInflater import androidx.core.app.ComponentActivity import androidx.viewbinding.ViewBinding /** * Create new [ViewBinding] associated with the [ComponentActivity] */ @JvmName("viewBindingActivity") inline fun ComponentActivity.viewBinding( crossinline bindingInflater: (LayoutInflater) -> T, setContentView: Boolean = false ) = lazy(LazyThreadSafetyMode.SYNCHRONIZED) { val binding = bindingInflater.invoke(layoutInflater) if (setContentView) { setContentView(binding.root) } binding } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/viewbindingdelegate/FragmentViewBindings.kt ================================================ @file:Suppress("RedundantVisibilityModifier", "unused") @file:JvmName("ReflectionFragmentViewBindings") package com.kunfei.bookshelf.utils.viewbindingdelegate import android.view.View import androidx.annotation.IdRes import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding private class FragmentViewBindingProperty( viewBinder: (F) -> T ) : ViewBindingProperty(viewBinder) { override fun getLifecycleOwner(thisRef: F) = thisRef.viewLifecycleOwner } /** * Create new [ViewBinding] associated with the [Fragment] */ @JvmName("viewBindingFragment") public fun Fragment.viewBinding(viewBinder: (F) -> T): ViewBindingProperty { return FragmentViewBindingProperty(viewBinder) } /** * Create new [ViewBinding] associated with the [Fragment] * * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used * @param viewProvider Provide a [View] from the Fragment. By default call [Fragment.requireView] */ @JvmName("viewBindingFragment") public inline fun Fragment.viewBinding( crossinline vbFactory: (View) -> T, crossinline viewProvider: (F) -> View = Fragment::requireView ): ViewBindingProperty { return viewBinding { fragment: F -> vbFactory(viewProvider(fragment)) } } /** * Create new [ViewBinding] associated with the [Fragment] * * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used * @param viewBindingRootId Root view's id that will be used as root for the view binding */ @JvmName("viewBindingFragment") public inline fun Fragment.viewBinding( crossinline vbFactory: (View) -> T, @IdRes viewBindingRootId: Int ): ViewBindingProperty { return viewBinding(vbFactory) { fragment: Fragment -> fragment.requireView().findViewById(viewBindingRootId) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/viewbindingdelegate/ViewBindingProperty.kt ================================================ @file:Suppress("RedundantVisibilityModifier") package com.kunfei.bookshelf.utils.viewbindingdelegate import android.os.Handler import android.os.Looper import androidx.annotation.MainThread import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.viewbinding.ViewBinding import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty public abstract class ViewBindingProperty( private val viewBinder: (R) -> T ) : ReadOnlyProperty { private var viewBinding: T? = null private val lifecycleObserver = ClearOnDestroyLifecycleObserver() private var thisRef: R? = null protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner @MainThread public override fun getValue(thisRef: R, property: KProperty<*>): T { viewBinding?.let { return it } this.thisRef = thisRef val lifecycle = getLifecycleOwner(thisRef).lifecycle if (lifecycle.currentState == Lifecycle.State.DESTROYED) { mainHandler.post { viewBinding = null } } else { lifecycle.addObserver(lifecycleObserver) } return viewBinder(thisRef).also { viewBinding = it } } @MainThread public fun clear() { val thisRef = thisRef ?: return this.thisRef = null getLifecycleOwner(thisRef).lifecycle.removeObserver(lifecycleObserver) mainHandler.post { viewBinding = null } } private inner class ClearOnDestroyLifecycleObserver : DefaultLifecycleObserver { @MainThread override fun onDestroy(owner: LifecycleOwner): Unit = clear() } private companion object { private val mainHandler = Handler(Looper.getMainLooper()) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/webdav/README.md ================================================ ## 用于网络备份的WebDav ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/webdav/WebDav.kt ================================================ package com.kunfei.bookshelf.utils.webdav import com.kunfei.bookshelf.base.BaseModelImpl import com.kunfei.bookshelf.utils.webdav.http.Handler import com.kunfei.bookshelf.utils.webdav.http.HttpAuth import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import org.jsoup.Jsoup 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", requestPropsStr.toRequestBody("text/plain".toMediaType())) HttpAuth.auth?.let { request.header( "Authorization", Credentials.basic(it.user, it.pass) ) } request.header("Depth", if (depth < 0) "infinity" else depth.toString()) return BaseModelImpl.getClient().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 = contentType?.toMediaType() // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 val fileBody = file.asRequestBody(mediaType) 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 = BaseModelImpl.getClient().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 BaseModelImpl.getClient().newCall(request.build()) .execute().body?.byteStream() } catch (e: IOException) { e.printStackTrace() } catch (e: IllegalArgumentException) { e.printStackTrace() } } return null } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/webdav/http/Handler.kt ================================================ package com.kunfei.bookshelf.utils.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 } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/utils/webdav/http/HttpAuth.kt ================================================ package com.kunfei.bookshelf.utils.webdav.http object HttpAuth { var auth: Auth? = null class Auth internal constructor(val user: String, val pass: String) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/AboutActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.view.KeyEvent; import android.view.MenuItem; import android.widget.PopupMenu; import androidx.appcompat.app.ActionBar; import com.google.zxing.EncodeHintType; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.databinding.ActivityAboutBinding; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.widget.modialog.MoDialogHUD; import cn.bingoogolapple.qrcode.zxing.QRCodeEncoder; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; /** * Created by GKF on 2017/12/15. * 关于 */ public class AboutActivity extends MBaseActivity { private MoDialogHUD moDialogHUD; private String[] allQQ = new String[]{"(公众号)开源阅读", "(QQ群)701903217", "(QQ群)805192012", "(QQ群)773736122", "(QQ群)981838750"}; private ActivityAboutBinding binding; public static void startThis(Context context) { Intent intent = new Intent(context, AboutActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } @Override protected IPresenter initInjector() { return null; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityAboutBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void initData() { moDialogHUD = new MoDialogHUD(this); } @Override protected void bindView() { this.setSupportActionBar(binding.toolbar); setupActionBar(); binding.tvVersion.setText(getString(R.string.version_name, MApplication.getVersionName())); } @Override protected void bindEvent() { binding.vwDonate.setOnClickListener(view -> DonateActivity.startThis(this)); binding.vwScoring.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, "market://details?id=" + getPackageName())); binding.vwMail.setOnClickListener(view -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com")); binding.vwGit.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, getString(R.string.this_github_url))); binding.vwDisclaimer.setOnClickListener(view -> moDialogHUD.showAssetMarkdown("disclaimer.md")); binding.vwUpdate.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, getString(R.string.latest_release_url))); binding.vwHomePage.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, getString(R.string.home_page_url))); binding.vwQq.setOnClickListener(view -> { PopupMenu popupMenu = new PopupMenu(AboutActivity.this, view); for (String qq : allQQ) { popupMenu.getMenu().add(qq); } popupMenu.setOnMenuItemClickListener(menuItem -> { joinGroup(menuItem.getTitle().toString()); return true; }); popupMenu.show(); }); binding.vwUpdateLog.setOnClickListener(view -> moDialogHUD.showAssetMarkdown("updateLog.md")); binding.vwFaq.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, "https://mp.weixin.qq.com/s?__biz=MzU2NjU0NjM1Mg==&mid=100000032&idx=1&sn=53e52168caf1ad9e507ab56381c45f1f&chksm=7cab9bff4bdc12e925e282effc1d4993a8652c248abc6169bd31d6fac133628fad54cf516043&mpshare=1&scene=1&srcid=0321CjdEk21qy8WjDgZ0I6sW&key=08039a5457341b11b054342370cc5462829ae3b54e4b265c42e28361773a6fa0e3105d706160d75b097b3ae41148dda265e2416b88f6b6a2391c1f33ec9f0bc62ea9edc86b75344494b598842ad620ac&ascene=1&uin=NzUwMTUxNzIx&devicetype=Windows+10&version=62060739&lang=zh_CN&pass_ticket=%2FD6keuc%2Fx%2Ba8YhupUUvefch8Gm07zVHa34Df5m1waxWQuCOohBN70NNcDEJsKE%2BV")); binding.vwShare.setOnClickListener(view -> { String url = "https://www.coolapk.com/apk/com.gedoor.monkeybook"; Single.create((SingleOnSubscribe) emitter -> { QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); Bitmap bitmap = QRCodeEncoder.syncEncodeQRCode(url, 600); emitter.onSuccess(bitmap); }).compose(RxUtils::toSimpleSingle) .subscribe(new MySingleObserver() { @Override public void onSuccess(Bitmap bitmap) { if (bitmap != null) { moDialogHUD.showImageText(bitmap, url); } } }); }); } private void joinGroup(String name) { String key; if (name.equals(allQQ[1])) { key = "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"; if (joinQQGroupError(key)) { copyName(name.substring(5)); } } else if (name.equals(allQQ[2])) { key = "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"; if (joinQQGroupError(key)) { copyName(name.substring(5)); } } else if (name.equals(allQQ[3])) { key = "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"; if (joinQQGroupError(key)) { copyName(name.substring(5)); } } else if (name.equals(allQQ[4])) { key = "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"; if (joinQQGroupError(key)) { copyName(name.substring(5)); } } else { copyName(name.substring(5)); } } private void copyName(String name) { ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = ClipData.newPlainText(null, name); if (clipboard != null) { clipboard.setPrimaryClip(clipData); toast(R.string.copy_complete); } } private boolean joinQQGroupError(String key) { Intent intent = new Intent(); intent.setData(Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D" + key)); // 此Flag可根据具体产品需要自定义,如设置,则在加群界面按返回,返回手Q主界面,不设置,按返回会返回到呼起产品界面 //intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) try { startActivity(intent); return false; } catch (Exception e) { return true; } } void openIntent(String intentName, String address) { try { Intent intent = new Intent(intentName); intent.setData(Uri.parse(address)); startActivity(intent); } catch (Exception e) { toast(R.string.can_not_open, ERROR); } } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.about); } } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Boolean mo = moDialogHUD.onKeyDown(keyCode, event); return mo || super.onKeyDown(keyCode, event); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/BookCoverEditActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Intent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.dao.SearchBookBeanDao; import com.kunfei.bookshelf.databinding.ActivityBookCoverEditBinding; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.SearchBookModel; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter; import java.util.ArrayList; import java.util.List; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; public class BookCoverEditActivity extends MBaseActivity { private ActivityBookCoverEditBinding binding; private SearchBookModel searchBookModel; private String name; private String author; private Boolean isLoading = true; private List urls = new ArrayList<>(); private List origins = new ArrayList<>(); @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityBookCoverEditBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.cover_change_source); } } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } @Override protected void initData() { name = getIntent().getStringExtra("name"); author = getIntent().getStringExtra("author"); ChangeCoverAdapter changeCoverAdapter = new ChangeCoverAdapter(); binding.rfRvChangeCover.setLayoutManager(new GridLayoutManager(this, 3)); binding.rfRvChangeCover.setAdapter(changeCoverAdapter); SearchBookModel.OnSearchListener searchListener = new SearchBookModel.OnSearchListener() { @Override public void refreshSearchBook() { binding.swipeRefreshLayout.setRefreshing(true); } @Override public void refreshFinish(Boolean value) { binding.swipeRefreshLayout.setRefreshing(false); isLoading = false; } @Override public void loadMoreFinish(Boolean value) { if (value) { isLoading = false; } } @Override public void loadMoreSearchBook(List value) { if (!value.isEmpty()) { SearchBookBean bookBean = value.get(0); if (bookBean.getName().equals(name) && bookBean.getCoverUrl() != null && !urls.contains(bookBean.getCoverUrl())) { urls.add(bookBean.getCoverUrl()); origins.add(bookBean.getOrigin()); changeCoverAdapter.notifyItemChanged(urls.size() - 1); } } } @Override public void searchBookError(Throwable throwable) { binding.swipeRefreshLayout.setRefreshing(false); isLoading = false; } @Override public int getItemCount() { return 0; } }; searchBookModel = new SearchBookModel(searchListener); binding.swipeRefreshLayout.setColorSchemeColors(ThemeStore.accentColor(MApplication.getInstance())); binding.swipeRefreshLayout.setOnRefreshListener(() -> { if (!isLoading) { isLoading = true; long time = System.currentTimeMillis(); searchBookModel.setSearchTime(time); searchBookModel.search(name, time, new ArrayList<>(), false); } }); Single.create((SingleOnSubscribe) e -> { List searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder() .where(SearchBookBeanDao.Properties.Name.eq(name), SearchBookBeanDao.Properties.Author.eq(author), SearchBookBeanDao.Properties.CoverUrl.isNotNull()) .build().list(); for (SearchBookBean searchBook : searchBookBeans) { BookSourceBean bean = BookSourceManager.getBookSourceByUrl(searchBook.getTag()); if (bean != null) { String url = searchBook.getCoverUrl(); if (url != null && !urls.contains(url)) { urls.add(url); origins.add(searchBook.getOrigin()); } } } e.onSuccess(true); }).compose(RxUtils::toSimpleSingle) .subscribe(new MySingleObserver() { @Override public void onSuccess(Boolean aBoolean) { if (urls.isEmpty()) { binding.swipeRefreshLayout.setRefreshing(true); long time = System.currentTimeMillis(); searchBookModel.setSearchTime(time); searchBookModel.search(name, time, new ArrayList<>(), false); } else { changeCoverAdapter.notifyDataSetChanged(); isLoading = false; } } }); } @Override protected void onDestroy() { super.onDestroy(); searchBookModel.onDestroy(); } @Override protected IPresenter initInjector() { return null; } public class ChangeCoverAdapter extends RefreshRecyclerViewAdapter { ChangeCoverAdapter() { super(false); } @Override public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_change_cover, parent, false)); } @Override public void onBindIViewHolder(RecyclerView.ViewHolder holder, int position) { MyViewHolder myViewHolder = (MyViewHolder) holder; myViewHolder.bind(urls.get(position), origins.get(position), holder); } @Override public int getIViewType(int position) { return 0; } @Override public int getICount() { return urls.size(); } class MyViewHolder extends RecyclerView.ViewHolder { ImageView ivCover; TextView tvSourceName; MyViewHolder(View itemView) { super(itemView); ivCover = itemView.findViewById(R.id.iv_cover); tvSourceName = itemView.findViewById(R.id.tv_source_name); } public void bind(String url, String origin, RecyclerView.ViewHolder holder) { tvSourceName.setText(origin); Glide.with(holder.itemView.getContext()) .load(url) .error(R.drawable.image_cover_default) .into(ivCover); ivCover.setOnClickListener(view -> { Intent intent = new Intent(); intent.putExtra("url", url); setResult(RESULT_OK, intent); finish(); }); } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/BookDetailActivity.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.activity; import static com.kunfei.bookshelf.presenter.BookDetailPresenter.FROM_BOOKSHELF; import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.View; import android.widget.PopupMenu; import android.widget.RadioButton; import androidx.annotation.NonNull; import androidx.core.content.FileProvider; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.bumptech.glide.request.RequestOptions; import com.google.zxing.EncodeHintType; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.AppActivityManager; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.BuildConfig; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.ActivityBookDetailBinding; import com.kunfei.bookshelf.help.BlurTransformation; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.glide.ImageLoader; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.presenter.BookDetailPresenter; import com.kunfei.bookshelf.presenter.ReadBookPresenter; import com.kunfei.bookshelf.presenter.contract.BookDetailContract; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.widget.modialog.ChangeSourceDialog; import com.kunfei.bookshelf.widget.modialog.MoDialogHUD; import java.io.File; import java.io.FileOutputStream; import cn.bingoogolapple.qrcode.zxing.QRCodeEncoder; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; public class BookDetailActivity extends MBaseActivity implements BookDetailContract.View { private ActivityBookDetailBinding binding; private MoDialogHUD moDialogHUD; private String author; private BookShelfBean bookShelfBean; private String coverPath; private String bookUrl; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected BookDetailContract.Presenter initInjector() { return new BookDetailPresenter(); } @Override protected void onCreateActivity() { setTheme(R.style.CAppTransparentTheme); binding = ActivityBookDetailBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void initData() { mPresenter.initData(getIntent()); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); String dataKey = String.valueOf(System.currentTimeMillis()); getIntent().putExtra("openFrom", FROM_BOOKSHELF); getIntent().putExtra("data_key", dataKey); BitIntentDataManager.getInstance().putData(dataKey, mPresenter.getBookShelf()); } @Override protected void bindView() { //弹窗 moDialogHUD = new MoDialogHUD(this); binding.tvIntro.setMovementMethod(ScrollingMovementMethod.getInstance()); if (mPresenter.getOpenFrom() == FROM_BOOKSHELF) { updateView(); } else { if (mPresenter.getSearchBook() == null) return; SearchBookBean searchBookBean = mPresenter.getSearchBook(); upImageView(searchBookBean.getCoverUrl(), searchBookBean.getName(), searchBookBean.getAuthor()); binding.tvName.setText(searchBookBean.getName()); author = searchBookBean.getAuthor(); binding.tvAuthor.setText(TextUtils.isEmpty(author) ? "未知" : author); bookUrl = searchBookBean.getNoteUrl(); String origin = TextUtils.isEmpty(searchBookBean.getOrigin()) ? "未知" : searchBookBean.getOrigin(); binding.tvOrigin.setText(origin); binding.tvChapter.setText(searchBookBean.getLastChapter()); // newest binding.tvIntro.setText(StringUtils.formatHtml2Intor(searchBookBean.getIntroduce())); binding.tvShelf.setText(R.string.add_to_shelf); binding.tvRead.setText(R.string.start_read); binding.tvRead.setOnClickListener(v -> { //放入书架 }); binding.tvIntro.setVisibility(View.INVISIBLE); binding.tvLoading.setVisibility(View.VISIBLE); binding.tvLoading.setText(R.string.loading); binding.tvLoading.setOnClickListener(null); } } @Override public void updateView() { bookShelfBean = mPresenter.getBookShelf(); BookInfoBean bookInfoBean; if (null != bookShelfBean) { if (BookShelfBean.LOCAL_TAG.equals(bookShelfBean.getTag())) { binding.ivMenu.setVisibility(View.GONE); } else { binding.ivMenu.setVisibility(View.VISIBLE); } bookInfoBean = bookShelfBean.getBookInfoBean(); binding.tvName.setText(bookInfoBean.getName()); author = bookInfoBean.getAuthor(); binding.tvAuthor.setText(TextUtils.isEmpty(author) ? "未知" : author); bookUrl = bookInfoBean.getNoteUrl(); ((RadioButton) binding.rgBookGroup.getChildAt(bookShelfBean.getGroup())).setChecked(true); if (mPresenter.getInBookShelf()) { binding.tvChapter.setText(bookShelfBean.getDurChapterName()); // last binding.tvShelf.setText(R.string.remove_from_bookshelf); binding.tvRead.setText(R.string.continue_read); binding.tvShelf.setOnClickListener(v -> { //从书架移出 mPresenter.removeFromBookShelf(); }); } else { if (!TextUtils.isEmpty(bookShelfBean.getLastChapterName())) { binding.tvChapter.setText(bookShelfBean.getLastChapterName()); // last } binding.tvShelf.setText(R.string.add_to_shelf); binding.tvRead.setText(R.string.start_read); binding.tvShelf.setOnClickListener(v -> { //放入书架 mPresenter.addToBookShelf(); }); } binding.tvIntro.setText(StringUtils.formatHtml2Intor(bookInfoBean.getIntroduce())); if (binding.tvIntro.getVisibility() != View.VISIBLE) { binding.tvIntro.setVisibility(View.VISIBLE); } String origin = bookInfoBean.getOrigin(); if (!TextUtils.isEmpty(origin)) { binding.ivWeb.setVisibility(View.VISIBLE); binding.tvOrigin.setText(origin); } else { binding.ivWeb.setVisibility(View.INVISIBLE); binding.tvOrigin.setVisibility(View.INVISIBLE); } if (!TextUtils.isEmpty(bookShelfBean.getCustomCoverPath())) { upImageView(bookShelfBean.getCustomCoverPath(), bookInfoBean.getName(), bookInfoBean.getAuthor()); } else { upImageView(bookInfoBean.getCoverUrl(), bookInfoBean.getName(), bookInfoBean.getAuthor()); } if (bookShelfBean.getTag().equals(BookShelfBean.LOCAL_TAG)) { binding.tvChangeOrigin.setVisibility(View.INVISIBLE); } else { binding.tvChangeOrigin.setVisibility(View.VISIBLE); } upChapterSizeTv(); } binding.tvLoading.setVisibility(View.GONE); binding.tvLoading.setOnClickListener(null); } @Override public void getBookShelfError() { binding.tvLoading.setVisibility(View.VISIBLE); binding.tvLoading.setText(R.string.load_error_retry); binding.tvLoading.setOnClickListener(v -> { binding.tvLoading.setText(R.string.loading); binding.tvLoading.setOnClickListener(null); mPresenter.getBookShelfInfo(); }); } private void upImageView(String path, String name, String author) { binding.ivCover.load(path, name, author); ImageLoader.INSTANCE.load(this, path) .transition(DrawableTransitionOptions.withCrossFade(1500)) .thumbnail(defaultCover()) .centerCrop() .apply(RequestOptions.bitmapTransform(new BlurTransformation(this, 25))) .into(binding.ivBlurCover); //模糊、渐变、缩小效果 } private RequestBuilder defaultCover() { return ImageLoader.INSTANCE.load(this, R.drawable.image_cover_default) .apply(RequestOptions.bitmapTransform(new BlurTransformation(this, 25))); } private void refresh() { binding.tvLoading.setVisibility(View.VISIBLE); binding.tvLoading.setText(R.string.loading); binding.tvLoading.setOnClickListener(null); mPresenter.getBookShelf().getBookInfoBean().setBookInfoHtml(null); mPresenter.getBookShelf().getBookInfoBean().setChapterListHtml(null); mPresenter.getBookShelfInfo(); } @SuppressLint("ClickableViewAccessibility") @Override protected void bindEvent() { binding.tvName.setOnClickListener(v -> { if (bookShelfBean == null) return; if (TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getName())) return; if (!AppActivityManager.getInstance().isExist(SearchBookActivity.class)) { SearchBookActivity.startByKey(this, bookShelfBean.getBookInfoBean().getName()); } else { RxBus.get().post(RxBusTag.SEARCH_BOOK, bookShelfBean.getBookInfoBean().getName()); } finish(); }); binding.ivBlurCover.setOnClickListener(null); binding.iflContent.setOnClickListener(v -> finish()); binding.tvToc.setOnClickListener(v -> { ChapterListActivity.startThis(this, mPresenter.getBookShelf(), mPresenter.getChapterList()); }); binding.tvChangeOrigin.setOnClickListener(view -> ChangeSourceDialog.builder(BookDetailActivity.this, mPresenter.getBookShelf()) .setCallback(searchBookBean -> { binding.tvOrigin.setText(searchBookBean.getOrigin()); binding.tvLoading.setVisibility(View.VISIBLE); binding.tvLoading.setText(R.string.loading); binding.tvLoading.setOnClickListener(null); if (mPresenter.getOpenFrom() == FROM_BOOKSHELF) { mPresenter.changeBookSource(searchBookBean); } else { mPresenter.initBookFormSearch(searchBookBean); mPresenter.getBookShelfInfo(); } }).show()); binding.tvRead.setOnClickListener(v -> readBook()); binding.ivMenu.setOnClickListener(view -> { PopupMenu popupMenu = new PopupMenu(this, view, Gravity.END); if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) { popupMenu.getMenu().add(Menu.NONE, R.id.menu_refresh, Menu.NONE, R.string.refresh); } if (mPresenter.getInBookShelf() && !mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) { if (mPresenter.getBookShelf().getAllowUpdate()) { popupMenu.getMenu().add(Menu.NONE, R.id.menu_disable_update, Menu.NONE, R.string.disable_update); } else { popupMenu.getMenu().add(Menu.NONE, R.id.menu_allow_update, Menu.NONE, R.string.allow_update); } } if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) { popupMenu.getMenu().add(Menu.NONE, R.id.menu_edit, Menu.NONE, R.string.edit_book_source); } if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) { popupMenu.getMenu().add(Menu.NONE, R.id.menu_copy_url, Menu.NONE, R.string.copy_url); } popupMenu.getMenu().add(Menu.NONE, R.id.menu_share, Menu.NONE, R.string.share_book); popupMenu.setOnMenuItemClickListener(menuItem -> { int itemId = menuItem.getItemId(); if (itemId == R.id.menu_refresh) { refresh(); } else if (itemId == R.id.menu_allow_update) { mPresenter.getBookShelf().setAllowUpdate(true); mPresenter.addToBookShelf(); } else if (itemId == R.id.menu_disable_update) { mPresenter.getBookShelf().setAllowUpdate(false); mPresenter.addToBookShelf(); } else if (itemId == R.id.menu_edit) { BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(mPresenter.getBookShelf().getTag()); if (sourceBean != null) { SourceEditActivity.startThis(this, sourceBean); } } else if (itemId == R.id.menu_copy_url) { ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = ClipData.newPlainText(null, mPresenter.getBookShelf().getNoteUrl()); if (clipboard != null) { clipboard.setPrimaryClip(clipData); toast(R.string.copy_complete); } } else if (itemId == R.id.menu_share) { share(); } return true; }); popupMenu.show(); }); binding.ivCover.setOnClickListener(view -> { if (mPresenter.getOpenFrom() == FROM_BOOKSHELF) { BookInfoEditActivity.startThis(this, mPresenter.getBookShelf().getNoteUrl()); } }); binding.tvAuthor.setOnClickListener(view -> { if (TextUtils.isEmpty(author)) return; if (!AppActivityManager.getInstance().isExist(SearchBookActivity.class)) { SearchBookActivity.startByKey(this, author); } else { RxBus.get().post(RxBusTag.SEARCH_BOOK, author); } finish(); }); binding.rgBookGroup.setOnCheckedChangeListener((radioGroup, i) -> { View checkView = radioGroup.findViewById(i); if (!checkView.isPressed()) { return; } int idx = radioGroup.indexOfChild(checkView) % (getResources().getStringArray(R.array.book_group_array).length - 1); mPresenter.getBookShelf().setGroup(idx); if (mPresenter.getInBookShelf()) { mPresenter.addToBookShelf(); } }); } @Override protected void firstRequest() { super.firstRequest(); if (mPresenter.getOpenFrom() == BookDetailPresenter.FROM_SEARCH) { //网络请求 mPresenter.getBookShelfInfo(); } } @Override public void readBook() { if (!mPresenter.getInBookShelf()) { BookshelfHelp.saveBookToShelf(mPresenter.getBookShelf()); if (mPresenter.getChapterList() != null) DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(mPresenter.getChapterList()); } Intent intent = new Intent(BookDetailActivity.this, ReadBookActivity.class); intent.putExtra("openFrom", ReadBookPresenter.OPEN_FROM_APP); intent.putExtra("inBookshelf", mPresenter.getInBookShelf()); String key = String.valueOf(System.currentTimeMillis()); String bookKey = "book" + key; intent.putExtra("bookKey", bookKey); BitIntentDataManager.getInstance().putData(bookKey, mPresenter.getBookShelf().clone()); startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (getStart_share_ele()) { finishAfterTransition(); } else { finish(); overridePendingTransition(0, android.R.anim.fade_out); } } else { finish(); overridePendingTransition(0, android.R.anim.fade_out); } } @SuppressLint("DefaultLocale") private void upChapterSizeTv() { String chapterSize = ""; if (mPresenter.getOpenFrom() == FROM_BOOKSHELF && bookShelfBean.getChapterListSize() > 0) { int newChapterNum = bookShelfBean.getChapterListSize() - 1 - bookShelfBean.getDurChapter(); if (newChapterNum > 0) chapterSize = String.format("(+%d)", newChapterNum); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Boolean mo = moDialogHUD.onKeyDown(keyCode, event); if (mo) return true; return super.onKeyDown(keyCode, event); } @Override public void finish() { super.finish(); overridePendingTransition(0, android.R.anim.fade_out); } @Override public void onDestroy() { moDialogHUD.dismiss(); super.onDestroy(); } private void share() { Single.create((SingleOnSubscribe) emitter -> { // 使用url String url = mPresenter.getBookShelf().getNoteUrl(); if (url == null) url = ""; int maxLength = 1273 - 1 - url.length(); BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(mPresenter.getBookShelf().getTag()); if (sourceBean != null) { // url=tvBookUrl.getText().toString()+"#"+ gson.toJson(sourceBean).replaceAll("\n\\s*\"[a-zA-Z]+\"(:\"\"|: \"\"| :\"\"| : \"\")\\s*,\\s*\n","\n").trim(); url = url + "#" + sourceBean.getJson(maxLength); Log.d("QRcode", "Length=" + url.length() + "\n" + url); Bitmap bitmap; QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); if (url.length() > 300) bitmap = QRCodeEncoder.syncEncodeQRCode(url, 800); else if (url.length() > 100) bitmap = QRCodeEncoder.syncEncodeQRCode(url, 500); else bitmap = QRCodeEncoder.syncEncodeQRCode(url, 300); QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); emitter.onSuccess(bitmap); } }).compose(RxUtils::toSimpleSingle) .subscribe(new MySingleObserver() { @Override public void onSuccess(Bitmap bitmap2) { try { File file = new File(BookDetailActivity.this.getExternalCacheDir(), binding.tvName.getText().toString() + ".png"); FileOutputStream fOut = new FileOutputStream(file); bitmap2.compress(Bitmap.CompressFormat.PNG, 80, fOut); fOut.flush(); fOut.close(); //noinspection ResultOfMethodCallIgnored file.setReadable(true, false); Uri contentUri = FileProvider.getUriForFile(BookDetailActivity.this, BuildConfig.APPLICATION_ID + ".fileProvider", file); final Intent intent = new Intent(Intent.ACTION_SEND); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Intent.EXTRA_STREAM, contentUri); intent.setType("image/png"); startActivity(Intent.createChooser(intent, "分享书籍")); } catch (Exception e) { toast(e.getLocalizedMessage()); } } }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/BookInfoEditActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.ActivityBookInfoEditBinding; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.utils.RealPathUtil; import com.kunfei.bookshelf.utils.SoftInputUtil; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.widget.modialog.MoDialogHUD; import kotlin.Unit; public class BookInfoEditActivity extends MBaseActivity { private final int ResultSelectCover = 103; private final int ResultEditCover = 104; private ActivityBookInfoEditBinding binding; private String noteUrl; private BookShelfBean book; private MoDialogHUD moDialogHUD; public static void startThis(Context context, String noteUrl) { Intent intent = new Intent(context, BookInfoEditActivity.class); intent.putExtra("noteUrl", noteUrl); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null && !TextUtils.isEmpty(savedInstanceState.getString("noteUrl"))) { noteUrl = savedInstanceState.getString("noteUrl"); } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putString("noteUrl", noteUrl); } /** * 布局载入 setContentView() */ @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityBookInfoEditBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(); binding.tilBookName.setHint(getString(R.string.book_name)); binding.tilBookAuthor.setHint(getString(R.string.author)); binding.tilCoverUrl.setHint(getString(R.string.cover_path)); binding.tilBookJj.setHint(getString(R.string.book_intro)); moDialogHUD = new MoDialogHUD(this); } /** * 数据初始化 */ @Override protected void initData() { if (!TextUtils.isEmpty(getIntent().getStringExtra("noteUrl"))) { noteUrl = getIntent().getStringExtra("noteUrl"); } if (!TextUtils.isEmpty(noteUrl)) { book = BookshelfHelp.getBook(noteUrl); if (book != null) { binding.tieBookName.setText(book.getBookInfoBean().getName()); binding.tieBookAuthor.setText(book.getBookInfoBean().getAuthor()); binding.tieBookJj.setText(book.getBookInfoBean().getIntroduce()); if (TextUtils.isEmpty(book.getCustomCoverPath())) { binding.tieCoverUrl.setText(book.getBookInfoBean().getCoverUrl()); } else { binding.tieCoverUrl.setText(book.getCustomCoverPath()); } } initCover(); } } /** * 事件触发绑定 */ @Override protected void bindEvent() { super.bindEvent(); binding.tvSelectCover.setOnClickListener(view -> selectCover()); binding.tvChangeCover.setOnClickListener(view -> { Intent intent = new Intent(BookInfoEditActivity.this, BookCoverEditActivity.class); intent.putExtra("name", book.getBookInfoBean().getName()); intent.putExtra("author", book.getBookInfoBean().getAuthor()); startActivityForResult(intent, ResultEditCover); }); binding.tvRefreshCover.setOnClickListener(view -> { book.setCustomCoverPath(binding.tieCoverUrl.getText().toString()); initCover(); }); } private void selectCover() { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.bg_image_per) .onGranted((requestCode) -> { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); startActivityForResult(intent, ResultSelectCover); return Unit.INSTANCE; }) .request(); } private void initCover() { if (!this.isFinishing() && book != null) { binding.ivCover.load(book.getCoverPath(), book.getName(), book.getAuthor()); } } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.book_info); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_book_info, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_save) { saveInfo(); } else if (id == android.R.id.home) { SoftInputUtil.hideIMM(getCurrentFocus()); finish(); } return super.onOptionsItemSelected(item); } private void saveInfo() { if (book != null) { book.getBookInfoBean().setName(binding.tieBookName.getText().toString()); book.getBookInfoBean().setAuthor(binding.tieBookAuthor.getText().toString()); book.getBookInfoBean().setIntroduce(binding.tieBookJj.getText().toString()); book.setCustomCoverPath(binding.tieCoverUrl.getText().toString()); initCover(); BookshelfHelp.saveBookToShelf(book); RxBus.get().post(RxBusTag.HAD_ADD_BOOK, book); SoftInputUtil.hideIMM(getCurrentFocus()); } finish(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Boolean mo = moDialogHUD.onKeyDown(keyCode, event); if (mo) return true; return super.onKeyDown(keyCode, event); } @Override public void onDestroy() { moDialogHUD.dismiss(); super.onDestroy(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case ResultSelectCover: if (resultCode == RESULT_OK && null != data) { binding.tieCoverUrl.setText(RealPathUtil.getPath(this, data.getData())); book.setCustomCoverPath(binding.tieCoverUrl.getText().toString()); initCover(); } break; case ResultEditCover: if (resultCode == RESULT_OK && null != data) { String url = data.getStringExtra("url"); binding.tieCoverUrl.setText(url); book.setCustomCoverPath(url); initCover(); } break; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/BookSourceActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.app.Activity; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.text.TextUtils; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.widget.LinearLayout; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.snackbar.Snackbar; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.dao.BookSourceBeanDao; import com.kunfei.bookshelf.databinding.ActivityBookSourceBinding; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.presenter.BookSourcePresenter; import com.kunfei.bookshelf.presenter.contract.BookSourceContract; import com.kunfei.bookshelf.service.ShareService; import com.kunfei.bookshelf.utils.ACache; import com.kunfei.bookshelf.utils.RealPathUtil; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.BookSourceAdapter; import com.kunfei.bookshelf.widget.filepicker.picker.FilePicker; import com.kunfei.bookshelf.widget.modialog.InputDialog; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import kotlin.Unit; /** * Created by GKF on 2017/12/16. * 书源管理 */ public class BookSourceActivity extends MBaseActivity implements BookSourceContract.View { private final int IMPORT_SOURCE = 102; private final int REQUEST_QR = 202; private ActivityBookSourceBinding binding; private ItemTouchCallback itemTouchCallback; private boolean selectAll = true; private MenuItem groupItem; private SubMenu groupMenu; private BookSourceAdapter adapter; private SearchView.SearchAutoComplete mSearchAutoComplete; private boolean isSearch; public static void startThis(Activity activity, int requestCode) { activity.startActivityForResult(new Intent(activity, BookSourceActivity.class), requestCode); } @Override protected BookSourceContract.Presenter initInjector() { return new BookSourcePresenter(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityBookSourceBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(); } @Override protected void onPause() { super.onPause(); } @Override protected void onDestroy() { super.onDestroy(); } @Override protected void initData() { } @Override protected void bindView() { super.bindView(); initSearchView(); initRecyclerView(); } @Override protected void firstRequest() { super.firstRequest(); refreshBookSource(); } private void initSearchView() { mSearchAutoComplete = binding.searchView.findViewById(R.id.search_src_text); mSearchAutoComplete.setTextSize(16); binding.searchView.setQueryHint(getString(R.string.search_book_source)); binding.searchView.onActionViewExpanded(); binding.searchView.clearFocus(); binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { isSearch = !TextUtils.isEmpty(newText); refreshBookSource(); return false; } }); } private void initRecyclerView() { binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL)); adapter = new BookSourceAdapter(this); binding.recyclerView.setAdapter(adapter); itemTouchCallback = new ItemTouchCallback(); itemTouchCallback.setOnItemTouchCallbackListener(adapter.getItemTouchCallbackListener()); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback); itemTouchHelper.attachToRecyclerView(binding.recyclerView); setDragEnable(getSort()); } private void setDragEnable(int sort) { if (itemTouchCallback == null) { return; } adapter.setSort(sort); itemTouchCallback.setDragEnable(sort == 0); } public void upDateSelectAll() { selectAll = true; for (BookSourceBean bookSourceBean : adapter.getDataList()) { if (!bookSourceBean.getEnable()) { selectAll = false; break; } } } private void selectAllDataS() { for (BookSourceBean bookSourceBean : adapter.getDataList()) { bookSourceBean.setEnable(!selectAll); } adapter.notifyDataSetChanged(); selectAll = !selectAll; AsyncTask.execute(() -> DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(adapter.getDataList())); setResult(RESULT_OK); } private void revertSelection() { for (BookSourceBean bookSourceBean : adapter.getDataList()) { bookSourceBean.setEnable(!bookSourceBean.getEnable()); } adapter.notifyDataSetChanged(); saveDate(adapter.getDataList()); setResult(RESULT_OK); } public void upSearchView(int size) { binding.searchView.setQueryHint(getString(R.string.search_book_source_num, size)); } @Override public void refreshBookSource() { if (isSearch) { List sourceBeanList; if (binding.searchView.getQuery().toString().equals("enabled")) { sourceBeanList = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .where(BookSourceBeanDao.Properties.Enable.eq(1)) .orderRaw(BookSourceManager.getBookSourceSort()) .orderAsc(BookSourceBeanDao.Properties.SerialNumber) .list(); } else { String term = "%" + binding.searchView.getQuery() + "%"; sourceBeanList = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder() .whereOr(BookSourceBeanDao.Properties.BookSourceName.like(term), BookSourceBeanDao.Properties.BookSourceGroup.like(term), BookSourceBeanDao.Properties.BookSourceUrl.like(term)) .orderRaw(BookSourceManager.getBookSourceSort()) .orderAsc(BookSourceBeanDao.Properties.SerialNumber) .list(); } adapter.resetDataS(sourceBeanList); } else { adapter.resetDataS(BookSourceManager.getAllBookSource()); } } public void delBookSource(BookSourceBean bookSource) { mPresenter.delData(bookSource); setResult(RESULT_OK); } public void saveDate(BookSourceBean date) { mPresenter.saveData(date); setResult(RESULT_OK); } public void saveDate(List date) { mPresenter.saveData(date); setResult(RESULT_OK); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.book_source_manage); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_book_source_activity, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { groupItem = menu.findItem(R.id.action_group); groupMenu = groupItem.getSubMenu(); upGroupMenu(); upSortMenu(); return super.onPrepareOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_add_book_source) { addBookSource(); } else if (id == R.id.action_select_all) { selectAllDataS(); } else if (id == R.id.action_import_book_source_local) { selectBookSourceFile(); } else if (id == R.id.action_import_book_source_onLine) { importBookSourceOnLine(); } else if (id == R.id.action_import_book_source_rwm) { scanBookSource(); } else if (id == R.id.action_revert_selection) { revertSelection(); } else if (id == R.id.action_del_select) { deleteDialog(); } else if (id == R.id.action_check_book_source) { mPresenter.checkBookSource(adapter.getSelectDataList()); } else if (id == R.id.action_check_find_source) { mPresenter.checkFindSource(adapter.getSelectDataList()); } else if (id == R.id.sort_manual) { upSourceSort(0); } else if (id == R.id.sort_auto) { upSourceSort(1); } else if (id == R.id.sort_pin_yin) { upSourceSort(2); } else if (id == R.id.show_enabled) { binding.searchView.setQuery("enabled", false); } else if (id == R.id.action_share_wifi) { ShareService.startThis(this, adapter.getSelectDataList()); } else if (id == android.R.id.home) { finish(); } if (item.getGroupId() == R.id.source_group) { binding.searchView.setQuery(item.getTitle(), true); } return super.onOptionsItemSelected(item); } public void upGroupMenu() { if (groupMenu == null) return; groupMenu.removeGroup(R.id.source_group); List groupList = BookSourceManager.getGroupList(); for (String groupName : new ArrayList<>(groupList)) { groupMenu.add(R.id.source_group, Menu.NONE, Menu.NONE, groupName); } } private void upSortMenu() { groupMenu.getItem(0).setChecked(false); groupMenu.getItem(1).setChecked(false); groupMenu.getItem(2).setChecked(false); groupMenu.getItem(getSort()).setChecked(true); } private void upSourceSort(int sort) { preferences.edit().putInt("SourceSort", sort).apply(); upSortMenu(); setDragEnable(sort); refreshBookSource(); } public int getSort() { return preferences.getInt("SourceSort", 0); } private void scanBookSource() { Intent intent = new Intent(this, QRCodeScanActivity.class); startActivityForResult(intent, REQUEST_QR); } private void addBookSource() { Intent intent = new Intent(this, SourceEditActivity.class); startActivityForResult(intent, SourceEditActivity.EDIT_SOURCE); } private void deleteDialog() { AlertDialog alertDialog = new AlertDialog.Builder(this) .setTitle(R.string.delete) .setMessage(R.string.del_msg) .setPositiveButton(R.string.ok, (dialog, which) -> mPresenter.delData(adapter.getSelectDataList())) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { }) .show(); ATH.setAlertDialogTint(alertDialog); } private void selectBookSourceFile() { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.please_grant_storage_permission) .onGranted((requestCode) -> { FilePicker filePicker = new FilePicker(BookSourceActivity.this, FilePicker.FILE); filePicker.setBackgroundColor(getResources().getColor(R.color.background)); filePicker.setTopBackgroundColor(getResources().getColor(R.color.background)); filePicker.setAllowExtensions(getResources().getStringArray(R.array.text_suffix)); filePicker.setOnFilePickListener(s -> mPresenter.importBookSourceLocal(s)); filePicker.show(); filePicker.getSubmitButton().setText(R.string.sys_file_picker); filePicker.getSubmitButton().setOnClickListener(view -> { filePicker.dismiss(); selectFileSys(); }); return Unit.INSTANCE; }) .request(); } private void importBookSourceOnLine() { String cu = ACache.get(this).getAsString("sourceUrl"); String[] cacheUrls = cu == null ? new String[]{} : cu.split(";"); List urlList = new ArrayList<>(Arrays.asList(cacheUrls)); InputDialog.builder(this) .setDefaultValue("") .setTitle(getString(R.string.input_book_source_url)) .setShowDel(true) .setAdapterValues(urlList) .setCallback(new InputDialog.Callback() { @Override public void setInputText(String inputText) { inputText = StringUtils.trim(inputText); if (!urlList.contains(inputText)) { urlList.add(0, inputText); ACache.get(BookSourceActivity.this).put("sourceUrl", TextUtils.join(";", urlList)); } mPresenter.importBookSource(inputText); } @Override public void delete(String value) { urlList.remove(value); ACache.get(BookSourceActivity.this).put("sourceUrl", TextUtils.join(";", urlList)); } }).show(); } private void selectFileSys() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"text/*", "application/json"}); intent.setType("*/*");//设置类型 startActivityForResult(intent, IMPORT_SOURCE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { switch (requestCode) { case SourceEditActivity.EDIT_SOURCE: refreshBookSource(); setResult(RESULT_OK); break; case IMPORT_SOURCE: if (data != null && data.getData() != null) { mPresenter.importBookSourceLocal(RealPathUtil.getPath(this, data.getData())); } break; case REQUEST_QR: if (data != null) { String result = data.getStringExtra("result"); if (!StringUtils.isTrimEmpty(result)) { if(result.replaceAll("\\s","").matches("^\\{.*\\}$")) { mPresenter.importBookSource(result); break; } result=result.trim(); String[] string=result.split("#",2); if(string.length==2){ if(string[1].replaceAll("\\s","").matches("^\\{.*\\}$")) { mPresenter.importBookSource(string[1]); break; } } mPresenter.importBookSource(result); }} break; } } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (isSearch) { try { //如果搜索框中有文字,则会先清空文字. mSearchAutoComplete.setText(""); } catch (Exception e) { e.printStackTrace(); } return true; } } return super.onKeyDown(keyCode, event); } @Override public Snackbar getSnackBar(String msg, int length) { return Snackbar.make(binding.llContent, msg, length); } @Override public void showSnackBar(String msg, int length) { super.showSnackBar(binding.llContent, msg, length); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ChapterListActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import static android.view.View.GONE; import static android.view.View.VISIBLE; import android.app.Activity; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SearchView; import androidx.fragment.app.Fragment; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.BaseTabActivity; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.databinding.ActivityChapterlistBinding; import com.kunfei.bookshelf.help.ReadBookControl; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.MaterialValueHelper; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.fragment.BookmarkFragment; import com.kunfei.bookshelf.view.fragment.ChapterListFragment; import java.util.Arrays; import java.util.List; public class ChapterListActivity extends BaseTabActivity { private ActivityChapterlistBinding binding; private ReadBookControl readBookControl = ReadBookControl.getInstance(); private SearchView searchView; private BookShelfBean bookShelf; private List chapterBeanList; public static void startThis(Activity activity, BookShelfBean bookShelf, List chapterBeanList) { Intent intent = new Intent(activity, ChapterListActivity.class); String key = String.valueOf(System.currentTimeMillis()); String bookKey = "book" + key; intent.putExtra("bookKey", bookKey); BitIntentDataManager.getInstance().putData(bookKey, bookShelf.clone()); String chapterListKey = "chapterList" + key; intent.putExtra("chapterListKey", chapterListKey); BitIntentDataManager.getInstance().putData(chapterListKey, chapterBeanList); activity.startActivity(intent); } @Override protected IPresenter initInjector() { return null; } @Override protected void onCreate(Bundle savedInstanceState) { setOrientation(readBookControl.getScreenDirection()); super.onCreate(savedInstanceState); RxBus.get().register(this); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (bookShelf != null) { String key = String.valueOf(System.currentTimeMillis()); String bookKey = "book" + key; getIntent().putExtra("bookKey", bookKey); BitIntentDataManager.getInstance().putData(bookKey, bookShelf.clone()); String chapterListKey = "chapterList" + key; getIntent().putExtra("chapterListKey", chapterListKey); BitIntentDataManager.getInstance().putData(chapterListKey, chapterBeanList); } } @Override protected void onDestroy() { RxBus.get().unregister(this); super.onDestroy(); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityChapterlistBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setupActionBar(); } @Override protected void bindView() { super.bindView(); mTlIndicator.setSelectedTabIndicatorColor(ThemeStore.accentColor(this)); mTlIndicator.setTabTextColors(ColorUtils.isColorLight(ThemeStore.primaryColor(this)) ? Color.BLACK : Color.WHITE, ThemeStore.accentColor(this)); } @SuppressWarnings("unchecked") @Override protected void initData() { String bookKey = getIntent().getStringExtra("bookKey"); bookShelf = (BookShelfBean) BitIntentDataManager.getInstance().getData(bookKey); String chapterListKey = getIntent().getStringExtra("chapterListKey"); chapterBeanList = (List) BitIntentDataManager.getInstance().getData(chapterListKey); } /**************abstract***********/ @Override protected List createTabFragments() { ChapterListFragment chapterListFragment = new ChapterListFragment(); BookmarkFragment bookmarkFragment = new BookmarkFragment(); return Arrays.asList(chapterListFragment, bookmarkFragment); } @Override protected List createTabTitles() { return Arrays.asList(getString(R.string.chapter_list), getString(R.string.bookmark)); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_search_view, menu); MenuItem search = menu.findItem(R.id.action_search); searchView = (SearchView) search.getActionView(); ATH.setTint(searchView, MaterialValueHelper.getPrimaryTextColor(this, ColorUtils.isColorLight(ThemeStore.primaryColor(this)))); searchView.setMaxWidth(getResources().getDisplayMetrics().widthPixels); searchView.onActionViewCollapsed(); searchView.setOnCloseListener(() -> { mTlIndicator.setVisibility(VISIBLE); return false; }); searchView.setOnSearchClickListener(view -> mTlIndicator.setVisibility(GONE)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { if (mTlIndicator.getSelectedTabPosition() == 1) { ((BookmarkFragment) mFragmentList.get(1)).startSearch(newText); } else { ((ChapterListFragment) mFragmentList.get(0)).startSearch(newText); } return false; } }); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { onBackPressed(); return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { if (mTlIndicator.getVisibility() != VISIBLE) { searchViewCollapsed(); } finish(); } //设置ToolBar private void setupActionBar() { setSupportActionBar(binding.toolbar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } public BookShelfBean getBookShelf() { return bookShelf; } public List getChapterBeanList() { return chapterBeanList; } public void searchViewCollapsed() { searchView.onActionViewCollapsed(); mTlIndicator.setVisibility(VISIBLE); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ChoiceBookActivity.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.activity; import android.annotation.SuppressLint; import android.content.Intent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.LinearLayoutManager; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.databinding.ActivityBookChoiceBinding; import com.kunfei.bookshelf.presenter.BookDetailPresenter; import com.kunfei.bookshelf.presenter.ChoiceBookPresenter; import com.kunfei.bookshelf.presenter.contract.ChoiceBookContract; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.ChoiceBookAdapter; import com.kunfei.bookshelf.widget.recycler.refresh.OnLoadMoreListener; import java.util.List; public class ChoiceBookActivity extends MBaseActivity implements ChoiceBookContract.View { private ActivityBookChoiceBinding binding; private ChoiceBookAdapter searchBookAdapter; private View viewRefreshError; @Override protected ChoiceBookContract.Presenter initInjector() { return new ChoiceBookPresenter(getIntent()); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityBookChoiceBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void onResume() { super.onResume(); } @Override protected void onPause() { super.onPause(); } @Override protected void initData() { searchBookAdapter = new ChoiceBookAdapter(this); } @SuppressLint("InflateParams") @Override protected void bindView() { this.setSupportActionBar(binding.toolbar); setupActionBar(); binding.rfRvSearchBooks.setRefreshRecyclerViewAdapter(searchBookAdapter, new LinearLayoutManager(this)); viewRefreshError = LayoutInflater.from(this).inflate(R.layout.view_refresh_error, null); viewRefreshError.findViewById(R.id.tv_refresh_again).setOnClickListener(v -> { searchBookAdapter.replaceAll(null); //刷新失败 ,重试 mPresenter.initPage(); mPresenter.toSearchBooks(null); startRefreshAnim(); }); binding.rfRvSearchBooks.setNoDataAndRefreshErrorView(LayoutInflater.from(this).inflate(R.layout.view_refresh_no_data, null), viewRefreshError); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(mPresenter.getTitle()); } } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } @Override protected void bindEvent() { searchBookAdapter.setCallback((animView, position, searchBookBean) -> { String dataKey = String.valueOf(System.currentTimeMillis()); Intent intent = new Intent(ChoiceBookActivity.this, BookDetailActivity.class); intent.putExtra("openFrom", BookDetailPresenter.FROM_SEARCH); intent.putExtra("data_key", dataKey); BitIntentDataManager.getInstance().putData(dataKey, searchBookBean); startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out); }); binding.rfRvSearchBooks.setBaseRefreshListener(() -> { mPresenter.initPage(); mPresenter.toSearchBooks(null); startRefreshAnim(); }); binding.rfRvSearchBooks.setLoadMoreListener(new OnLoadMoreListener() { @Override public void startLoadMore() { mPresenter.toSearchBooks(null); } @Override public void loadMoreErrorTryAgain() { mPresenter.toSearchBooks(null); } }); } @Override public void refreshSearchBook(List books) { searchBookAdapter.replaceAll(books); } @Override public void refreshFinish(Boolean isAll) { binding.rfRvSearchBooks.finishRefresh(isAll, true); } @Override public void loadMoreFinish(Boolean isAll) { binding.rfRvSearchBooks.finishLoadMore(isAll, true); } @Override public void loadMoreSearchBook(final List books) { if (books.size() <= 0) { loadMoreFinish(true); return; } searchBookAdapter.addAll(books); loadMoreFinish(false); } @Override public void searchBookError(String msg) { if (mPresenter.getPage() > 1) { binding.rfRvSearchBooks.finishLoadMore(true, true); } else { //刷新失败 binding.rfRvSearchBooks.refreshError(); if (msg != null) { ((TextView) viewRefreshError.findViewById(R.id.tv_error_msg)).setText(msg); } else { ((TextView) viewRefreshError.findViewById(R.id.tv_error_msg)).setText(R.string.get_data_error); } } } @Override protected void onDestroy() { super.onDestroy(); } @Override public void addBookShelfFailed(String massage) { toast(massage, ERROR); } @Override public void startRefreshAnim() { binding.rfRvSearchBooks.startRefresh(); } @Override protected void firstRequest() { super.firstRequest(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/DonateActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; import android.widget.Toast; import androidx.appcompat.app.ActionBar; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.databinding.ActivityDonateBinding; import com.kunfei.bookshelf.help.Donate; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * Created by GKF on 2018/1/13. * 捐赠页面 */ public class DonateActivity extends MBaseActivity { private ActivityDonateBinding binding; public static void startThis(Context context) { Intent intent = new Intent(context, DonateActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected IPresenter initInjector() { return null; } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityDonateBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void bindView() { this.setSupportActionBar(binding.toolbar); setupActionBar(); } @Override protected void initData() { } @Override protected void bindEvent() { binding.vwZfbTz.setOnClickListener(view -> Donate.aliDonate(this)); binding.cvWxGzh.setOnClickListener(view -> { ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = ClipData.newPlainText(null, "开源阅读"); if (clipboard != null) { clipboard.setPrimaryClip(clipData); toast(R.string.copy_complete); } }); binding.vwZfbHb.setOnClickListener(view -> openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbhbrwm.png")); binding.vwZfbRwm.setOnClickListener(view -> openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbskrwm.jpg")); binding.vwWxRwm.setOnClickListener(view -> openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/wxskrwm.jpg")); binding.vwQqRwm.setOnClickListener(view -> openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/qqskrwm.jpg")); binding.vwZfbHbSsm.setOnClickListener(view -> getZfbHb(this)); } public static void getZfbHb(Context context) { ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = ClipData.newPlainText(null, "537954522"); if (clipboard != null) { clipboard.setPrimaryClip(clipData); Toast.makeText(context, "高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包", Toast.LENGTH_LONG).show(); } try { PackageManager packageManager = context.getApplicationContext().getPackageManager(); Intent intent = packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone"); assert intent != null; intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); } finally { MApplication.getInstance().upDonateHb(); } } private void openActionViewIntent(String address) { try { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(address)); startActivity(intent); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, R.string.can_not_open, Toast.LENGTH_SHORT).show(); } } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.donate); } } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/DownloadActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import static com.kunfei.bookshelf.service.DownloadService.addDownloadAction; import static com.kunfei.bookshelf.service.DownloadService.finishDownloadAction; import static com.kunfei.bookshelf.service.DownloadService.obtainDownloadListAction; import static com.kunfei.bookshelf.service.DownloadService.progressDownloadAction; import static com.kunfei.bookshelf.service.DownloadService.removeDownloadAction; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.LinearLayoutManager; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.bean.DownloadBookBean; import com.kunfei.bookshelf.databinding.ActivityRecyclerVewBinding; import com.kunfei.bookshelf.service.DownloadService; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.DownloadAdapter; import java.lang.ref.WeakReference; import java.util.ArrayList; public class DownloadActivity extends MBaseActivity { private ActivityRecyclerVewBinding binding; private DownloadAdapter adapter; private DownloadReceiver receiver; public static void startThis(Activity activity) { Intent intent = new Intent(activity, DownloadActivity.class); activity.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onDestroy() { if (receiver != null) { unregisterReceiver(receiver); } super.onDestroy(); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } /** * 布局载入 setContentView() */ @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityRecyclerVewBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(); } /** * 数据初始化 */ @Override protected void initData() { receiver = new DownloadReceiver(this); IntentFilter filter = new IntentFilter(); filter.addAction(addDownloadAction); filter.addAction(removeDownloadAction); filter.addAction(progressDownloadAction); filter.addAction(obtainDownloadListAction); filter.addAction(finishDownloadAction); registerReceiver(receiver, filter); } @Override protected void bindView() { initRecyclerView(); } private void initRecyclerView() { binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new DownloadAdapter(this); binding.recyclerView.setAdapter(adapter); binding.recyclerView.setItemAnimator(null); DownloadService.obtainDownloadList(this); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.download_offline); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_book_download, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_cancel) { DownloadService.cancelDownload(this); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } private static class DownloadReceiver extends BroadcastReceiver { WeakReference ref; public DownloadReceiver(DownloadActivity activity) { this.ref = new WeakReference<>(activity); } @Override public void onReceive(Context context, Intent intent) { DownloadAdapter adapter = ref.get().adapter; if (adapter == null || intent == null) { return; } String action = intent.getAction(); if (action != null) { switch (action) { case addDownloadAction: DownloadBookBean downloadBook = intent.getParcelableExtra("downloadBook"); adapter.addData(downloadBook); break; case removeDownloadAction: downloadBook = intent.getParcelableExtra("downloadBook"); adapter.removeData(downloadBook); break; case progressDownloadAction: downloadBook = intent.getParcelableExtra("downloadBook"); adapter.upData(downloadBook); break; case finishDownloadAction: adapter.upDataS(null); break; case obtainDownloadListAction: ArrayList downloadBooks = intent.getParcelableArrayListExtra("downloadBooks"); adapter.upDataS(downloadBooks); break; } } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ImportBookActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.viewpager.widget.ViewPager; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.BaseTabActivity; import com.kunfei.bookshelf.databinding.ActivityImportBookBinding; import com.kunfei.bookshelf.presenter.ImportBookPresenter; import com.kunfei.bookshelf.presenter.contract.ImportBookContract; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.fragment.BaseFileFragment; import com.kunfei.bookshelf.view.fragment.FileCategoryFragment; import com.kunfei.bookshelf.view.fragment.LocalBookFragment; import java.io.File; import java.util.Arrays; import java.util.List; /** * 导入本地书籍 */ public class ImportBookActivity extends BaseTabActivity implements ImportBookContract.View { private static final String TAG = "ImportBookActivity"; private ActivityImportBookBinding binding; private LocalBookFragment mLocalFragment; private FileCategoryFragment mCategoryFragment; private BaseFileFragment mCurFragment; private BaseFileFragment.OnFileCheckedListener mListener = new BaseFileFragment.OnFileCheckedListener() { @Override public void onItemCheckedChange(boolean isChecked) { changeMenuStatus(); } @Override public void onCategoryChanged() { //状态归零 mCurFragment.setCheckedAll(false); //改变菜单 changeMenuStatus(); //改变是否能够全选 changeCheckedAllStatus(); } }; @Override protected ImportBookContract.Presenter initInjector() { return new ImportBookPresenter(); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityImportBookBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); setupActionBar(); } @Override protected void initData() { } @Override protected void bindView() { super.bindView(); mTlIndicator.setSelectedTabIndicatorColor(ThemeStore.accentColor(this)); mTlIndicator.setTabTextColors(getResources().getColor(R.color.tv_text_default), ThemeStore.accentColor(this)); } @Override protected List createTabFragments() { mCategoryFragment = new FileCategoryFragment(); mLocalFragment = new LocalBookFragment(); return Arrays.asList(mCategoryFragment, mLocalFragment); } @Override protected List createTabTitles() { return Arrays.asList(getString(R.string.files_tree), getString(R.string.intelligent_import)); } @Override protected void bindEvent() { binding.fileSystemCbSelectedAll.setOnClickListener( (view) -> { //设置全选状态 boolean isChecked = binding.fileSystemCbSelectedAll.isChecked(); mCurFragment.setCheckedAll(isChecked); //改变菜单状态 changeMenuStatus(); } ); mVp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { mCurFragment = (BaseFileFragment) mFragmentList.get(position); //改变菜单状态 changeMenuStatus(); } @Override public void onPageScrollStateChanged(int state) { } }); binding.fileSystemBtnAddBook.setOnClickListener( (v) -> { //获取选中的文件 List files = mCurFragment.getCheckedFiles(); //转换成CollBook,并存储 mPresenter.importBooks(files); } ); binding.fileSystemBtnDelete.setOnClickListener( (v) -> { //弹出,确定删除文件吗。 new AlertDialog.Builder(this) .setTitle(getString(R.string.del_file)) .setMessage(getString(R.string.sure_del_file)) .setPositiveButton(getResources().getString(R.string.ok), (dialog, which) -> { //删除选中的文件 mCurFragment.deleteCheckedFiles(); //提示删除文件成功 toast(R.string.del_file_success); }) .setNegativeButton(getResources().getString(R.string.cancel), null) .show(); } ); mCategoryFragment.setOnFileCheckedListener(mListener); mLocalFragment.setOnFileCheckedListener(mListener); } @Override protected void firstRequest() { super.firstRequest(); mCurFragment = (BaseFileFragment) mFragmentList.get(0); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.book_local); } } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } /** * 改变底部选择栏的状态 */ private void changeMenuStatus() { //点击、删除状态的设置 if (mCurFragment.getCheckedCount() == 0) { binding.fileSystemBtnAddBook.setText(getString(R.string.nb_file_add_shelf)); //设置某些按钮的是否可点击 setMenuClickable(false); if (binding.fileSystemCbSelectedAll.isChecked()) { mCurFragment.setChecked(false); binding.fileSystemCbSelectedAll.setChecked(mCurFragment.isCheckedAll()); } } else { binding.fileSystemBtnAddBook.setText(getString(R.string.nb_file_add_shelves, mCurFragment.getCheckedCount())); setMenuClickable(true); //全选状态的设置 //如果选中的全部的数据,则判断为全选 if (mCurFragment.getCheckedCount() == mCurFragment.getCheckableCount()) { //设置为全选 mCurFragment.setChecked(true); binding.fileSystemCbSelectedAll.setChecked(mCurFragment.isCheckedAll()); } //如果曾今是全选则替换 else if (mCurFragment.isCheckedAll()) { mCurFragment.setChecked(false); binding.fileSystemCbSelectedAll.setChecked(mCurFragment.isCheckedAll()); } } //重置全选的文字 if (mCurFragment.isCheckedAll()) { binding.fileSystemCbSelectedAll.setText(R.string.cancel); } else { binding.fileSystemCbSelectedAll.setText(getString(R.string.select_all)); } } private void setMenuClickable(boolean isClickable) { //设置是否可删除 binding.fileSystemBtnDelete.setEnabled(isClickable); binding.fileSystemBtnDelete.setClickable(isClickable); //设置是否可添加书籍 binding.fileSystemBtnAddBook.setEnabled(isClickable); binding.fileSystemBtnAddBook.setClickable(isClickable); } /** * 改变全选按钮的状态 */ private void changeCheckedAllStatus() { //获取可选择的文件数量 int count = mCurFragment.getCheckableCount(); //设置是否能够全选 if (count > 0) { binding.fileSystemCbSelectedAll.setClickable(true); binding.fileSystemCbSelectedAll.setEnabled(true); } else { binding.fileSystemCbSelectedAll.setClickable(false); binding.fileSystemCbSelectedAll.setEnabled(false); } } @Override public void addSuccess() { //设置HashMap为false mCurFragment.setCheckedAll(false); //改变菜单状态 changeMenuStatus(); //改变是否可以全选 changeCheckedAllStatus(); } @Override public void addError(String msg) { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/MainActivity.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.activity; import static com.kunfei.bookshelf.utils.NetworkUtils.isNetWorkAvailable; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.PorterDuff; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatImageView; import androidx.core.view.GravityCompat; import androidx.fragment.app.Fragment; import androidx.viewpager.widget.ViewPager; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.tabs.TabLayout; import com.hwangjr.rxbus.RxBus; import com.kunfei.bookshelf.BuildConfig; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.BaseTabActivity; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.ActivityMainBinding; import com.kunfei.bookshelf.help.FileHelp; import com.kunfei.bookshelf.help.ProcessTextHelp; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.help.storage.BackupRestoreUi; import com.kunfei.bookshelf.model.UpLastChapterModel; import com.kunfei.bookshelf.presenter.BookSourcePresenter; import com.kunfei.bookshelf.presenter.MainPresenter; import com.kunfei.bookshelf.presenter.contract.MainContract; import com.kunfei.bookshelf.service.WebService; import com.kunfei.bookshelf.utils.ACache; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.theme.NavigationViewUtil; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.fragment.BookListFragment; import com.kunfei.bookshelf.view.fragment.FindBookFragment; import com.kunfei.bookshelf.widget.modialog.InputDialog; import com.kunfei.bookshelf.widget.modialog.MoDialogHUD; import java.util.Arrays; import java.util.List; import java.util.Objects; import kotlin.Unit; public class MainActivity extends BaseTabActivity implements MainContract.View, BookListFragment.CallbackValue { private final int requestSource = 14; private String[] mTitles; private final int REQUEST_QR = 202; private ActivityMainBinding binding; private AppCompatImageView vwNightTheme; private int group; private ActionBarDrawerToggle mDrawerToggle; private MoDialogHUD moDialogHUD; private long exitTime = 0; private boolean resumed = false; private final Handler handler = new Handler(Looper.getMainLooper()); @Override protected MainContract.Presenter initInjector() { return new MainPresenter(); } @Override protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { resumed = savedInstanceState.getBoolean("resumed"); } group = preferences.getInt("bookshelfGroup", 0); super.onCreate(savedInstanceState); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean("resumed", resumed); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override public void onResume() { super.onResume(); String shared_url = preferences.getString("shared_url", ""); if (shared_url.length() > 1) { InputDialog.builder(this) .setTitle(getString(R.string.add_book_url)) .setDefaultValue(shared_url) .setCallback(new InputDialog.Callback() { @Override public void setInputText(String inputText) { inputText = StringUtils.trim(inputText); mPresenter.addBookUrl(inputText); } @Override public void delete(String value) { } }).show(); preferences.edit() .putString("shared_url", "") .apply(); } } /** * 沉浸状态栏 */ @Override public void initImmersionBar() { super.initImmersionBar(); } @Override protected void initData() { mTitles = new String[]{getString(R.string.bookshelf), getString(R.string.find)}; } @Override public boolean isRecreate() { return isRecreate; } @Override public int getGroup() { return group; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } @Override protected List createTabFragments() { BookListFragment bookListFragment = null; FindBookFragment findBookFragment = null; for (Fragment fragment : getSupportFragmentManager().getFragments()) { if (fragment instanceof BookListFragment) { bookListFragment = (BookListFragment) fragment; } else if (fragment instanceof FindBookFragment) { findBookFragment = (FindBookFragment) fragment; } } if (bookListFragment == null) bookListFragment = new BookListFragment(); if (findBookFragment == null) findBookFragment = new FindBookFragment(); return Arrays.asList(bookListFragment, findBookFragment); } @Override protected List createTabTitles() { return Arrays.asList(mTitles); } @Override protected void bindView() { super.bindView(); setSupportActionBar(binding.mainView.toolbar); setupActionBar(); binding.mainView.cardSearch.setCardBackgroundColor(ThemeStore.primaryColorDark(this)); initDrawer(); initTabLayout(); upGroup(group); moDialogHUD = new MoDialogHUD(this); if (!preferences.getBoolean("behaviorMain", true)) { AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) binding.mainView.toolbar.getLayoutParams(); params.setScrollFlags(0); } //点击跳转搜索页 binding.mainView.cardSearch.setOnClickListener(view -> startActivityByAnim(new Intent(this, SearchBookActivity.class), binding.mainView.toolbar, "sharedView", android.R.anim.fade_in, android.R.anim.fade_out)); } //初始化TabLayout和ViewPager private void initTabLayout() { mTlIndicator.setBackgroundColor(ThemeStore.backgroundColor(this)); mTlIndicator.setSelectedTabIndicatorColor(ThemeStore.accentColor(this)); //TabLayout使用自定义Item for (int i = 0; i < mTlIndicator.getTabCount(); i++) { TabLayout.Tab tab = mTlIndicator.getTabAt(i); if (tab == null) return; tab.setCustomView(tab_icon(mTitles[i])); View customView = tab.getCustomView(); if (customView == null) return; TextView tv = customView.findViewById(R.id.tabtext); tab.setContentDescription(String.format("%s,%s", tv.getText(), getString(R.string.click_on_selected_show_menu))); ImageView im = customView.findViewById(R.id.tabicon); if (tab.isSelected()) { im.setVisibility(View.VISIBLE); } else { im.setVisibility(View.GONE); } } mTlIndicator.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { View customView = tab.getCustomView(); if (customView == null) return; ImageView im = customView.findViewById(R.id.tabicon); im.setVisibility(View.VISIBLE); } @Override public void onTabUnselected(TabLayout.Tab tab) { View customView = tab.getCustomView(); if (customView == null) return; ImageView im = customView.findViewById(R.id.tabicon); im.setVisibility(View.GONE); } @Override public void onTabReselected(TabLayout.Tab tab) { View tabView = (View) Objects.requireNonNull(tab.getCustomView()).getParent(); if (tab.getPosition() == 0) { showBookGroupMenu(tabView); } else { showFindMenu(tabView); } } }); } /** * 显示分组菜单 */ private void showBookGroupMenu(View view) { PopupMenu popupMenu = new PopupMenu(this, view); for (int j = 0; j < getResources().getStringArray(R.array.book_group_array).length; j++) { popupMenu.getMenu().add(0, 0, j, getResources().getStringArray(R.array.book_group_array)[j]); } popupMenu.setOnMenuItemClickListener(menuItem -> { upGroup(menuItem.getOrder()); return true; }); popupMenu.setOnDismissListener(popupMenu1 -> updateTabItemIcon(0, false)); popupMenu.show(); updateTabItemIcon(0, true); } /** * 显示发现菜单 */ private void showFindMenu(View view) { PopupMenu popupMenu = new PopupMenu(this, view); popupMenu.getMenu().add(0, 0, 0, getString(R.string.switch_display_style)); popupMenu.getMenu().add(0, 0, 1, getString(R.string.clear_find_cache)); boolean findTypeIsFlexBox = preferences.getBoolean("findTypeIsFlexBox", true); boolean showFindLeftView = preferences.getBoolean("showFindLeftView", true); if (findTypeIsFlexBox) { popupMenu.getMenu().add(0, 0, 2, showFindLeftView ? "隐藏左侧栏" : "显示左侧栏"); } popupMenu.setOnMenuItemClickListener(menuItem -> { FindBookFragment findBookFragment = getFindFragment(); switch (menuItem.getOrder()) { case 0: preferences.edit() .putBoolean("findTypeIsFlexBox", !findTypeIsFlexBox) .apply(); if (findBookFragment != null) { findBookFragment.upStyle(); } break; case 1: ACache.get(this, "findCache").clear(); if (findBookFragment != null) { findBookFragment.refreshData(); } break; case 2: preferences.edit() .putBoolean("showFindLeftView", !showFindLeftView) .apply(); if (findBookFragment != null) { findBookFragment.upUI(); } break; } return true; }); popupMenu.setOnDismissListener(popupMenu1 -> updateTabItemIcon(1, false)); popupMenu.show(); updateTabItemIcon(1, true); } /** * 更新Tab图标 */ private void updateTabItemIcon(int index, boolean showMenu) { TabLayout.Tab tab = mTlIndicator.getTabAt(index); if (tab == null) return; View customView = tab.getCustomView(); if (customView == null) return; ImageView im = customView.findViewById(R.id.tabicon); if (showMenu) { im.setImageResource(R.drawable.ic_arrow_drop_up); } else { im.setImageResource(R.drawable.ic_arrow_drop_down); } } /** * 更新Tab文字 */ private void updateTabItemText(int group) { TabLayout.Tab tab = mTlIndicator.getTabAt(0); if (tab == null) return; View customView = tab.getCustomView(); if (customView == null) return; TextView tv = customView.findViewById(R.id.tabtext); tv.setText(getResources().getStringArray(R.array.book_group_array)[group]); tab.setContentDescription(String.format("%s,%s", tv.getText(), getString(R.string.click_on_selected_show_menu))); } private View tab_icon(String name) { @SuppressLint("InflateParams") View tabView = LayoutInflater.from(this).inflate(R.layout.tab_view_icon_right, null); TextView tv = tabView.findViewById(R.id.tabtext); tv.setText(name); ImageView im = tabView.findViewById(R.id.tabicon); im.setVisibility(View.VISIBLE); im.setImageResource(R.drawable.ic_arrow_drop_down); return tabView; } public ViewPager getViewPager() { return mVp; } public BookListFragment getBookListFragment() { try { return (BookListFragment) mFragmentList.get(0); } catch (Exception e) { return null; } } public FindBookFragment getFindFragment() { try { return (FindBookFragment) mFragmentList.get(1); } catch (Exception e) { return null; } } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // 这个必须要,没有的话进去的默认是个箭头。。正常应该是三横杠的 mDrawerToggle.syncState(); if (vwNightTheme != null) { upThemeVw(); } } @Override public boolean onPrepareOptionsMenu(Menu menu) { return super.onPrepareOptionsMenu(menu); } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main_activity, menu); return super.onCreateOptionsMenu(menu); } /** * 菜单事件 */ @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_add_local) { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.import_per) .onGranted((requestCode) -> { startActivity(new Intent(MainActivity.this, ImportBookActivity.class)); return Unit.INSTANCE; }) .request(); } else if (id == R.id.action_add_url) { InputDialog.builder(this) .setTitle(getString(R.string.add_book_url)) .setCallback(new InputDialog.Callback() { @Override public void setInputText(String inputText) { inputText = StringUtils.trim(inputText); mPresenter.addBookUrl(inputText); } @Override public void delete(String value) { } }).show(); } else if (id == R.id.action_add_qrcode) { Intent intent = new Intent(this, QRCodeScanActivity.class); //noinspection deprecation startActivityForResult(intent, REQUEST_QR); } else if (id == R.id.action_download_all) { if (!isNetWorkAvailable()) { toast(R.string.network_connection_unavailable); } else { RxBus.get().post(RxBusTag.DOWNLOAD_ALL, 10000); } } else if (id == R.id.menu_bookshelf_layout) { selectBookshelfLayout(); } else if (id == R.id.action_arrange_bookshelf) { if (getBookListFragment() != null) { getBookListFragment().setArrange(true); } } else if (id == R.id.action_web_start) { boolean startedThisTime = WebService.startThis(this); if (!startedThisTime) { toast(getString(R.string.web_service_already_started_hint)); } } else if (id == android.R.id.home) { if (binding.drawer.isDrawerOpen(GravityCompat.START)) { binding.drawer.closeDrawers(); } else { binding.drawer.openDrawer(GravityCompat.START, !MApplication.isEInkMode); } } return super.onOptionsItemSelected(item); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } //初始化侧边栏 private void initDrawer() { mDrawerToggle = new ActionBarDrawerToggle(this, binding.drawer, R.string.navigation_drawer_open, R.string.navigation_drawer_close); mDrawerToggle.syncState(); binding.drawer.addDrawerListener(mDrawerToggle); setUpNavigationView(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mDrawerToggle.onConfigurationChanged(newConfig); } private void upGroup(int group) { if (this.group != group) { SharedPreferences.Editor editor = preferences.edit(); editor.putInt("bookshelfGroup", group); editor.apply(); } this.group = group; RxBus.get().post(RxBusTag.UPDATE_GROUP, group); RxBus.get().post(RxBusTag.REFRESH_BOOK_LIST, false); //更换Tab文字 updateTabItemText(group); } /** * 侧边栏按钮 */ private void setUpNavigationView() { binding.navigationView.setBackgroundColor(ThemeStore.backgroundColor(this)); NavigationViewUtil.setItemIconColors(binding.navigationView, getResources().getColor(R.color.tv_text_default), ThemeStore.accentColor(this)); NavigationViewUtil.disableScrollbar(binding.navigationView); @SuppressLint("InflateParams") View headerView = LayoutInflater.from(this).inflate(R.layout.navigation_header, null); AppCompatImageView imageView = headerView.findViewById(R.id.iv_read); imageView.setColorFilter(ThemeStore.accentColor(this)); binding.navigationView.addHeaderView(headerView); Menu drawerMenu = binding.navigationView.getMenu(); vwNightTheme = drawerMenu.findItem(R.id.action_theme).getActionView().findViewById(R.id.iv_theme_day_night); upThemeVw(); vwNightTheme.setOnClickListener(view -> setNightTheme(!isNightTheme())); binding.navigationView.setNavigationItemSelectedListener(menuItem -> { binding.drawer.closeDrawer(GravityCompat.START, !MApplication.isEInkMode); int itemId = menuItem.getItemId(); if (itemId == R.id.action_book_source_manage) { handler.postDelayed(() -> BookSourceActivity.startThis(this, requestSource), 200); } else if (itemId == R.id.action_replace_rule) { handler.postDelayed(() -> ReplaceRuleActivity.startThis(this, null), 200); } else if (itemId == R.id.action_download) { handler.postDelayed(() -> DownloadActivity.startThis(this), 200); } else if (itemId == R.id.action_setting) { handler.postDelayed(() -> SettingActivity.startThis(this), 200); } else if (itemId == R.id.action_about) { handler.postDelayed(() -> AboutActivity.startThis(this), 200); } else if (itemId == R.id.action_donate) { handler.postDelayed(() -> DonateActivity.startThis(this), 200); } else if (itemId == R.id.action_backup) { handler.postDelayed(() -> BackupRestoreUi.INSTANCE.backup(this), 200); } else if (itemId == R.id.action_restore) { handler.postDelayed(() -> BackupRestoreUi.INSTANCE.restore(this), 200); } else if (itemId == R.id.action_theme) { handler.postDelayed(() -> ThemeSettingActivity.startThis(this), 200); } return true; }); } /** * 更新主题切换按钮 */ private void upThemeVw() { if (isNightTheme()) { vwNightTheme.setImageResource(R.drawable.ic_daytime); vwNightTheme.setContentDescription(getString(R.string.click_to_day)); } else { vwNightTheme.setImageResource(R.drawable.ic_brightness); vwNightTheme.setContentDescription(getString(R.string.click_to_night)); } vwNightTheme.getDrawable().mutate().setColorFilter(ThemeStore.accentColor(this), PorterDuff.Mode.SRC_ATOP); } private void selectBookshelfLayout() { new AlertDialog.Builder(this) .setTitle("选择书架布局") .setItems(R.array.bookshelf_layout, (dialog, which) -> { preferences.edit().putInt("bookshelfLayout", which).apply(); recreate(); }).show(); } /** * 新版本运行 */ private void versionUpRun() { if (preferences.getInt("versionCode", 0) != MApplication.getVersionCode()) { //保存版本号 preferences.edit() .putInt("versionCode", MApplication.getVersionCode()) .apply(); //更新日志 moDialogHUD.showAssetMarkdown("updateLog.md"); } } @Override protected void firstRequest() { if (!isRecreate) { versionUpRun(); } if (!Objects.equals(MApplication.downloadPath, FileHelp.getFilesPath())) { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.get_storage_per) .request(); } handler.postDelayed(() -> { UpLastChapterModel.getInstance().startUpdate(); if (BuildConfig.DEBUG) { ProcessTextHelp.setProcessTextEnable(false); } }, 60 * 1000); } @Override public void dismissHUD() { moDialogHUD.dismiss(); } public void onRestore(String msg) { moDialogHUD.showLoading(msg); } @SuppressLint("RtlHardcoded") @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Boolean mo = moDialogHUD.onKeyDown(keyCode, event); if (mo) { return true; } else if (mTlIndicator.getSelectedTabPosition() != 0) { Objects.requireNonNull(mTlIndicator.getTabAt(0)).select(); return true; } else { if (keyCode == KeyEvent.KEYCODE_BACK) { if (binding.drawer.isDrawerOpen(GravityCompat.START)) { binding.drawer.closeDrawer(GravityCompat.START, !MApplication.isEInkMode); return true; } exit(); return true; } return super.onKeyDown(keyCode, event); } } /** * 退出 */ public void exit() { if ((System.currentTimeMillis() - exitTime) > 2000) { showSnackBar(binding.mainView.toolbar, getString(R.string.double_click_exit)); exitTime = System.currentTimeMillis(); } else { finish(); } } @Override public void recreate() { super.recreate(); } @Override protected void onDestroy() { UpLastChapterModel.destroy(); DbHelper.getDaoSession().getBookContentBeanDao().deleteAll(); super.onDestroy(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); BackupRestoreUi.INSTANCE.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case requestSource: if (resultCode == RESULT_OK) { FindBookFragment findBookFragment = getFindFragment(); if (findBookFragment != null) { findBookFragment.refreshData(); } } break; case REQUEST_QR: if (resultCode == RESULT_OK) { String result = data.getStringExtra("result"); if (!StringUtils.isTrimEmpty(result)) { result=result.trim(); // 如果只有书源,则导入书源 if(result.replaceAll("(\\s|\n)*","").matches("^\\{.*$")) { new BookSourcePresenter().importBookSource(result); break; } // String[] string=result.split("#",2); // mPresenter.addBookUrl(string[0]); mPresenter.addBookUrl(result); } } break; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/QRCodeScanActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.appcompat.app.ActionBar; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.databinding.ActivityQrcodeCaptureBinding; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.utils.RealPathUtil; import com.kunfei.bookshelf.widget.filepicker.picker.FilePicker; import cn.bingoogolapple.qrcode.core.QRCodeView; import kotlin.Unit; /** * Created by GKF on 2018/1/29. */ public class QRCodeScanActivity extends MBaseActivity implements QRCodeView.Delegate { private ActivityQrcodeCaptureBinding binding; private final int REQUEST_QR_IMAGE = 202; private boolean flashlightIsOpen; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } /** * 布局载入 setContentView() */ @Override protected void onCreateActivity() { binding = ActivityQrcodeCaptureBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(); } /** * 数据初始化 */ @Override protected void initData() { binding.zxingview.setDelegate(this); binding.fabFlashlight.setOnClickListener(view -> { if (flashlightIsOpen) { flashlightIsOpen = false; binding.zxingview.closeFlashlight(); } else { flashlightIsOpen = true; binding.zxingview.openFlashlight(); } }); } @Override protected void onStart() { super.onStart(); startCamera(); } private void startCamera() { new PermissionsCompat.Builder(this) .addPermissions(Permissions.CAMERA) .rationale(R.string.qr_per) .onGranted((requestCode) -> { binding.zxingview.setVisibility(View.VISIBLE); binding.zxingview.startSpotAndShowRect(); // 显示扫描框,并开始识别 return Unit.INSTANCE; }) .request(); } @Override protected void onStop() { binding.zxingview.stopCamera(); // 关闭摄像头预览,并且隐藏扫描框 super.onStop(); } @Override protected void onDestroy() { binding.zxingview.onDestroy(); // 销毁二维码扫描控件 super.onDestroy(); } @Override public void onScanQRCodeSuccess(String result) { Intent intent = new Intent(); intent.putExtra("result", result); setResult(RESULT_OK, intent); finish(); } @Override public void onCameraAmbientBrightnessChanged(boolean isDark) { } @Override public void onScanQRCodeOpenCameraError() { } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.scan_qr_code); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_qr_code_scan, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_choose_from_gallery) { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.get_storage_per) .onGranted((requestCode) -> { chooseFromGallery(); return Unit.INSTANCE; }) .request(); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); binding.zxingview.startSpotAndShowRect(); // 显示扫描框,并开始识别 if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_QR_IMAGE) { final String picturePath = RealPathUtil.getPath(this, data.getData()); // 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调 binding.zxingview.decodeQRCode(picturePath); } } private void chooseFromGallery() { try { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); startActivityForResult(intent, REQUEST_QR_IMAGE); } catch (Exception ignored) { FilePicker picker = new FilePicker(this, FilePicker.FILE); picker.setBackgroundColor(getResources().getColor(R.color.background)); picker.setTopBackgroundColor(getResources().getColor(R.color.background)); picker.setItemHeight(30); picker.setOnFilePickListener(currentPath -> binding.zxingview.decodeQRCode(currentPath)); picker.show(); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ReadBookActivity.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.activity; import static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.PorterDuff; import android.net.Uri; import android.os.BatteryManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import com.kunfei.basemvplib.AppActivityManager; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.BookmarkBean; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import com.kunfei.bookshelf.bean.TxtChapterRuleBean; import com.kunfei.bookshelf.dao.TxtChapterRuleBeanDao; import com.kunfei.bookshelf.databinding.ActivityBookReadBinding; import com.kunfei.bookshelf.help.ChapterContentHelp; import com.kunfei.bookshelf.help.ReadBookControl; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.help.storage.Backup; import com.kunfei.bookshelf.model.ReplaceRuleManager; import com.kunfei.bookshelf.model.TxtChapterRuleManager; import com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl; import com.kunfei.bookshelf.presenter.ReadBookPresenter; import com.kunfei.bookshelf.presenter.contract.ReadBookContract; import com.kunfei.bookshelf.service.ReadAloudService; import com.kunfei.bookshelf.utils.ActivityExtensionsKt; import com.kunfei.bookshelf.utils.BatteryUtil; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.utils.SoftInputUtil; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.SystemUtil; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.dialog.SourceLoginDialog; import com.kunfei.bookshelf.view.popupwindow.CheckAddShelfPop; import com.kunfei.bookshelf.view.popupwindow.MoreSettingPop; import com.kunfei.bookshelf.view.popupwindow.ReadAdjustMarginPop; import com.kunfei.bookshelf.view.popupwindow.ReadAdjustPop; import com.kunfei.bookshelf.view.popupwindow.ReadBottomMenu; import com.kunfei.bookshelf.view.popupwindow.ReadInterfacePop; import com.kunfei.bookshelf.view.popupwindow.ReadLongPressPop; import com.kunfei.bookshelf.widget.modialog.BookmarkDialog; import com.kunfei.bookshelf.widget.modialog.ChangeSourceDialog; import com.kunfei.bookshelf.widget.modialog.DownLoadDialog; import com.kunfei.bookshelf.widget.modialog.InputDialog; import com.kunfei.bookshelf.widget.modialog.MoDialogHUD; import com.kunfei.bookshelf.widget.modialog.ReplaceRuleDialog; import com.kunfei.bookshelf.widget.page.PageLoader; import com.kunfei.bookshelf.widget.page.PageLoaderNet; import com.kunfei.bookshelf.widget.page.PageView; import com.kunfei.bookshelf.widget.page.TxtChapter; import com.kunfei.bookshelf.widget.page.animation.PageAnimation; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; import javax.script.SimpleBindings; import kotlin.Unit; /** * 阅读界面 */ public class ReadBookActivity extends MBaseActivity implements ReadBookContract.View, View.OnTouchListener { private final int payActivityRequest = 1234; public final int fontDirRequest = 24345; private ActivityBookReadBinding binding; private Animation menuTopIn; private Animation menuTopOut; private Animation menuBottomIn; private Animation menuBottomOut; private ActionBar actionBar; private PageLoader mPageLoader; private final Handler mHandler = new Handler(); private Runnable autoPageRunnable; private Runnable keepScreenRunnable; private Runnable upHpbNextPage; private int nextPageTime; private String noteUrl; private Boolean isAdd = false; //判断是否已经添加进书架 private ReadAloudService.Status aloudStatus = ReadAloudService.Status.STOP; private int screenTimeOut; private final int upHpbInterval = 100; private Menu menu; private CheckAddShelfPop checkAddShelfPop; private MoDialogHUD moDialogHUD; private ThisBatInfoReceiver batInfoReceiver; private final ReadBookControl readBookControl = ReadBookControl.getInstance(); private boolean autoPage = false; private boolean aloudNextPage; private int lastX, lastY; @Override protected ReadBookContract.Presenter initInjector() { return new ReadBookPresenter(); } @Override protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { noteUrl = savedInstanceState.getString("noteUrl"); isAdd = savedInstanceState.getBoolean("isAdd"); } readBookControl.initTextDrawableIndex(); super.onCreate(savedInstanceState); screenTimeOut = getResources().getIntArray(R.array.screen_time_out_value)[readBookControl.getScreenTimeOut()]; keepScreenRunnable = this::unKeepScreenOn; autoPageRunnable = this::nextPage; upHpbNextPage = this::upHpbNextPage; } @Override protected void onCreateActivity() { setOrientation(readBookControl.getScreenDirection()); binding = ActivityBookReadBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && readBookControl.getToLh()) { if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; getWindow().setAttributes(lp); } } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (mPresenter.getBookShelf() != null) { outState.putString("noteUrl", mPresenter.getBookShelf().getNoteUrl()); outState.putBoolean("isAdd", isAdd); String key = String.valueOf(System.currentTimeMillis()); String bookKey = "book" + key; getIntent().putExtra("bookKey", bookKey); BitIntentDataManager.getInstance().putData(bookKey, mPresenter.getBookShelf().clone()); String chapterListKey = "chapterList" + key; getIntent().putExtra("chapterListKey", chapterListKey); BitIntentDataManager.getInstance().putData(chapterListKey, mPresenter.getChapterList()); } } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { initImmersionBar(); } } /** * 状态栏 */ @Override protected void initImmersionBar() { ActivityExtensionsKt.fullScreen(this); int flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); flag = flag | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; if (readBookControl.getHideNavigationBar()) { flag = flag | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; if (binding.readMenuBottom.getVisibility() != View.VISIBLE) { flag = flag | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; } } if (readBookControl.getHideStatusBar()) { if (binding.readMenuBottom.getVisibility() != View.VISIBLE) { flag = flag | View.SYSTEM_UI_FLAG_FULLSCREEN; } } getWindow().getDecorView().setSystemUiVisibility(flag); if (binding.readMenuBottom.getVisibility() == View.VISIBLE) { if (isImmersionBarEnabled()) { ActivityExtensionsKt.setStatusBarColorAuto(this, ThemeStore.primaryColor(this), false, true); } else { ActivityExtensionsKt.setStatusBarColorAuto(this, ColorUtils.darkenColor(ThemeStore.primaryColor(this)), false, true); } changeNavigationBarColor(); } else { if (isImmersionBarEnabled()) { getWindow().setStatusBarColor(Color.TRANSPARENT); ActivityExtensionsKt.setLightStatusBar(this, readBookControl.getDarkStatusIcon()); } else { getWindow().setStatusBarColor(getResources().getColor(R.color.ate_switch_track_normal_light)); ActivityExtensionsKt.setLightStatusBar(this, false); } } changeNavigationBarColor(); screenOffTimerStart(); } /** * 修改导航栏颜色 */ private void changeNavigationBarColor() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { getWindow().setNavigationBarDividerColor(Color.TRANSPARENT); } int barColorType = readBookControl.getNavBarColor(); if (binding.readMenuBottom.getVisibility() == View.VISIBLE || binding.readAdjustPop.getVisibility() == View.VISIBLE || binding.readInterfacePop.getVisibility() == View.VISIBLE || binding.readAdjustMarginPop.getVisibility() == View.VISIBLE || binding.moreSettingPop.getVisibility() == View.VISIBLE) { barColorType = 0; } switch (barColorType) { case 1: ActivityExtensionsKt.setNavigationBarColorAuto(this, getResources().getColor(R.color.black)); break; case 2: ActivityExtensionsKt.setNavigationBarColorAuto(this, getResources().getColor(R.color.white)); break; case 3: ActivityExtensionsKt.setNavigationBarColorAuto(this, readBookControl.getBgColor()); break; } } /** * 取消亮屏保持 */ private void unKeepScreenOn() { keepScreenOn(false); } /** * @param keepScreenOn 是否保持亮屏 */ public void keepScreenOn(boolean keepScreenOn) { if (keepScreenOn) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } /** * 重置黑屏时间 */ private void screenOffTimerStart() { if (screenTimeOut < 0) { keepScreenOn(true); return; } int screenOffTime = screenTimeOut * 1000 - SystemUtil.getScreenOffTime(this); if (screenOffTime > 0) { mHandler.removeCallbacks(keepScreenRunnable); keepScreenOn(true); mHandler.postDelayed(keepScreenRunnable, screenOffTime); } else { keepScreenOn(false); } } /** * 自动翻页 */ private void autoPage() { mHandler.removeCallbacks(upHpbNextPage); mHandler.removeCallbacks(autoPageRunnable); if (autoPage) { binding.pbNextPage.setVisibility(View.VISIBLE); //每页按字数计算一次时间 nextPageTime = mPageLoader.curPageLength() * 60 * 1000 / readBookControl.getCPM(); if (0 == nextPageTime) nextPageTime = 1000; binding.pbNextPage.setMax(nextPageTime); mHandler.postDelayed(upHpbNextPage, upHpbInterval); mHandler.postDelayed(autoPageRunnable, nextPageTime); } else { binding.pbNextPage.setVisibility(View.INVISIBLE); } binding.readMenuBottom.setAutoPage(autoPage); } /** * 更新自动翻页进度条 */ private void upHpbNextPage() { nextPageTime = nextPageTime - upHpbInterval; binding.pbNextPage.setProgress(nextPageTime); mHandler.postDelayed(upHpbNextPage, upHpbInterval); } /** * 停止自动翻页 */ private void autoPageStop() { autoPage = false; autoPage(); } /** * 下一页 */ private void nextPage() { runOnUiThread(() -> { screenOffTimerStart(); if (mPageLoader != null) { mPageLoader.skipToNextPage(); } }); } @Override protected void initData() { mPresenter.saveProgress(); //显示菜单 menuTopIn = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_top_in); menuTopIn.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { initImmersionBar(); BookChapterBean durChapter = mPresenter.getDurChapter(); BookSourceBean source = mPresenter.getBookSource(); if (durChapter != null && source != null) { if (TextUtils.isEmpty(source.getLoginUrl())) { binding.login.setVisibility(View.GONE); binding.pay.setVisibility(View.GONE); } else if (durChapter.getIsVip() && !durChapter.getIsPay() && !TextUtils.isEmpty(source.getPayAction())) { binding.login.setVisibility(View.VISIBLE); binding.pay.setVisibility(View.VISIBLE); } else { binding.login.setVisibility(View.VISIBLE); } } else { binding.login.setVisibility(View.GONE); binding.pay.setVisibility(View.GONE); } } @Override public void onAnimationEnd(Animation animation) { binding.vMenuBg.setOnClickListener(v -> popMenuOut()); initImmersionBar(); int nbh = ActivityExtensionsKt.getNavigationBarHeight(ReadBookActivity.this); binding.readMenuBottom.setNavigationBarHeight(nbh); } @Override public void onAnimationRepeat(Animation animation) { } }); menuBottomIn = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_bottom_in); menuBottomIn.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { initImmersionBar(); } @Override public void onAnimationEnd(Animation animation) { binding.vMenuBg.setOnClickListener(v -> popMenuOut()); } @Override public void onAnimationRepeat(Animation animation) { } }); //隐藏菜单 menuTopOut = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_top_out); menuTopOut.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { binding.vMenuBg.setOnClickListener(null); initImmersionBar(); } @Override public void onAnimationEnd(Animation animation) { binding.flMenu.setVisibility(View.INVISIBLE); binding.llMenuTop.setVisibility(View.INVISIBLE); binding.readMenuBottom.setVisibility(View.INVISIBLE); binding.readAdjustPop.setVisibility(View.INVISIBLE); binding.readAdjustMarginPop.setVisibility(View.INVISIBLE); binding.readInterfacePop.setVisibility(View.INVISIBLE); binding.moreSettingPop.setVisibility(View.INVISIBLE); initImmersionBar(); } @Override public void onAnimationRepeat(Animation animation) { } }); menuBottomOut = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_bottom_out); menuBottomOut.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { binding.vMenuBg.setOnClickListener(null); initImmersionBar(); } @Override public void onAnimationEnd(Animation animation) { binding.flMenu.setVisibility(View.INVISIBLE); binding.llMenuTop.setVisibility(View.INVISIBLE); binding.readMenuBottom.setVisibility(View.INVISIBLE); binding.readAdjustPop.setVisibility(View.INVISIBLE); binding.readAdjustMarginPop.setVisibility(View.INVISIBLE); binding.readInterfacePop.setVisibility(View.INVISIBLE); binding.moreSettingPop.setVisibility(View.INVISIBLE); initImmersionBar(); } @Override public void onAnimationRepeat(Animation animation) { } }); if (MApplication.isEInkMode) { menuTopIn.setDuration(0); menuTopOut.setDuration(0); menuBottomIn.setDuration(0); menuBottomOut.setDuration(0); } } @Override protected void bindView() { this.setSupportActionBar(binding.toolbar); setupActionBar(); mPresenter.initData(this); binding.appBar.setPadding(0, ScreenUtils.getStatusBarHeight(), 0, 0); binding.appBar.setBackgroundColor(ThemeStore.primaryColor(this)); binding.readMenuBottom.setFabNightTheme(isNightTheme()); //弹窗 moDialogHUD = new MoDialogHUD(this); initBottomMenu(); initReadInterfacePop(); initReadAdjustPop(); initReadAdjustMarginPop(); initMoreSettingPop(); initMediaPlayer(); initReadLongPressPop(); binding.pageView.setBackground(readBookControl.getTextBackground(this)); binding.cursorLeft.getDrawable().setColorFilter(ThemeStore.accentColor(this), PorterDuff.Mode.SRC_ATOP); binding.cursorRight.getDrawable().setColorFilter(ThemeStore.accentColor(this), PorterDuff.Mode.SRC_ATOP); } /** * 初始化播放界面 */ private void initMediaPlayer() { binding.mediaPlayerPop.setIvChapterClickListener(v -> ChapterListActivity.startThis(ReadBookActivity.this, mPresenter.getBookShelf(), mPresenter.getChapterList())); binding.mediaPlayerPop.setIvTimerClickListener(v -> ReadAloudService.setTimer(getContext(), 10)); binding.mediaPlayerPop.setIvCoverBgClickListener(v -> { binding.flMenu.setVisibility(View.VISIBLE); binding.llMenuTop.setVisibility(View.VISIBLE); binding.llMenuTop.startAnimation(menuTopIn); }); binding.mediaPlayerPop.setPlayClickListener(v -> onMediaButton(ReadAloudService.ActionMediaPlay)); binding.mediaPlayerPop.setPrevClickListener(v -> { mPresenter.getBookShelf().setDurChapterPage(0); mPageLoader.skipToPrePage(); }); binding.mediaPlayerPop.setNextClickListener(v -> { mPresenter.getBookShelf().setDurChapterPage(0); mPageLoader.skipToNextPage(); }); binding.mediaPlayerPop.setCallback(dur -> ReadAloudService.setProgress(ReadBookActivity.this, dur)); } /** * 初始化底部菜单 */ private void initBottomMenu() { binding.readMenuBottom.setListener(new ReadBottomMenu.Callback() { @Override public void skipToPage(int page) { if (mPageLoader != null) { mPageLoader.skipToPage(page); } } @Override public void onMediaButton() { ReadBookActivity.this.onMediaButton(ReadAloudService.ActionMediaPlay); } @Override public void autoPage() { if (ReadAloudService.running) { ReadBookActivity.this.toast(R.string.aloud_can_not_auto_page); return; } ReadBookActivity.this.autoPage = !ReadBookActivity.this.autoPage; ReadBookActivity.this.autoPage(); } @Override public void setNightTheme() { ReadBookActivity.this.setNightTheme(!isNightTheme()); } @Override public void skipPreChapter() { if (mPresenter.getBookShelf() != null) { mPageLoader.skipPreChapter(); } } @Override public void skipNextChapter() { if (mPresenter.getBookShelf() != null) { mPageLoader.skipNextChapter(); } } @Override public void openReplaceRule() { popMenuOut(); ReplaceRuleActivity.startThis(ReadBookActivity.this, mPresenter.getBookShelf()); } @Override public void openChapterList() { ReadBookActivity.this.popMenuOut(); if (!mPresenter.getChapterList().isEmpty()) { mHandler.postDelayed(() -> ChapterListActivity.startThis(ReadBookActivity.this, mPresenter.getBookShelf(), mPresenter.getChapterList()), menuTopOut.getDuration()); } } @Override public void openAdjust() { ReadBookActivity.this.popMenuOut(); mHandler.postDelayed(ReadBookActivity.this::readAdjustIn, menuBottomOut.getDuration() + 100); } @Override public void openReadInterface() { ReadBookActivity.this.popMenuOut(); mHandler.postDelayed(ReadBookActivity.this::readInterfaceIn, menuBottomOut.getDuration() + 100); } @Override public void openMoreSetting() { ReadBookActivity.this.popMenuOut(); mHandler.postDelayed(ReadBookActivity.this::moreSettingIn, menuBottomOut.getDuration() + 100); } @Override public void toast(int id) { ReadBookActivity.this.toast(id); } @Override public void dismiss() { popMenuOut(); } }); } /** * 初始化调节 */ private void initReadAdjustPop() { binding.readAdjustPop.setListener(this, new ReadAdjustPop.Callback() { @Override public void speechRateFollowSys() { if (ReadAloudService.running) { ReadAloudService.stop(ReadBookActivity.this); } } @Override public void changeSpeechRate(int speechRate) { if (ReadAloudService.running) { ReadAloudService.pause(ReadBookActivity.this); ReadAloudService.resume(ReadBookActivity.this); } } }); } /** * 初始化调节 */ private void initReadAdjustMarginPop() { binding.readAdjustMarginPop.setListener(this, new ReadAdjustMarginPop.Callback() { @Override public void upTextSize() { if (mPageLoader != null) { mPageLoader.setTextSize(); } } @Override public void upMargin() { if (mPageLoader != null) { mPageLoader.upMargin(); } } @Override public void refresh() { if (mPageLoader != null) { mPageLoader.refreshUi(); } } }); } /** * 初始化界面设置 */ private void initReadInterfacePop() { binding.readInterfacePop.setListener(this, new ReadInterfacePop.Callback() { @Override public void upPageMode() { if (mPageLoader != null) { mPageLoader.setPageMode(PageAnimation.Mode.getPageMode(readBookControl.getPageMode())); } } @Override public void upTextSize() { if (mPageLoader != null) { mPageLoader.setTextSize(); } } @Override public void upMargin() { if (mPageLoader != null) { mPageLoader.upMargin(); } } @Override public void bgChange() { readBookControl.initTextDrawableIndex(); binding.pageView.setBackground(readBookControl.getTextBackground(ReadBookActivity.this)); initImmersionBar(); if (mPageLoader != null) { mPageLoader.refreshUi(); } } @Override public void refresh() { if (mPageLoader != null) { mPageLoader.refreshUi(); } } }); } /** * 初始化其它设置 */ private void initMoreSettingPop() { binding.moreSettingPop.setListener(new MoreSettingPop.Callback() { @Override public void upBar() { initImmersionBar(); } @Override public void keepScreenOnChange(int keepScreenOn) { screenTimeOut = getResources().getIntArray(R.array.screen_time_out_value)[keepScreenOn]; screenOffTimerStart(); } @Override public void recreate() { ReadBookActivity.this.recreate(); } @Override public void refreshPage() { if (mPageLoader != null) { mPageLoader.refreshUi(); } } }); } @SuppressLint("ClickableViewAccessibility") @Override protected void bindEvent() { binding.tvChapterName.setOnClickListener(v -> { if (mPresenter.getBookSource() != null) { SourceEditActivity.startThis(this, mPresenter.getBookSource()); } }); //打开URL binding.tvChapterUrl.setOnClickListener(view -> { try { String url = binding.tvChapterUrl.getText().toString(); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); startActivity(intent); } catch (Exception e) { toast(R.string.can_not_open); } }); binding.login.setOnClickListener(v -> login()); binding.pay.setOnClickListener(v -> pay()); binding.cursorLeft.setOnTouchListener(this); binding.cursorRight.setOnTouchListener(this); binding.flContent.setOnTouchListener(this); } /** * 开始加载 */ @Override public void startLoadingBook() { initPageView(); binding.mediaPlayerPop.setCover(mPresenter.getBookShelf().getCustomCoverPath() != null ? mPresenter.getBookShelf().getCustomCoverPath() : mPresenter.getBookShelf().getBookInfoBean().getCoverUrl()); } /** * 加载阅读页面 */ private void initPageView() { mPageLoader = binding.pageView.getPageLoader(this, mPresenter.getBookShelf(), new PageLoader.Callback() { @Override public List getChapterList() { return mPresenter.getChapterList(); } /** * @param pos:切换章节的序号 */ @Override public void onChapterChange(int pos) { if (mPresenter.getChapterList().isEmpty()) return; if (pos >= mPresenter.getChapterList().size()) return; mPresenter.getBookShelf().setDurChapterName(mPresenter.getChapterList().get(pos).getDurChapterName()); actionBar.setTitle(mPresenter.getBookShelf().getBookInfoBean().getName()); if (mPresenter.getBookShelf().getChapterListSize() > 0) { BookChapterBean chapter = mPresenter.getChapterList().get(pos); if (chapter.getIsVip() && !chapter.getIsPay()) { toast("付费章节未购买,如已购买请登录并刷新目录"); } binding.tvChapterName.setVisibility(View.VISIBLE); binding.tvChapterName.setText(mPresenter.getChapterList().get(pos).getDurChapterName()); binding.tvChapterUrl.setVisibility(View.VISIBLE); binding.tvChapterUrl.setText(NetworkUtils.getAbsoluteURL(mPresenter.getBookShelf().getBookInfoBean().getChapterUrl(), chapter.getDurChapterUrl())); } else { binding.tvChapterName.setVisibility(View.GONE); binding.tvChapterUrl.setVisibility(View.GONE); } if (mPresenter.getBookShelf().getChapterListSize() == 1) { binding.readMenuBottom.setTvPre(false); binding.readMenuBottom.setTvNext(false); } else { if (pos == 0) { binding.readMenuBottom.setTvPre(false); binding.readMenuBottom.setTvNext(true); } else if (pos == mPresenter.getBookShelf().getChapterListSize() - 1) { binding.readMenuBottom.setTvPre(true); binding.readMenuBottom.setTvNext(false); } else { binding.readMenuBottom.setTvPre(true); binding.readMenuBottom.setTvNext(true); } } } /** * @param chapters:返回章节目录 */ @Override public void onCategoryFinish(List chapters) { mPresenter.setChapterList(chapters); mPresenter.getBookShelf().setChapterListSize(chapters.size()); mPresenter.getBookShelf().setDurChapterName(chapters.get(mPresenter.getBookShelf().getDurChapter()).getDurChapterName()); mPresenter.getBookShelf().setLastChapterName(chapters.get(mPresenter.getChapterList().size() - 1).getDurChapterName()); mPresenter.saveProgress(); } /** * 总页数变化 */ @Override public void onPageCountChange(int count) { binding.readMenuBottom.getReadProgress().setMax(Math.max(0, count - 1)); binding.readMenuBottom.getReadProgress().setProgress(0); // 如果处于错误状态,那么就冻结使用 binding.readMenuBottom.getReadProgress().setEnabled( mPageLoader.getPageStatus() != TxtChapter.Status.LOADING && mPageLoader.getPageStatus() != TxtChapter.Status.ERROR ); } /** * 翻页成功 */ @Override public void onPageChange(int chapterIndex, int pageIndex, boolean resetReadAloud) { mPresenter.getBookShelf().setDurChapter(chapterIndex); mPresenter.getBookShelf().setDurChapterPage(pageIndex); mPresenter.saveProgress(); binding.readMenuBottom.getReadProgress().post( () -> binding.readMenuBottom.getReadProgress().setProgress(pageIndex) ); Long end = mPresenter.getDurChapter().getEnd(); int audioSize = end != null ? end.intValue() : 0; binding.mediaPlayerPop.upAudioSize(audioSize); binding.mediaPlayerPop.upAudioDur(mPresenter.getBookShelf().getDurChapterPage()); if (mPresenter.getBookShelf().isAudio() && mPageLoader.getPageStatus() == TxtChapter.Status.FINISH) { if (binding.mediaPlayerPop.getVisibility() != View.VISIBLE) { binding.mediaPlayerPop.setVisibility(View.VISIBLE); } } else { if (binding.mediaPlayerPop.getVisibility() == View.VISIBLE) { binding.mediaPlayerPop.setVisibility(View.GONE); } } if ((ReadAloudService.running)) { if (resetReadAloud) { readAloud(); return; } if (pageIndex == 0) { readAloud(); return; } } //启动朗读 if (getIntent().getBooleanExtra("readAloud", false) && pageIndex >= 0 && mPageLoader.getContent() != null) { getIntent().putExtra("readAloud", false); onMediaButton(ReadAloudService.ActionMediaPlay); return; } autoPage(); } @Override public void vipPop() { moDialogHUD.showTwoButton(ReadBookActivity.this.getString(R.string.donate_s), "领取红包", (v) -> { DonateActivity.getZfbHb(ReadBookActivity.this); mHandler.postDelayed(() -> { ReadBookActivity.this.refreshDurChapter(); moDialogHUD.dismiss(); }, 2000); }, "关注公众号", (v) -> { ClipboardManager clipboard = (ClipboardManager) ReadBookActivity.this.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = ClipData.newPlainText(null, "开源阅读软件"); if (clipboard != null) { clipboard.setPrimaryClip(clipData); toast("[开源阅读软件],已复制成功,可到微信搜索"); } MApplication.getInstance().upDonateHb(); mHandler.postDelayed(() -> { ReadBookActivity.this.refreshDurChapter(); moDialogHUD.dismiss(); }, 1000); }, true); } } ); mPageLoader.updateBattery(BatteryUtil.getLevel(this)); binding.pageView.setTouchListener(new PageView.TouchListener() { @Override public void onTouch() { screenOffTimerStart(); } @Override public void center() { popMenuIn(); } @Override public void onTouchClearCursor() { binding.cursorLeft.setVisibility(View.INVISIBLE); binding.cursorRight.setVisibility(View.INVISIBLE); binding.readLongPress.setVisibility(View.INVISIBLE); } @Override public void onLongPress() { if (!binding.pageView.isRunning()) { selectTextCursorShow(); showAction(binding.cursorLeft); } } }); mPageLoader.refreshChapterList(); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { if (v.getId() == R.id.cursor_left || v.getId() == R.id.cursor_right) { int ea = event.getAction(); switch (ea) { case MotionEvent.ACTION_DOWN: lastX = (int) event.getRawX();// 获取触摸事件触摸位置的原始X坐标 lastY = (int) event.getRawY(); binding.readLongPress.setVisibility(View.INVISIBLE); break; case MotionEvent.ACTION_MOVE: int dx = (int) event.getRawX() - lastX; int dy = (int) event.getRawY() - lastY; int l = v.getLeft() + dx; int b = v.getBottom() + dy; int r = v.getRight() + dx; int t = v.getTop() + dy; v.layout(l, t, r, b); lastX = (int) event.getRawX(); lastY = (int) event.getRawY(); v.postInvalidate(); //移动过程中要画线 binding.pageView.setSelectMode(PageView.SelectMode.SelectMoveForward); int hh = binding.cursorLeft.getHeight(); int ww = binding.cursorLeft.getWidth(); if (v.getId() == R.id.cursor_left) { binding.pageView.setFirstSelectTxtChar(binding.pageView.getCurrentTxtChar(lastX + ww, lastY - hh)); } else { binding.pageView.setLastSelectTxtChar(binding.pageView.getCurrentTxtChar(lastX - ww, lastY - hh)); } binding.pageView.invalidate(); break; case MotionEvent.ACTION_UP: showAction(v); //v.layout(l, t, r, b); break; default: break; } } return true; } public void showAction(View clickView) { binding.readLongPress.setVisibility(View.VISIBLE); //如果太靠右,则靠左 int[] aa = ScreenUtils.getScreenSize(this); if ((binding.cursorLeft.getX() + ScreenUtils.dpToPx(200)) > aa[0]) { binding.readLongPress.setX(aa[0] - ScreenUtils.dpToPx(200)); } else { binding.readLongPress.setX(binding.cursorLeft.getX() + binding.cursorLeft.getWidth() + ScreenUtils.dpToPx(5)); } //如果太靠上 if ((binding.cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()) - ScreenUtils.dpToPx(60)) < 0) { binding.readLongPress.setY(binding.cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize())); } else { binding.readLongPress.setY(binding.cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()) - ScreenUtils.dpToPx(40)); } } /** * 显示 */ private void selectTextCursorShow() { if (binding.pageView.getFirstSelectTxtChar() == null || binding.pageView.getLastSelectTxtChar() == null) return; //show Cursor on current position cursorShow(); //set current word selected binding.pageView.invalidate(); hideSnackBar(); } @SuppressWarnings("ConstantConditions") private void cursorShow() { binding.cursorLeft.setVisibility(View.VISIBLE); binding.cursorRight.setVisibility(View.VISIBLE); int hh = binding.cursorLeft.getHeight(); int ww = binding.cursorLeft.getWidth(); if (binding.pageView.getFirstSelectTxtChar() != null) { binding.cursorLeft.setX(binding.pageView.getFirstSelectTxtChar().getTopLeftPosition().x - ww); binding.cursorLeft.setY(binding.pageView.getFirstSelectTxtChar().getBottomLeftPosition().y); binding.cursorRight.setX(binding.pageView.getFirstSelectTxtChar().getBottomRightPosition().x); binding.cursorRight.setY(binding.pageView.getFirstSelectTxtChar().getBottomRightPosition().y); } } /** * 长按选择按钮 */ private void initReadLongPressPop() { binding.readLongPress.setListener(new ReadLongPressPop.OnBtnClickListener() { @Override public void copySelect() { ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = ClipData.newPlainText(null, binding.pageView.getSelectStr()); if (clipboard != null) { clipboard.setPrimaryClip(clipData); toast("所选内容已经复制到剪贴板"); } binding.cursorLeft.setVisibility(View.INVISIBLE); binding.cursorRight.setVisibility(View.INVISIBLE); binding.readLongPress.setVisibility(View.INVISIBLE); binding.pageView.clearSelect(); } @Override public void replaceSelect() { ReplaceRuleBean oldRuleBean = new ReplaceRuleBean(); oldRuleBean.setReplaceSummary(binding.pageView.getSelectStr().trim()); oldRuleBean.setEnable(true); oldRuleBean.setRegex(binding.pageView.getSelectStr().trim()); oldRuleBean.setIsRegex(false); oldRuleBean.setReplacement(""); oldRuleBean.setSerialNumber(0); oldRuleBean.setUseTo(String.format("%s,%s", mPresenter.getBookShelf().getBookInfoBean().getName(), mPresenter.getBookShelf().getTag())); ReplaceRuleDialog.builder(ReadBookActivity.this, oldRuleBean, mPresenter.getBookShelf(), ReplaceRuleDialog.DefaultUI) .setPositiveButton(replaceRuleBean1 -> ReplaceRuleManager.saveData(replaceRuleBean1) .subscribe(new MySingleObserver() { @Override public void onSuccess(@NonNull Boolean aBoolean) { binding.cursorLeft.setVisibility(View.INVISIBLE); binding.cursorRight.setVisibility(View.INVISIBLE); binding.readLongPress.setVisibility(View.INVISIBLE); binding.pageView.setSelectMode(PageView.SelectMode.Normal); moDialogHUD.dismiss(); refresh(false); } })).show(); } @Override public void replaceSelectAd() { String selectString = binding.pageView.getSelectStr(); if (selectString != null) { String spacer = null; String name = (mPresenter.getBookShelf().getBookInfoBean().getName()); if (name != null) if (name.trim().length() > 0) spacer = "|" + Pattern.quote(name.trim()); // spacer = "|" + Matcher.quoteReplacement(name.trim()); name = (mPresenter.getBookShelf().getBookInfoBean().getAuthor()); if (name != null) if (name.trim().length() > 0) if (spacer != null) spacer = spacer + "|" + Pattern.quote(name.trim()); else spacer = "|" + Pattern.quote(name.trim()); String rule = "(\\s*\n\\s*" + spacer + ")"; selectString = ReplaceRuleManager.formateAdRule( selectString.replaceAll(rule, "\n") ); } ReplaceRuleBean oldRuleBean = new ReplaceRuleBean(); oldRuleBean.setReplaceSummary(getString(R.string.replace_ad) + "-" + mPresenter.getBookShelf().getTag()); oldRuleBean.setEnable(true); oldRuleBean.setRegex(selectString); oldRuleBean.setIsRegex(false); oldRuleBean.setReplacement(""); oldRuleBean.setSerialNumber(0); oldRuleBean.setUseTo(mPresenter.getBookShelf().getTag()); ReplaceRuleDialog.builder(ReadBookActivity.this, oldRuleBean, mPresenter.getBookShelf(), ReplaceRuleDialog.AddAdUI) .setPositiveButton(replaceRuleBean1 -> ReplaceRuleManager.mergeAdRules(replaceRuleBean1) .subscribe(new MySingleObserver() { @Override public void onSuccess(@NonNull Boolean aBoolean) { binding.cursorLeft.setVisibility(View.INVISIBLE); binding.cursorRight.setVisibility(View.INVISIBLE); binding.readLongPress.setVisibility(View.INVISIBLE); binding.pageView.setSelectMode(PageView.SelectMode.Normal); moDialogHUD.dismiss(); refresh(false); } })).show(); } }); } /** * 设置ToolBar */ private void setupActionBar() { actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } /** * 添加菜单 */ @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_book_read_activity, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { this.menu = menu; upMenu(); return super.onPrepareOptionsMenu(menu); } /** * 菜单事件 */ @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.enable_replace) { mPresenter.getBookShelf().setReplaceEnable(!mPresenter.getBookShelf().getReplaceEnable()); refresh(false); } else if (id == R.id.action_change_source) { changeSource(); } else if (id == R.id.action_refresh) { refreshDurChapter(); } else if (id == R.id.action_download) { download(); } else if (id == R.id.add_bookmark) { showBookmark(null); } else if (id == R.id.action_copy_text) { popMenuOut(); if (mPageLoader != null) { moDialogHUD.showText(mPageLoader.getAllContent()); } } else if (id == R.id.disable_book_source) { mPresenter.disableDurBookSource(); } else if (id == R.id.action_book_info) { BookInfoEditActivity.startThis(this, mPresenter.getBookShelf().getNoteUrl()); } else if (id == R.id.action_set_charset) { setCharset(); } else if (id == R.id.update_chapter_list) { if (mPageLoader != null) { mPageLoader.updateChapter(); } } else if (id == R.id.action_set_regex) { setTextChapterRegex(); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } private void login() { BookSourceBean source = mPresenter.getBookSource(); if (TextUtils.isEmpty(source.getLoginUi())) { SourceLoginActivity.startThis(this, source); } else { SourceLoginDialog.Companion.start( getSupportFragmentManager(), source.getBookSourceUrl() ); } } private void pay() { BookShelfBean book = mPresenter.getBookShelf(); BookChapterBean chapter = mPresenter.getDurChapter(); BookSourceBean source = mPresenter.getBookSource(); String payRule = source.getPayAction(); if (chapter.getIsVip() && !chapter.getIsPay() && !TextUtils.isEmpty(payRule)) { String result = ""; if (payRule.startsWith("http")) { result = payRule; } else { try { SimpleBindings bindings = new SimpleBindings(); bindings.put("java", source); bindings.put("source", source); bindings.put("book", book); bindings.put("chapter", chapter); result = SCRIPT_ENGINE.eval(payRule, bindings).toString(); } catch (Exception e) { e.printStackTrace(); } } if (result.startsWith("http")) { Intent webIntent = new Intent(this, WebViewActivity.class); webIntent.putExtra("url", result); webIntent.putExtra("title", "购买"); BitIntentDataManager.getInstance().putData(result, mPresenter.getBookSource().getHeaderMap(true)); //noinspection deprecation startActivityForResult(webIntent, payActivityRequest); } } } /** * 刷新当前章节 */ private void refreshDurChapter() { if (!NetworkUtils.isNetWorkAvailable()) { toast("网络不可用,无法刷新当前章节!"); return; } ReadBookActivity.this.popMenuOut(); if (mPageLoader != null) { if (mPageLoader instanceof PageLoaderNet) { ((PageLoaderNet) mPageLoader).refreshDurChapter(); } } } /** * 书签 */ @Override public void showBookmark(BookmarkBean bookmarkBean) { this.popMenuOut(); boolean isAdd = false; if (mPresenter.getBookShelf() != null) { if (bookmarkBean == null) { isAdd = true; bookmarkBean = new BookmarkBean(); bookmarkBean.setNoteUrl(mPresenter.getBookShelf().getNoteUrl()); bookmarkBean.setBookName(mPresenter.getBookShelf().getBookInfoBean().getName()); bookmarkBean.setChapterIndex(mPresenter.getBookShelf().getDurChapter()); bookmarkBean.setPageIndex(mPresenter.getBookShelf().getDurChapterPage()); bookmarkBean.setChapterName(mPresenter.getBookShelf().getDurChapterName()); } BookmarkDialog.builder(this, bookmarkBean, isAdd) .setPositiveButton(new BookmarkDialog.Callback() { @Override public void saveBookmark(BookmarkBean bookmarkBean) { mPresenter.saveBookmark(bookmarkBean); } @Override public void delBookmark(BookmarkBean bookmarkBean) { mPresenter.delBookmark(bookmarkBean); } @Override public void openChapter(int chapterIndex, int pageIndex) { skipToChapter(chapterIndex, pageIndex); } }).show(); } } @Override public void skipToChapter(int chapterIndex, int pageIndex) { if (mPageLoader != null) { mPageLoader.skipToChapter(chapterIndex, pageIndex); } } /** * 自动换源 */ public void autoChangeSource() { mPresenter.autoChangeSource(); } /** * 换源 */ private void changeSource() { if (!NetworkUtils.isNetWorkAvailable()) { toast(R.string.network_connection_unavailable); return; } ReadBookActivity.this.popMenuOut(); if (mPresenter.getBookShelf() != null) { ChangeSourceDialog.builder(this, mPresenter.getBookShelf()) .setCallback(searchBookBean -> { if (!Objects.equals(searchBookBean.getNoteUrl(), mPresenter.getBookShelf().getNoteUrl())) { if (mPageLoader != null) { mPageLoader.setStatus(TxtChapter.Status.CHANGE_SOURCE); } mPresenter.changeBookSource(searchBookBean); } }).show(); } } /** * 下载 */ private void download() { if (!NetworkUtils.isNetWorkAvailable()) { toast(R.string.network_connection_unavailable); return; } ReadBookActivity.this.popMenuOut(); if (mPresenter.getBookShelf() != null) { //弹出离线下载界面 int endIndex = mPresenter.getBookShelf().getChapterListSize() - 1; DownLoadDialog.builder(this, mPresenter.getBookShelf().getDurChapter(), endIndex, mPresenter.getBookShelf().getChapterListSize()) .setPositiveButton((start, end) -> mPresenter.addDownload(start, end)).show(); } } /** * 设置编码 */ private void setCharset() { final String charset = mPresenter.getBookShelf().getBookInfoBean().getCharset(); String[] a = new String[]{"UTF-8", "GB2312", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII"}; List values = new ArrayList<>(Arrays.asList(a)); InputDialog.builder(this) .setTitle(getString(R.string.input_charset)) .setDefaultValue(charset) .setAdapterValues(values) .setCallback(new InputDialog.Callback() { @Override public void setInputText(String inputText) { inputText = inputText.trim(); if (!Objects.equals(charset, inputText)) { mPresenter.getBookShelf().getBookInfoBean().setCharset(inputText); mPresenter.saveProgress(); if (mPageLoader != null) { mPageLoader.updateChapter(); } } } @Override public void delete(String value) { } }).show(); } /** * 设置TXT目录正则 */ private void setTextChapterRegex() { if (mPresenter.getBookShelf().getNoteUrl().toLowerCase().matches(".*\\.txt")) { int checkedItem = 0; List ruleBeanList = TxtChapterRuleManager.getEnabled(); List ruleNameList = new ArrayList<>(); String rule = mPresenter.getBookShelf().getBookInfoBean().getChapterUrl(); if (!TextUtils.isEmpty(rule)) { TxtChapterRuleBean ruleBean = DbHelper.getDaoSession().getTxtChapterRuleBeanDao().queryBuilder() .where(TxtChapterRuleBeanDao.Properties.Rule.eq(rule)) .limit(1).unique(); if (ruleBean != null) { if (!ruleBean.getEnable()) { ruleBeanList.add(ruleBean); checkedItem = ruleBeanList.size() - 1; } else { checkedItem = ruleBeanList.indexOf(ruleBean); } } else { ruleBean = new TxtChapterRuleBean(); ruleBean.setName(rule); ruleBean.setRule(rule); ruleBeanList.add(ruleBean); checkedItem = ruleBeanList.size() - 1; } } for (TxtChapterRuleBean bean : ruleBeanList) { ruleNameList.add(bean.getName()); } if (checkedItem < 0) { checkedItem = 0; } AlertDialog dialog = new AlertDialog.Builder(this, R.style.alertDialogTheme) .setTitle("选择目录正则") .setSingleChoiceItems(ruleNameList.toArray(new String[0]), checkedItem, (dialog1, which) -> { if (which < 0) return; mPresenter.getBookShelf().getBookInfoBean().setChapterUrl(ruleBeanList.get(which).getRule()); mPresenter.saveProgress(); if (mPageLoader != null) { mPageLoader.updateChapter(); } dialog1.dismiss(); }) .setPositiveButton("管理正则", (dialog12, which) -> TxtChapterRuleActivity.startThis(ReadBookActivity.this)) .show(); ATH.setAlertDialogTint(dialog); } } /** * 显示调节 */ private void readAdjustIn() { binding.flMenu.setVisibility(View.VISIBLE); binding.readAdjustPop.show(); binding.readAdjustPop.setVisibility(View.VISIBLE); binding.readAdjustPop.startAnimation(menuBottomIn); } /** * 显示自定义边界调节 */ public void readAdjustMarginIn() { binding.flMenu.setVisibility(View.VISIBLE); binding.readAdjustMarginPop.show(); binding.readAdjustMarginPop.setVisibility(View.VISIBLE); binding.readAdjustMarginPop.startAnimation(menuBottomIn); } /** * 显示界面设置 */ private void readInterfaceIn() { binding.flMenu.setVisibility(View.VISIBLE); binding.readInterfacePop.setVisibility(View.VISIBLE); binding.readInterfacePop.startAnimation(menuBottomIn); } /** * 显示更多设置 */ private void moreSettingIn() { binding.flMenu.setVisibility(View.VISIBLE); binding.moreSettingPop.setVisibility(View.VISIBLE); binding.moreSettingPop.startAnimation(menuBottomIn); } /** * 显示菜单 */ private void popMenuIn() { binding.flMenu.setVisibility(View.VISIBLE); binding.llMenuTop.setVisibility(View.VISIBLE); binding.readMenuBottom.setVisibility(View.VISIBLE); binding.llMenuTop.startAnimation(menuTopIn); binding.readMenuBottom.startAnimation(menuBottomIn); hideSnackBar(); } /** * 隐藏菜单 */ private void popMenuOut() { if (binding.flMenu.getVisibility() == View.VISIBLE) { if (binding.llMenuTop.getVisibility() == View.VISIBLE) { binding.llMenuTop.startAnimation(menuTopOut); } if (binding.readMenuBottom.getVisibility() == View.VISIBLE) { binding.readMenuBottom.startAnimation(menuBottomOut); } if (binding.moreSettingPop.getVisibility() == View.VISIBLE) { binding.moreSettingPop.startAnimation(menuBottomOut); } if (binding.readAdjustPop.getVisibility() == View.VISIBLE) { binding.readAdjustPop.startAnimation(menuBottomOut); } if (binding.readInterfacePop.getVisibility() == View.VISIBLE) { binding.readInterfacePop.startAnimation(menuBottomOut); } if (binding.readAdjustMarginPop.getVisibility() == View.VISIBLE) { binding.readAdjustMarginPop.startAnimation(menuBottomOut); } } } /** * 朗读 */ private void readAloud() { aloudNextPage = false; String unReadContent = mPageLoader.getUnReadContent(); if (mPresenter.getBookShelf().isAudio()) { try { unReadContent = new AnalyzeUrl(unReadContent, mPresenter.getBookSource().getBookSourceUrl(), mPresenter.getBookSource(), mPresenter.getBookSource().getHeaderMap(true)).getRuleUrl(); } catch (Exception e) { e.printStackTrace(); } } if (mPresenter.getBookShelf() != null && mPageLoader != null && !StringUtils.isTrimEmpty(unReadContent)) { ReadAloudService.play(ReadBookActivity.this, false, unReadContent, mPresenter.getBookShelf().getBookInfoBean().getName(), ChapterContentHelp.getInstance().replaceContent(mPresenter.getBookShelf().getBookInfoBean().getName(), mPresenter.getBookShelf().getTag(), mPresenter.getBookShelf().getDurChapterName(), mPresenter.getBookShelf().getReplaceEnable()), mPresenter.getBookShelf().isAudio(), mPresenter.getBookShelf().getDurChapterPage()); } } /** * 检查是否加入书架 */ public boolean checkAddShelf() { if (isAdd || mPresenter.getBookShelf() == null || TextUtils.isEmpty(mPresenter.getBookShelf().getBookInfoBean().getName())) { return true; } else if (mPresenter.getChapterList().isEmpty()) { mPresenter.removeFromShelf(); return true; } else { if (checkAddShelfPop == null) { checkAddShelfPop = new CheckAddShelfPop(this, mPresenter.getBookShelf().getBookInfoBean().getName(), new CheckAddShelfPop.OnItemClickListener() { @Override public void clickExit() { mPresenter.removeFromShelf(); } @Override public void clickAddShelf() { mPresenter.addToShelf(null); checkAddShelfPop.dismiss(); } }); } if (!checkAddShelfPop.isShowing()) { checkAddShelfPop.showAtLocation(binding.flContent, Gravity.CENTER, 0, 0); } return false; } } /** * 更新朗读状态 */ @Override public void upAloudState(ReadAloudService.Status status) { aloudStatus = status; autoPageStop(); switch (status) { case NEXT: if (mPageLoader == null) { ReadAloudService.stop(this); break; } if (!mPageLoader.skipNextChapter()) { ReadAloudService.stop(this); } break; case PLAY: binding.readMenuBottom.setFabReadAloudImage(R.drawable.ic_pause_outline_24dp); binding.readMenuBottom.setReadAloudTimer(true); binding.mediaPlayerPop.setFabReadAloudImage(R.drawable.ic_pause_24dp); binding.mediaPlayerPop.setSeekBarEnable(true); break; case PAUSE: binding.readMenuBottom.setFabReadAloudImage(R.drawable.ic_play_outline_24dp); binding.readMenuBottom.setReadAloudTimer(true); binding.mediaPlayerPop.setFabReadAloudImage(R.drawable.ic_play_24dp); binding.mediaPlayerPop.setSeekBarEnable(false); break; default: binding.readMenuBottom.setFabReadAloudImage(R.drawable.ic_read_aloud); binding.readMenuBottom.setReadAloudTimer(false); binding.mediaPlayerPop.setFabReadAloudImage(R.drawable.ic_play_24dp); binding.pageView.drawPage(0); binding.pageView.invalidate(); binding.pageView.drawPage(-1); binding.pageView.drawPage(1); binding.pageView.invalidate(); } } /** * 更新定时 */ @Override public void upAloudTimer(String text) { binding.readMenuBottom.setReadAloudTimer(text); } /** * 开始朗读第start个字符 */ @Override public void readAloudStart(int start) { aloudNextPage = true; if (mPageLoader != null) { mPageLoader.readAloudStart(start); } } /** * 朗读长度 */ @Override public void readAloudLength(int readAloudLength) { if (mPageLoader != null && aloudNextPage) { mPageLoader.readAloudLength(readAloudLength); } } /** * 刷新 */ @Override public void refresh(boolean recreate) { if (recreate) { recreate(); } else { binding.flContent.setBackground(readBookControl.getTextBackground(this)); if (mPageLoader != null) { mPageLoader.refreshUi(); } binding.readInterfacePop.setBg(); initImmersionBar(); } } /** * 按键事件 */ @Override public boolean dispatchKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); int action = event.getAction(); boolean isDown = action == 0; if (keyCode == KeyEvent.KEYCODE_MENU) { return isDown ? this.onKeyDown(keyCode, event) : this.onKeyUp(keyCode, event); } return super.dispatchKeyEvent(event); } /** * 按键事件 */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Boolean mo = moDialogHUD.onKeyDown(keyCode, event); if (mo) { return true; } else { if (keyCode == KeyEvent.KEYCODE_BACK) { if (binding.readInterfacePop.getVisibility() == View.VISIBLE || binding.readAdjustPop.getVisibility() == View.VISIBLE || binding.readAdjustMarginPop.getVisibility() == View.VISIBLE || binding.moreSettingPop.getVisibility() == View.VISIBLE) { popMenuOut(); return true; } else if (binding.flMenu.getVisibility() == View.VISIBLE) { finish(); return true; } else if (ReadAloudService.running && aloudStatus == ReadAloudService.Status.PLAY) { ReadAloudService.pause(this); if (!mPresenter.getBookShelf().isAudio()) { toast(R.string.read_aloud_pause); } return true; } else { finish(); return true; } } else if (keyCode == KeyEvent.KEYCODE_MENU) { if (binding.flMenu.getVisibility() == View.VISIBLE) { popMenuOut(); } else { popMenuIn(); } return true; } else if (binding.flMenu.getVisibility() != View.VISIBLE) { if (keyCode == preferences.getInt("nextKeyCode", 0)) { if (mPageLoader != null && keyCode != 0) { mPageLoader.skipToNextPage(); } return true; } if (keyCode == preferences.getInt("prevKeyCode", 0)) { if (mPageLoader != null && keyCode != 0) { mPageLoader.skipToPrePage(); } return true; } if (readBookControl.getCanKeyTurn(aloudStatus == ReadAloudService.Status.PLAY) && keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (mPageLoader != null) { mPageLoader.skipToNextPage(); } return true; } else if (readBookControl.getCanKeyTurn(aloudStatus == ReadAloudService.Status.PLAY) && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { if (mPageLoader != null) { mPageLoader.skipToPrePage(); } return true; } else if (keyCode == KeyEvent.KEYCODE_SPACE) { nextPage(); return true; } } return super.onKeyDown(keyCode, event); } } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (binding.flMenu.getVisibility() != View.VISIBLE) { if (readBookControl.getCanKeyTurn(aloudStatus == ReadAloudService.Status.PLAY) && keyCode != 0 && (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == preferences.getInt("nextKeyCode", 0) || keyCode == preferences.getInt("prevKeyCode", 0))) { return true; } } return super.onKeyUp(keyCode, event); } /** * 更新菜单 */ @Override public void upMenu() { if (menu == null) return; boolean onLine = mPresenter.getBookShelf() != null && !mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG); if (onLine) { binding.tvChapterUrl.setVisibility(View.VISIBLE); binding.atvLine.setVisibility(View.VISIBLE); } else { binding.tvChapterUrl.setVisibility(View.GONE); binding.atvLine.setVisibility(View.GONE); } for (int i = 0; i < menu.size(); i++) { int groupId = menu.getItem(i).getGroupId(); if (groupId == R.id.menuOnLine) { menu.getItem(i).setVisible(onLine); menu.getItem(i).setEnabled(onLine); } else if (groupId == R.id.menuLocal) { menu.getItem(i).setVisible(!onLine); menu.getItem(i).setEnabled(!onLine); } else if (groupId == R.id.menu_text) { boolean isTxt = mPresenter.getBookShelf() != null && mPresenter.getBookShelf().getNoteUrl().toLowerCase().endsWith(".txt"); menu.getItem(i).setVisible(isTxt); menu.getItem(i).setEnabled(isTxt); } if (menu.getItem(i).getItemId() == R.id.enable_replace) { menu.getItem(i).setChecked(mPresenter.getBookShelf() != null && mPresenter.getBookShelf().getReplaceEnable()); } } } /** * 更新音频长度 */ @Override public void upAudioSize(int audioSize) { binding.mediaPlayerPop.upAudioSize(audioSize); } /** * 更新播放进度 */ @Override public void upAudioDur(int audioDur) { binding.mediaPlayerPop.upAudioDur(audioDur); mPresenter.getBookShelf().setDurChapterPage(audioDur); mPresenter.saveProgress(); } @Override public String getNoteUrl() { return noteUrl; } @Override public Boolean getAdd() { return isAdd; } @Override public void setAdd(Boolean isAdd) { this.isAdd = isAdd; } @Override public void openBookFromOther() { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.please_grant_storage_permission) .onGranted((requestCode) -> { mPresenter.openBookFromOther(ReadBookActivity.this); return Unit.INSTANCE; }) .request(); } /** * 朗读按钮 */ @Override public void onMediaButton(String cmd) { if (!ReadAloudService.running) { aloudStatus = ReadAloudService.Status.STOP; SystemUtil.ignoreBatteryOptimization(this); } switch (aloudStatus) { case PAUSE: switch (cmd) { case ReadAloudService.ActionMediaPlay: ReadAloudService.resume(this); binding.readMenuBottom.setFabReadAloudText(getString(R.string.read_aloud)); break; case ReadAloudService.ActionMediaPrev: //停止倒计时 ReadAloudService.setTimer(getContext(), ReadAloudService.maxTimeMinute + 1); //语音提示倒计时结束 ReadAloudService.tts_ui_timer_stop(this); break; case ReadAloudService.ActionMediaNext: //翻到上一章并开始朗读 if (mPageLoader != null) { mPageLoader.skipPreChapter(); } ReadAloudService.resume(this); binding.readMenuBottom.setFabReadAloudText(getString(R.string.read_aloud)); break; } break; case PLAY: switch (cmd) { case ReadAloudService.ActionMediaPlay: ReadAloudService.pause(this); binding.readMenuBottom.setFabReadAloudText(getString(R.string.read_aloud_pause)); break; case ReadAloudService.ActionMediaPrev: //倒计时增加 ReadAloudService.setTimer(getContext(), 10); //语音提示剩余时间 ReadAloudService.tts_ui_timer_remaining(this); break; case ReadAloudService.ActionMediaNext: //翻到下一章 if (mPageLoader != null) { mPageLoader.skipNextChapter(); } break; } break; default: ReadBookActivity.this.popMenuOut(); readAloud(); } } public void selectFontDir() { try { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //noinspection deprecation startActivityForResult(intent, fontDirRequest); } catch (Exception e) { e.printStackTrace(); toast(e.getLocalizedMessage()); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == fontDirRequest && resultCode == RESULT_OK) { if (data != null) { Uri uri = data.getData(); if (uri != null) { int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; getContentResolver().takePersistableUriPermission(uri, modeFlags); binding.readInterfacePop.showFontSelector(uri); } } } initImmersionBar(); } @SuppressLint("DefaultLocale") @Override protected void onResume() { super.onResume(); SoftInputUtil.hideIMM(getCurrentFocus()); if (batInfoReceiver == null) { batInfoReceiver = new ThisBatInfoReceiver(); batInfoReceiver.registerThis(); } screenOffTimerStart(); if (mPageLoader != null) { if (!mPageLoader.updateBattery(BatteryUtil.getLevel(this))) { mPageLoader.updateTime(); } } } @Override protected void onPause() { super.onPause(); if (batInfoReceiver != null) { batInfoReceiver.unregisterThis(); } } @Override protected void onDestroy() { super.onDestroy(); if (batInfoReceiver != null) { batInfoReceiver.unregisterThis(); } ReadAloudService.stop(this); if (mPageLoader != null) { mPageLoader.closeBook(); mPageLoader = null; } } /** * 结束 */ @Override public void finish() { if (!checkAddShelf()) { return; } if (!AppActivityManager.getInstance().isExist(MainActivity.class)) { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); } Backup.INSTANCE.autoBack(); super.finish(); } @Override public void changeSourceFinish(BookShelfBean book) { if (mPageLoader != null && mPageLoader instanceof PageLoaderNet) { ((PageLoaderNet) mPageLoader).changeSourceFinish(book); } } /** * 时间和电量广播 */ class ThisBatInfoReceiver extends BroadcastReceiver { @SuppressLint("DefaultLocale") @Override public void onReceive(Context context, Intent intent) { if (readBookControl.getHideStatusBar()) { if (Intent.ACTION_TIME_TICK.equals(intent.getAction())) { if (mPageLoader != null) { mPageLoader.updateTime(); } } else if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) { int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); if (mPageLoader != null) { mPageLoader.updateBattery(level); } } } } public void registerThis() { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_TIME_TICK); filter.addAction(Intent.ACTION_BATTERY_CHANGED); ReadBookActivity.this.registerReceiver(batInfoReceiver, filter); } public void unregisterThis() { ReadBookActivity.this.unregisterReceiver(batInfoReceiver); batInfoReceiver = null; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ReadStyleActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import androidx.appcompat.app.ActionBar; import com.hwangjr.rxbus.RxBus; import com.jaredrummler.android.colorpicker.ColorPickerDialog; import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.ActivityReadStyleBinding; import com.kunfei.bookshelf.help.ReadBookControl; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.utils.ActivityExtensionsKt; import com.kunfei.bookshelf.utils.BitmapUtil; import com.kunfei.bookshelf.utils.ContextExtensionsKt; import com.kunfei.bookshelf.utils.MeUtils; import com.kunfei.bookshelf.utils.RealPathUtil; import com.kunfei.bookshelf.widget.filepicker.picker.FilePicker; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import kotlin.Unit; public class ReadStyleActivity extends MBaseActivity implements ColorPickerDialogListener { private final int ResultSelectBg = 103; private final int SELECT_TEXT_COLOR = 201; private final int SELECT_BG_COLOR = 301; private ActivityReadStyleBinding binding; private ReadBookControl readBookControl = ReadBookControl.getInstance(); private int textDrawableIndex; private int textColor; private int bgColor; private Drawable bgDrawable; private int bgCustom; private boolean darkStatusIcon; private String bgPath; private BgImgListAdapter bgImgListAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } /** * 布局载入 setContentView() */ @Override protected void onCreateActivity() { binding = ActivityReadStyleBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); binding.llContent.setPadding(0, ContextExtensionsKt.getStatusBarHeight(this), 0, 0); this.setSupportActionBar(binding.toolbar); setupActionBar(); setTextKind(readBookControl); } @Override protected void initImmersionBar() { super.initImmersionBar(); ActivityExtensionsKt.setLightStatusBar(this, darkStatusIcon); } /** * 数据初始化 */ @Override protected void initData() { Intent intent = getIntent(); textDrawableIndex = intent.getIntExtra("index", 1); bgCustom = readBookControl.getBgCustom(textDrawableIndex); textColor = readBookControl.getTextColor(textDrawableIndex); Resources resources = this.getResources(); DisplayMetrics dm = resources.getDisplayMetrics(); int width = dm.widthPixels; int height = dm.heightPixels; bgDrawable = readBookControl.getBgDrawable(textDrawableIndex, getContext(), width, height); bgColor = readBookControl.getBgColor(textDrawableIndex); darkStatusIcon = readBookControl.getDarkStatusIcon(textDrawableIndex); bgPath = readBookControl.getBgPath(textDrawableIndex); upText(); upBg(); } /** * 事件触发绑定 */ @Override protected void bindEvent() { binding.swDarkStatusIcon.setChecked(darkStatusIcon); binding.swDarkStatusIcon.setOnCheckedChangeListener((compoundButton, b) -> { darkStatusIcon = b; initImmersionBar(); }); //文字背景点击事件 binding.llContent.setOnClickListener((view) -> { if (binding.llBottom.getVisibility() == View.GONE) { binding.llBottom.setVisibility(View.VISIBLE); } else { binding.llBottom.setVisibility(View.GONE); } }); //选择文字颜色 binding.tvSelectTextColor.setOnClickListener(view -> ColorPickerDialog.newBuilder() .setColor(textColor) .setShowAlphaSlider(false) .setDialogType(ColorPickerDialog.TYPE_CUSTOM) .setDialogId(SELECT_TEXT_COLOR) .show(ReadStyleActivity.this)); //选择背景颜色 binding.tvSelectBgColor.setOnClickListener(view -> ColorPickerDialog.newBuilder() .setColor(bgColor) .setShowAlphaSlider(false) .setDialogType(ColorPickerDialog.TYPE_CUSTOM) .setDialogId(SELECT_BG_COLOR) .show(ReadStyleActivity.this)); //背景图列表 bgImgListAdapter = new BgImgListAdapter(this); bgImgListAdapter.initList(); binding.bgImgList.setAdapter(bgImgListAdapter); binding.bgImgList.setOnItemClickListener((adapterView, view, i, l) -> { if (i == 0) { selectImage(); } else { bgPath = bgImgListAdapter.getItemAssetsFile(i - 1); setAssetsBg(bgPath); } }); //选择背景图片 binding.tvSelectBgImage.setOnClickListener(view -> selectImage()); //恢复默认 binding.tvDefault.setOnClickListener(view -> { bgCustom = 0; textColor = readBookControl.getDefaultTextColor(textDrawableIndex); bgDrawable = readBookControl.getDefaultBgDrawable(textDrawableIndex, this); upText(); upBg(); }); } private void selectImage() { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.bg_image_per) .onGranted((requestCode) -> { selectImageDialog(); return Unit.INSTANCE; }) .request(); } private void selectImageDialog() { try { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); startActivityForResult(intent, ResultSelectBg); } catch (Exception e) { FilePicker picker = new FilePicker(this, FilePicker.FILE); picker.setBackgroundColor(getResources().getColor(R.color.background)); picker.setTopBackgroundColor(getResources().getColor(R.color.background)); picker.setItemHeight(30); picker.setOnFilePickListener(this::setCustomBg); picker.show(); } } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.read_style); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_read_style_activity, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_save) { saveStyle(); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } /** * 保存配置 */ private void saveStyle() { readBookControl.setTextColor(textDrawableIndex, textColor); readBookControl.setBgCustom(textDrawableIndex, bgCustom); readBookControl.setBgColor(textDrawableIndex, bgColor); readBookControl.setDarkStatusIcon(textDrawableIndex, darkStatusIcon); if (bgCustom == 2 || bgCustom == 3) { readBookControl.setBgPath(textDrawableIndex, bgPath); } readBookControl.initTextDrawableIndex(); RxBus.get().post(RxBusTag.UPDATE_READ, false); finish(); } private void setTextKind(ReadBookControl readBookControl) { binding.tvContent.setTextSize(readBookControl.getTextSize()); } private void upText() { binding.tvContent.setTextColor(textColor); } private void upBg() { binding.llContent.setBackground(bgDrawable); } /** * 自定义背景 */ public void setCustomBg(String bgPath) { try { Resources resources = this.getResources(); DisplayMetrics dm = resources.getDisplayMetrics(); int width = dm.widthPixels; int height = dm.heightPixels; Bitmap bitmap = BitmapUtil.getFitSampleBitmap(bgPath, width, height); bgCustom = 2; bgDrawable = new BitmapDrawable(getResources(), bitmap); upBg(); } catch (Exception e) { e.printStackTrace(); toast(e.getMessage(), ERROR); } } public void setAssetsBg(String path) { try { Resources resources = ReadStyleActivity.this.getResources(); DisplayMetrics dm = resources.getDisplayMetrics(); int width = dm.widthPixels; int height = dm.heightPixels; Bitmap bitmap = MeUtils.getFitAssetsSampleBitmap(ReadStyleActivity.this.getAssets(), path, width, height); bgCustom = 3; bgDrawable = new BitmapDrawable(getResources(), bitmap); upBg(); } catch (Exception e) { e.printStackTrace(); toast(e.getMessage(), ERROR); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == ResultSelectBg) { if (resultCode == RESULT_OK && null != data) { try { bgPath = RealPathUtil.getPath(this, data.getData()); setCustomBg(bgPath); } catch (Exception ignored) { } } } } /** * Callback that is invoked when a color is selected from the color picker dialog. * @param dialogId The dialog id used to create the dialog instance. * @param color The selected color */ @Override public void onColorSelected(int dialogId, int color) { switch (dialogId) { case SELECT_TEXT_COLOR: textColor = color; upText(); break; case SELECT_BG_COLOR: bgCustom = 1; bgColor = color; bgDrawable = new ColorDrawable(bgColor); upBg(); } } /** * Callback that is invoked when the color picker dialog was dismissed. * @param dialogId The dialog id used to create the dialog instance. */ @Override public void onDialogDismissed(int dialogId) { } private static class BgImgListAdapter extends BaseAdapter { private final Context context; private final LayoutInflater mInflater; private List assetsFiles; final BitmapFactory.Options options = new BitmapFactory.Options(); BgImgListAdapter(Context context) { this.context = context; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); options.inJustDecodeBounds = false; options.inSampleSize = 4; } void initList() { AssetManager am = context.getAssets(); String[] path; try { path = am.list("bg"); //获取所有,填入目录获取该目录下所有资源 } catch (IOException e) { e.printStackTrace(); return; } assetsFiles = new ArrayList<>(); Collections.addAll(assetsFiles, path); } @Override public int getCount() { return assetsFiles.size() + 1; } @Override public Object getItem(int position) { return position; } @Override public long getItemId(int position) { return position; } String getItemAssetsFile(int position) { return "bg/" + assetsFiles.get(position); } @SuppressLint("InflateParams") @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if(convertView==null){ holder = new ViewHolder(); convertView = mInflater.inflate(R.layout.item_read_bg, null); holder.mImage = convertView.findViewById(R.id.iv_bg); holder.mTitle = convertView.findViewById(R.id.tv_desc); convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } if (position == 0) { holder.mTitle.setText("选择背景"); holder.mTitle.setTextColor(Color.parseColor("#101010")); holder.mImage.setImageBitmap(BitmapFactory.decodeResource(context.getResources(), R.drawable.icon_image)); } else { String path = assetsFiles.get(position - 1); holder.mTitle.setText(getFileName(path)); holder.mTitle.setTextColor(Color.parseColor("#909090")); try { BitmapDrawable bitmapDrawable = (BitmapDrawable) holder.mImage.getDrawable(); //如果图片还未回收,先强制回收该图片 if (bitmapDrawable != null && !bitmapDrawable.getBitmap().isRecycled()) { bitmapDrawable.getBitmap().recycle(); } //该变现实的图片 Bitmap bmp = MeUtils.getFitAssetsSampleBitmap(context.getAssets(), getItemAssetsFile(position - 1), 256, 256); holder.mImage.setImageBitmap(bmp); } catch (Exception e) { e.printStackTrace(); holder.mImage.setImageBitmap(null); } } return convertView; } String getFileName(String path) { int start = path.lastIndexOf("/"); int end = path.lastIndexOf("."); if (end < 0) end = path.length(); return path.substring(start + 1, end); } private static class ViewHolder { private TextView mTitle ; private ImageView mImage; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ReceivingSharedActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.utils.StringUtils; public class ReceivingSharedActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String action = getIntent().getAction(); String type = getIntent().getType(); if (Intent.ACTION_SEND.equals(action) && type != null) { if ("text/plain".equals(type)) { String text = getIntent().getStringExtra(Intent.EXTRA_TEXT); if (openUrl(text)) { SearchBookActivity.startByKey(this, text); } finish(); return; } } if (Intent.ACTION_PROCESS_TEXT.equals(action) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && type != null) { if ("text/plain".equals(type)) { String text = getIntent().getStringExtra(Intent.EXTRA_PROCESS_TEXT); if (openUrl(text)) { SearchBookActivity.startByKey(this, text); } finish(); return; } } finish(); } private boolean openUrl(String text) { if (StringUtils.isTrimEmpty(text)) { return false; } String[] urls = text.split("\\s"); StringBuilder result = new StringBuilder(); for (String url : urls) { if (url.matches("http.+")) result.append("\n").append(url.trim()); } if (result.length() > 1) { MApplication.getConfigPreferences().edit() .putString("shared_url", result.toString()) .apply(); Intent intent = new Intent(); intent.setClass(ReceivingSharedActivity.this, MainActivity.class); this.startActivity(intent); return false; } else { return true; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ReplaceRuleActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.widget.LinearLayout; import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.snackbar.Snackbar; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.ActivityRecyclerVewBinding; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.model.ReplaceRuleManager; import com.kunfei.bookshelf.presenter.ReplaceRulePresenter; import com.kunfei.bookshelf.presenter.contract.ReplaceRuleContract; import com.kunfei.bookshelf.utils.ACache; import com.kunfei.bookshelf.utils.RealPathUtil; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.ReplaceRuleAdapter; import com.kunfei.bookshelf.widget.filepicker.picker.FilePicker; import com.kunfei.bookshelf.widget.modialog.InputDialog; import com.kunfei.bookshelf.widget.modialog.MoDialogHUD; import com.kunfei.bookshelf.widget.modialog.ReplaceRuleDialog; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import kotlin.Unit; /** * Created by GKF on 2017/12/16. * 书源管理 */ public class ReplaceRuleActivity extends MBaseActivity implements ReplaceRuleContract.View { private final int IMPORT_SOURCE = 102; private ActivityRecyclerVewBinding binding; private BookShelfBean bookShelfBean; private MoDialogHUD moDialogHUD; private ReplaceRuleAdapter adapter; private boolean selectAll = true; public static void startThis(Context context, BookShelfBean shelfBean) { String key = String.valueOf(System.currentTimeMillis()); Intent intent = new Intent(context, ReplaceRuleActivity.class); BitIntentDataManager.getInstance().putData(key, shelfBean); intent.putExtra("data_key", key); context.startActivity(intent); } @Override protected ReplaceRuleContract.Presenter initInjector() { return new ReplaceRulePresenter(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityRecyclerVewBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void initData() { String dataKey = getIntent().getStringExtra("data_key"); if (!TextUtils.isEmpty(dataKey)) { bookShelfBean = (BookShelfBean) BitIntentDataManager.getInstance().getData(dataKey); } this.setSupportActionBar(binding.toolbar); setupActionBar(); initRecyclerView(); moDialogHUD = new MoDialogHUD(this); refresh(); } private void initRecyclerView() { binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL)); adapter = new ReplaceRuleAdapter(this); binding.recyclerView.setAdapter(adapter); ItemTouchCallback itemTouchCallback = new ItemTouchCallback(); itemTouchCallback.setOnItemTouchCallbackListener(adapter.getItemTouchCallbackListener()); itemTouchCallback.setDragEnable(true); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback); itemTouchHelper.attachToRecyclerView(binding.recyclerView); } public void editReplaceRule(ReplaceRuleBean replaceRuleBean) { ReplaceRuleDialog.builder(this, replaceRuleBean, bookShelfBean) .setPositiveButton(replaceRuleBean1 -> ReplaceRuleManager.saveData(replaceRuleBean1) .subscribe(new MySingleObserver() { @Override public void onSuccess(Boolean aBoolean) { refresh(); } })).show(); } public void upDateSelectAll() { selectAll = true; for (ReplaceRuleBean replaceRuleBean : adapter.getData()) { if (replaceRuleBean.getEnable() == null || !replaceRuleBean.getEnable()) { selectAll = false; break; } } } private void selectAllDataS() { for (ReplaceRuleBean replaceRuleBean : adapter.getData()) { replaceRuleBean.setEnable(!selectAll); } adapter.notifyDataSetChanged(); selectAll = !selectAll; ReplaceRuleManager.addDataS(adapter.getData()); } public void delData(ReplaceRuleBean replaceRuleBean) { mPresenter.delData(replaceRuleBean); } public void saveDataS() { mPresenter.saveData(adapter.getData()); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.replace_rule_title); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_replace_rule_activity, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_add_replace_rule) { editReplaceRule(null); } else if (id == R.id.action_select_all) { selectAllDataS(); } else if (id == R.id.action_import) { selectReplaceRuleFile(); } else if (id == R.id.action_import_onLine) { String cacheUrl = ACache.get(this).getAsString("replaceUrl"); String[] cacheUrls = cacheUrl == null ? new String[]{} : cacheUrl.split(";"); List urlList = new ArrayList<>(Arrays.asList(cacheUrls)); InputDialog.builder(this) .setTitle(getString(R.string.input_replace_url)) .setDefaultValue(cacheUrl) .setAdapterValues(urlList) .setCallback(new InputDialog.Callback() { @Override public void setInputText(String inputText) { inputText = StringUtils.trim(inputText); if (!urlList.contains(inputText)) { urlList.add(0, inputText); ACache.get(ReplaceRuleActivity.this).put("replaceUrl", TextUtils.join(";", urlList)); } mPresenter.importDataS(inputText); } @Override public void delete(String value) { } }).show(); } else if (id == R.id.action_del_all) { mPresenter.delData(adapter.getData()); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } private void selectReplaceRuleFile() { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.get_storage_per) .onGranted((requestCode) -> { FilePicker filePicker = new FilePicker(ReplaceRuleActivity.this, FilePicker.FILE); filePicker.setBackgroundColor(getResources().getColor(R.color.background)); filePicker.setTopBackgroundColor(getResources().getColor(R.color.background)); filePicker.setItemHeight(30); filePicker.setAllowExtensions(getResources().getStringArray(R.array.text_suffix)); filePicker.setOnFilePickListener(s -> mPresenter.importDataSLocal(s)); filePicker.show(); filePicker.getSubmitButton().setText(R.string.sys_file_picker); filePicker.getSubmitButton().setOnClickListener(view -> { filePicker.dismiss(); selectFileSys(); }); return Unit.INSTANCE; }) .request(); } private void selectFileSys() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"text/*", "application/json"}); intent.setType("*/*");//设置类型 startActivityForResult(intent, IMPORT_SOURCE); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Boolean mo = moDialogHUD.onKeyDown(keyCode, event); if (mo) { return true; } else { if (keyCode == KeyEvent.KEYCODE_BACK) { finish(); return true; } return super.onKeyDown(keyCode, event); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == IMPORT_SOURCE) { if (data != null) { mPresenter.importDataSLocal(RealPathUtil.getPath(this, data.getData())); } } } @Override public void refresh() { ReplaceRuleManager.getAll() .subscribe(new MySingleObserver>() { @Override public void onSuccess(List replaceRuleBeans) { adapter.resetDataS(replaceRuleBeans); } }); } @Override protected void onDestroy() { RxBus.get().post(RxBusTag.UPDATE_READ, false); super.onDestroy(); } @Override public Snackbar getSnackBar(String msg, int length) { return Snackbar.make(binding.llContent, msg, length); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/SearchBookActivity.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.activity; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SearchView; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.flexbox.FlexboxLayoutManager; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.bean.SearchHistoryBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.ActivitySearchBookBinding; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.presenter.BookDetailPresenter; import com.kunfei.bookshelf.presenter.SearchBookPresenter; import com.kunfei.bookshelf.presenter.contract.SearchBookContract; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.Selector; import com.kunfei.bookshelf.utils.SoftInputUtil; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.SearchBookAdapter; import com.kunfei.bookshelf.view.adapter.SearchBookshelfAdapter; import com.kunfei.bookshelf.widget.explosion_field.ExplosionField; import com.kunfei.bookshelf.widget.recycler.refresh.OnLoadMoreListener; import java.util.List; import java.util.Objects; public class SearchBookActivity extends MBaseActivity implements SearchBookContract.View, SearchBookshelfAdapter.CallBack { private final int requestSource = 14; private ActivitySearchBookBinding binding; private View refreshErrorView; private ExplosionField mExplosionField; private SearchBookAdapter searchBookAdapter; private SearchView.SearchAutoComplete mSearchAutoComplete; private boolean showHistory; private String searchKey; private Menu menu; private SearchBookshelfAdapter searchBookshelfAdapter; public static void startByKey(Context context, String searchKey) { Intent intent = new Intent(context, SearchBookActivity.class); intent.putExtra("searchKey", searchKey); context.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected SearchBookContract.Presenter initInjector() { return new SearchBookPresenter(); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivitySearchBookBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void initData() { mExplosionField = ExplosionField.attach2Window(this); searchBookAdapter = new SearchBookAdapter(this); searchBookshelfAdapter = new SearchBookshelfAdapter(this); } @SuppressLint("InflateParams") @Override protected void bindView() { binding.cardSearch.setCardBackgroundColor(ThemeStore.primaryColorDark(this)); initSearchView(); setSupportActionBar(binding.toolbar); setupActionBar(); binding.fabSearchStop.hide(); binding.fabSearchStop.setBackgroundTintList(Selector.colorBuild() .setDefaultColor(ThemeStore.accentColor(this)) .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(this))) .create()); binding.llSearchHistory.setOnClickListener(null); binding.rfRvSearchBooks.setRefreshRecyclerViewAdapter(searchBookAdapter, new LinearLayoutManager(this)); refreshErrorView = LayoutInflater.from(this).inflate(R.layout.view_refresh_error, null); refreshErrorView.findViewById(R.id.tv_refresh_again).setOnClickListener(v -> { //刷新失败 ,重试 toSearch(); }); binding.rfRvSearchBooks.setNoDataAndRefreshErrorView(LayoutInflater.from(this).inflate(R.layout.view_refresh_no_data, null), refreshErrorView); searchBookAdapter.setItemClickListener((view, position) -> { String dataKey = String.valueOf(System.currentTimeMillis()); Intent intent = new Intent(SearchBookActivity.this, BookDetailActivity.class); intent.putExtra("openFrom", BookDetailPresenter.FROM_SEARCH); intent.putExtra("data_key", dataKey); BitIntentDataManager.getInstance().putData(dataKey, searchBookAdapter.getItemData(position)); startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out); }); binding.fabSearchStop.setOnClickListener(view -> { binding.fabSearchStop.hide(); mPresenter.stopSearch(); }); binding.rvBookshelf.setLayoutManager(new FlexboxLayoutManager(this)); binding.rvBookshelf.setAdapter(searchBookshelfAdapter); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.action_search); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_book_search_activity, menu); this.menu = menu; initMenu(); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_book_source_manage) { BookSourceActivity.startThis(this, requestSource); } else if (id == android.R.id.home) { SoftInputUtil.hideIMM(getCurrentFocus()); finish(); } else { if (item.getGroupId() == R.id.source_group) { item.setChecked(true); if (Objects.equals(getString(R.string.all_source), item.getTitle().toString())) { MApplication.SEARCH_GROUP = null; } else { MApplication.SEARCH_GROUP = item.getTitle().toString(); } mPresenter.initSearchEngineS(MApplication.SEARCH_GROUP); } } return super.onOptionsItemSelected(item); } private void initSearchView() { mSearchAutoComplete = binding.searchView.findViewById(R.id.search_src_text); binding.searchView.setQueryHint(getString(R.string.search_book_key)); //获取到TextView的控件 mSearchAutoComplete.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); mSearchAutoComplete.setPadding(15, 0, 0, 0); binding.searchView.onActionViewExpanded(); LinearLayout editFrame = binding.searchView.findViewById(R.id.search_edit_frame); ImageView closeButton = binding.searchView.findViewById(R.id.search_close_btn); ImageView goButton = binding.searchView.findViewById(R.id.search_go_btn); LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) editFrame.getLayoutParams(); params.setMargins(20, 0, 10, 0); editFrame.setLayoutParams(params); closeButton.setScaleX(0.9f); closeButton.setScaleY(0.9f); closeButton.setPadding(0, 0, 0, 0); goButton.setPadding(0, 0, 0, 0); binding.searchView.setSubmitButtonEnabled(true); binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { if (TextUtils.isEmpty(query)) return false; searchKey = query.trim(); if (!searchKey.toLowerCase().startsWith("set:")) { toSearch(); binding.searchView.clearFocus(); } else { parseSecretCode(searchKey); finish(); } return false; } @Override public boolean onQueryTextChange(String newText) { if (newText != null) { List beans = BookshelfHelp.searchBookInfo(newText); searchBookshelfAdapter.setItems(beans); if (beans.size() > 0) { binding.tvBookshelf.setVisibility(View.VISIBLE); binding.rvBookshelf.setVisibility(View.VISIBLE); } else { binding.tvBookshelf.setVisibility(View.GONE); binding.rvBookshelf.setVisibility(View.GONE); } } else { binding.tvBookshelf.setVisibility(View.GONE); binding.rvBookshelf.setVisibility(View.GONE); } if (!newText.toLowerCase().startsWith("set")) { mPresenter.querySearchHistory(newText); } else { showHideSetting(); } return false; } }); binding.searchView.setOnQueryTextFocusChangeListener((view, b) -> { showHistory = b; if (!b && binding.searchView.getQuery().toString().trim().equals("")) { finish(); } if (showHistory) { binding.fabSearchStop.hide(); mPresenter.stopSearch(); } openOrCloseHistory(showHistory); }); } @Override protected void bindEvent() { binding.tvSearchHistoryClean.setOnClickListener(v -> { mExplosionField.explode(binding.tflSearchHistory, true); mPresenter.cleanSearchHistory(); }); binding.rfRvSearchBooks.setLoadMoreListener(new OnLoadMoreListener() { @Override public void startLoadMore() { binding.fabSearchStop.show(); mPresenter.toSearchBooks(null, false); } @Override public void loadMoreErrorTryAgain() { binding.fabSearchStop.show(); mPresenter.toSearchBooks(null, true); } }); } @Override protected void firstRequest() { super.firstRequest(); Intent intent = this.getIntent(); searchBook(intent.getStringExtra("searchKey")); } @Override public void onPause() { super.onPause(); showHistory = binding.llSearchHistory.getVisibility() == View.VISIBLE; } @Override public void onResume() { super.onResume(); openOrCloseHistory(showHistory); } @Override public void searchBook(String searchKey) { if (!TextUtils.isEmpty(searchKey)) { binding.searchView.setQuery(searchKey, true); showHistory = false; } else { showHistory = true; mPresenter.querySearchHistory(""); } openOrCloseHistory(showHistory); } private void initMenu() { if (menu == null) return; menu.removeGroup(R.id.source_group); menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source); List groupList = BookSourceManager.getEnableGroupList(); for (String groupName : groupList) { menu.add(R.id.source_group, Menu.NONE, Menu.NONE, groupName); } menu.setGroupCheckable(R.id.source_group, true, true); if (MApplication.SEARCH_GROUP != null) { boolean hasGroup = false; for (int i = 0; i < menu.size(); i++) { if (menu.getItem(i).getTitle().toString().equals(MApplication.SEARCH_GROUP)) { menu.getItem(i).setChecked(true); hasGroup = true; break; } } if (!hasGroup) { menu.getItem(1).setChecked(true); } } else { menu.getItem(1).setChecked(true); } } private void showHideSetting() { binding.tflSearchHistory.removeAllViews(); TextView tagView; String[] hideSettings = {"show_nav_shelves", "fade_tts", "use_regex_in_new_rule", "blur_sim_back", "async_draw", "disable_scroll_click_turn"}; for (String text : hideSettings) { tagView = (TextView) getLayoutInflater().inflate(R.layout.item_search_history, binding.tflSearchHistory, false); tagView.setTag(text); tagView.setText(text); tagView.setOnClickListener(view -> { String key = "set:" + view.getTag(); binding.searchView.setQuery(key, false); }); binding.tflSearchHistory.addView(tagView); } } private void parseSecretCode(String code) { code = code.toLowerCase().replaceAll("^\\s*set:", "").trim(); String[] param = code.split("\\s+"); String msg = null; boolean enable = param.length == 1 || !param[1].equals("false"); switch (param[0]) { case "show_nav_shelves": MApplication.getConfigPreferences().edit().putBoolean("showNavShelves", enable).apply(); msg = "已" + (enable ? "启" : "禁") + "用侧边栏书架!"; RxBus.get().post(RxBusTag.RECREATE, true); break; case "fade_tts": MApplication.getConfigPreferences().edit().putBoolean("fadeTTS", enable).apply(); msg = "已" + (enable ? "启" : "禁") + "用朗读时淡入淡出!"; break; case "use_regex_in_new_rule": MApplication.getConfigPreferences().edit().putBoolean("useRegexInNewRule", enable).apply(); msg = "已" + (enable ? "启" : "禁") + "用新建替换规则时默认使用正则表达式!"; break; case "blur_sim_back": MApplication.getConfigPreferences().edit().putBoolean("blurSimBack", enable).apply(); msg = "已" + (enable ? "启" : "禁") + "用仿真翻页背景虚化!"; break; case "async_draw": MApplication.getConfigPreferences().edit().putBoolean("asyncDraw", enable).apply(); msg = "已" + (enable ? "启" : "禁") + "用异步加载!"; break; case "disable_scroll_click_turn": MApplication.getConfigPreferences().edit().putBoolean("disableScrollClickTurn", enable).apply(); msg = "已" + (enable ? "禁" : "启") + "用滚动模式点击翻页!"; break; } if (msg == null) { toast("无法识别设置密码: " + code, 0, -1); } else { toast(msg, 0, 1); } } /** * 开始搜索 */ private void toSearch() { if (!TextUtils.isEmpty(searchKey)) { mPresenter.insertSearchHistory(); //执行搜索请求 new Handler().postDelayed(() -> { mPresenter.initPage(); binding.rfRvSearchBooks.startRefresh(); binding.fabSearchStop.show(); mPresenter.toSearchBooks(searchKey, false); }, 300); } } private void openOrCloseHistory(Boolean open) { if (open) { if (binding.llSearchHistory.getVisibility() != View.VISIBLE) { binding.llSearchHistory.setVisibility(View.VISIBLE); } } else { if (binding.llSearchHistory.getVisibility() == View.VISIBLE) { binding.llSearchHistory.setVisibility(View.GONE); } } } private void addNewHistories(List historyBeans) { binding.tflSearchHistory.removeAllViews(); if (historyBeans != null) { TextView tagView; for (SearchHistoryBean searchHistoryBean : historyBeans) { tagView = (TextView) getLayoutInflater().inflate(R.layout.item_search_history, binding.tflSearchHistory, false); tagView.setTag(searchHistoryBean); tagView.setText(searchHistoryBean.getContent()); tagView.setOnClickListener(view -> { SearchHistoryBean historyBean = (SearchHistoryBean) view.getTag(); List beans = BookshelfHelp.searchBookInfo(historyBean.getContent()); binding.searchView.setQuery(historyBean.getContent(), beans.isEmpty()); }); tagView.setOnLongClickListener(view -> { SearchHistoryBean historyBean = (SearchHistoryBean) view.getTag(); mExplosionField.explode(view); view.setOnLongClickListener(null); mPresenter.cleanSearchHistory(historyBean); return true; }); binding.tflSearchHistory.addView(tagView); } } } @Override public void insertSearchHistorySuccess(SearchHistoryBean searchHistoryBean) { //搜索历史插入或者修改成功 mPresenter.querySearchHistory(searchKey); } @Override public void querySearchHistorySuccess(List data) { addNewHistories(data); if (binding.tflSearchHistory.getChildCount() > 0) { binding.tvSearchHistoryClean.setVisibility(View.VISIBLE); } else { binding.tvSearchHistoryClean.setVisibility(View.INVISIBLE); } } @Override public void refreshSearchBook() { searchBookAdapter.upData(SearchBookAdapter.DataAction.CLEAR, null); } @Override public void refreshFinish(Boolean isAll) { binding.fabSearchStop.hide(); binding.rfRvSearchBooks.finishRefresh(isAll, true); } @Override public void loadMoreFinish(Boolean isAll) { binding.fabSearchStop.hide(); binding.rfRvSearchBooks.finishLoadMore(isAll, true); } @Override public void searchBookError(Throwable throwable) { if (searchBookAdapter.getICount() == 0) { ((TextView) refreshErrorView.findViewById(R.id.tv_error_msg)).setText(throwable.getMessage()); binding.rfRvSearchBooks.refreshError(); } else { binding.rfRvSearchBooks.loadMoreError(); } } @Override public void loadMoreSearchBook(final List books) { searchBookAdapter.addAll(books, mSearchAutoComplete.getText().toString().trim()); } @Override protected void onDestroy() { mPresenter.stopSearch(); mExplosionField.clear(); super.onDestroy(); } @Override public EditText getEdtContent() { return mSearchAutoComplete; } @Override public SearchBookAdapter getSearchBookAdapter() { return searchBookAdapter; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == requestSource) { initMenu(); mPresenter.initSearchEngineS(MApplication.SEARCH_GROUP); } } } @Override public void finish() { super.finish(); overridePendingTransition(0, android.R.anim.fade_out); } @Override public void openBookInfo(BookInfoBean bookInfoBean) { Intent intent = new Intent(this, BookDetailActivity.class); intent.putExtra("noteUrl", bookInfoBean.getNoteUrl()); startActivity(intent); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/SettingActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Context; import android.content.Intent; import android.view.MenuItem; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.databinding.ActivitySettingsBinding; import com.kunfei.bookshelf.help.storage.BackupRestoreUi; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.fragment.SettingsFragment; /** * Created by GKF on 2017/12/16. * 设置 */ public class SettingActivity extends MBaseActivity { private ActivitySettingsBinding binding; private final SettingsFragment settingsFragment = new SettingsFragment(); public static void startThis(Context context) { context.startActivity(new Intent(context, SettingActivity.class)); } @Override protected IPresenter initInjector() { return null; } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivitySettingsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(getString(R.string.setting)); getFragmentManager().beginTransaction() .replace(R.id.settingsFrameLayout, settingsFragment, "settings") .commit(); } @Override protected void initData() { } //设置ToolBar public void setupActionBar(String title) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(title); } } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } @Override public void finish() { if (getFragmentManager().findFragmentByTag("settings") == null) { getFragmentManager().beginTransaction() .replace(R.id.settingsFrameLayout, settingsFragment, "settings") .commit(); } else { super.finish(); } } @Override protected void onDestroy() { super.onDestroy(); } @Override public void onBackPressed() { super.onBackPressed(); } @Override public void initImmersionBar() { super.initImmersionBar(); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); BackupRestoreUi.INSTANCE.onActivityResult(requestCode, resultCode, data); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/SourceDebugActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SearchView; import androidx.recyclerview.widget.LinearLayoutManager; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.ActivitySourceDebugBinding; import com.kunfei.bookshelf.model.content.Debug; import com.kunfei.bookshelf.utils.SoftInputUtil; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.SourceDebugAdapter; import io.reactivex.disposables.CompositeDisposable; public class SourceDebugActivity extends MBaseActivity { private final int REQUEST_QR = 202; private ActivitySourceDebugBinding binding; private SourceDebugAdapter adapter; private CompositeDisposable compositeDisposable; private String sourceTag; public static void startThis(Context context, String sourceUrl) { if (TextUtils.isEmpty(sourceUrl)) return; Intent intent = new Intent(context, SourceDebugActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("sourceUrl", sourceUrl); context.startActivity(intent); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); RxBus.get().register(this); } @Override protected void onDestroy() { Debug.SOURCE_DEBUG_TAG = null; RxBus.get().unregister(this); if (compositeDisposable != null) { compositeDisposable.dispose(); } super.onDestroy(); } /** * 布局载入 setContentView() */ @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivitySourceDebugBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } /** * 数据初始化 */ @Override protected void initData() { sourceTag = getIntent().getStringExtra("sourceUrl"); } @Override protected void bindView() { super.bindView(); this.setSupportActionBar(binding.toolbar); setupActionBar(); initSearchView(); adapter = new SourceDebugAdapter(this); binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerView.setAdapter(adapter); } private void initSearchView() { binding.searchView.setQueryHint(getString(R.string.debug_hint)); binding.searchView.onActionViewExpanded(); binding.searchView.setSubmitButtonEnabled(true); binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { if (TextUtils.isEmpty(query)) return false; startDebug(query); SoftInputUtil.hideIMM(binding.searchView); return true; } @Override public boolean onQueryTextChange(String newText) { return false; } }); } private void startDebug(String key) { if (TextUtils.isEmpty(sourceTag) || TextUtils.isEmpty(key)) { toast(R.string.cannot_empty); return; } if (compositeDisposable != null) { compositeDisposable.dispose(); } compositeDisposable = new CompositeDisposable(); binding.loading.start(); adapter.clearData(); Debug.newDebug(sourceTag, key, compositeDisposable); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_debug_activity, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_scan) { scan(); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } private void scan() { Intent intent = new Intent(this, QRCodeScanActivity.class); startActivityForResult(intent, REQUEST_QR); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == REQUEST_QR) { String result = data.getStringExtra("result"); if (!StringUtils.isTrimEmpty(result)) { binding.searchView.setQuery(result, true); } } } } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.PRINT_DEBUG_LOG)}) public void printDebugLog(String msg) { adapter.addData(msg); if (msg.equals("finish")) { binding.loading.stop(); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/SourceEditActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import static android.text.TextUtils.isEmpty; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.widget.EditText; import android.widget.PopupWindow; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.zxing.EncodeHintType; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.BuildConfig; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.constant.BookType; import com.kunfei.bookshelf.databinding.ActivitySourceEditBinding; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.presenter.SourceEditPresenter; import com.kunfei.bookshelf.presenter.contract.SourceEditContract; import com.kunfei.bookshelf.service.ShareService; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.SoftInputUtil; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.SourceEditAdapter; import com.kunfei.bookshelf.view.dialog.SourceLoginDialog; import com.kunfei.bookshelf.view.popupwindow.KeyboardToolPop; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import cn.bingoogolapple.qrcode.zxing.QRCodeEncoder; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; /** * Created by GKF on 2018/1/26. * 编辑书源 */ public class SourceEditActivity extends MBaseActivity implements SourceEditContract.View, KeyboardToolPop.CallBack { public final static int EDIT_SOURCE = 1101; private final int REQUEST_QR = 202; private ActivitySourceEditBinding binding; private SourceEditAdapter adapter; private final List sourceEditList = new ArrayList<>(); private final List findEditList = new ArrayList<>(); private BookSourceBean bookSourceBean; private int serialNumber; private boolean enable; private String title; private PopupWindow mSoftKeyboardTool; private boolean mIsSoftKeyBoardShowing = false; private boolean showFind; private String[] keyHelp = {"@", "&", "|", "%", "/", ":", "[", "]", "(", ")", "{", "}", "<", ">", "\\", "$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag"}; public static void startThis(Object object, BookSourceBean sourceBean) { String key = String.valueOf(System.currentTimeMillis()); BitIntentDataManager.getInstance().putData(key, sourceBean.clone()); if (object instanceof Activity) { Activity activity = (Activity) object; Intent intent = new Intent(activity, SourceEditActivity.class); intent.putExtra("data_key", key); activity.startActivityForResult(intent, EDIT_SOURCE); } else if (object instanceof Fragment) { Fragment fragment = (Fragment) object; Intent intent = new Intent(fragment.getContext(), SourceEditActivity.class); intent.putExtra("data_key", key); fragment.startActivityForResult(intent, EDIT_SOURCE); } else if (object instanceof Context) { Context context = (Context) object; Intent intent = new Intent(context, SourceEditActivity.class); intent.putExtra("data_key", key); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } @Override protected SourceEditContract.Presenter initInjector() { return new SourceEditPresenter(); } @Override protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { title = savedInstanceState.getString("title"); serialNumber = savedInstanceState.getInt("serialNumber"); enable = savedInstanceState.getBoolean("enable"); } super.onCreate(savedInstanceState); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putString("title", title); outState.putInt("serialNumber", serialNumber); outState.putBoolean("enable", enable); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivitySourceEditBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void initData() { String key = this.getIntent().getStringExtra("data_key"); if (title == null) { if (isEmpty(key)) { title = getString(R.string.add_book_source); bookSourceBean = new BookSourceBean(); } else { title = getString(R.string.edit_book_source); bookSourceBean = (BookSourceBean) BitIntentDataManager.getInstance().getData(key); serialNumber = bookSourceBean.getSerialNumber(); enable = bookSourceBean.getEnable(); } } } @Override protected void bindView() { this.setSupportActionBar(binding.toolbar); setupActionBar(); mSoftKeyboardTool = new KeyboardToolPop(this, Arrays.asList(keyHelp), this); getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new KeyboardOnGlobalChangeListener()); adapter = new SourceEditAdapter(this); binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerView.setAdapter(adapter); adapter.reSetData(sourceEditList); setText(bookSourceBean); } @Override protected void bindEvent() { super.bindEvent(); binding.tvEditFind.setOnClickListener(v -> { binding.recyclerView.clearFocus(); if (showFind) { adapter.reSetData(sourceEditList); binding.tvEditFind.setText(R.string.edit_find); } else { adapter.reSetData(findEditList); binding.tvEditFind.setText(R.string.back); } showFind = !showFind; binding.recyclerView.scrollToPosition(0); }); } private boolean canSaveBookSource() { SoftInputUtil.hideIMM(binding.recyclerView); binding.recyclerView.clearFocus(); BookSourceBean bookSourceBean = getBookSource(true); if (isEmpty(bookSourceBean.getBookSourceName()) || isEmpty(bookSourceBean.getBookSourceUrl())) { toast(R.string.non_null_source_name_url, ERROR); return false; } return true; } @Override public String getBookSourceStr(boolean hasFind) { Gson gson = new GsonBuilder() .disableHtmlEscaping() .setPrettyPrinting() .create(); return gson.toJson(getBookSource(hasFind)); } @Override public void setText(BookSourceBean bookSourceBean) { sourceEditList.clear(); findEditList.clear(); adapter.notifyDataSetChanged(); sourceEditList.add(new SourceEdit("bookSourceUrl", bookSourceBean.getBookSourceUrl(), R.string.book_source_url)); sourceEditList.add(new SourceEdit("bookSourceName", bookSourceBean.getBookSourceName(), R.string.book_source_name)); sourceEditList.add(new SourceEdit("bookSourceGroup", bookSourceBean.getBookSourceGroup(), R.string.book_source_group)); sourceEditList.add(new SourceEdit("loginUrl", bookSourceBean.getLoginUrl(), R.string.book_source_login_url)); sourceEditList.add(new SourceEdit("loginUi", bookSourceBean.getLoginUi(), R.string.login_ui)); sourceEditList.add(new SourceEdit("loginCheckJs", bookSourceBean.getLoginCheckJs(), R.string.login_check_js)); //搜索 sourceEditList.add(new SourceEdit("ruleSearchUrl", bookSourceBean.getRuleSearchUrl(), R.string.rule_search_url)); sourceEditList.add(new SourceEdit("ruleSearchList", bookSourceBean.getRuleSearchList(), R.string.rule_search_list)); sourceEditList.add(new SourceEdit("ruleSearchName", bookSourceBean.getRuleSearchName(), R.string.rule_search_name)); sourceEditList.add(new SourceEdit("ruleSearchAuthor", bookSourceBean.getRuleSearchAuthor(), R.string.rule_search_author)); sourceEditList.add(new SourceEdit("ruleSearchKind", bookSourceBean.getRuleSearchKind(), R.string.rule_search_kind)); sourceEditList.add(new SourceEdit("ruleSearchLastChapter", bookSourceBean.getRuleSearchLastChapter(), R.string.rule_search_last_chapter)); sourceEditList.add(new SourceEdit("ruleSearchIntroduce", bookSourceBean.getRuleSearchIntroduce(), R.string.rule_search_introduce)); sourceEditList.add(new SourceEdit("ruleSearchCoverUrl", bookSourceBean.getRuleSearchCoverUrl(), R.string.rule_search_cover_url)); sourceEditList.add(new SourceEdit("ruleSearchNoteUrl", bookSourceBean.getRuleSearchNoteUrl(), R.string.rule_search_note_url)); //详情页 sourceEditList.add(new SourceEdit("ruleBookUrlPattern", bookSourceBean.getRuleBookUrlPattern(), R.string.book_url_pattern)); sourceEditList.add(new SourceEdit("ruleBookInfoInit", bookSourceBean.getRuleBookInfoInit(), R.string.rule_book_info_init)); sourceEditList.add(new SourceEdit("ruleBookName", bookSourceBean.getRuleBookName(), R.string.rule_book_name)); sourceEditList.add(new SourceEdit("ruleBookAuthor", bookSourceBean.getRuleBookAuthor(), R.string.rule_book_author)); sourceEditList.add(new SourceEdit("ruleCoverUrl", bookSourceBean.getRuleCoverUrl(), R.string.rule_cover_url)); sourceEditList.add(new SourceEdit("ruleIntroduce", bookSourceBean.getRuleIntroduce(), R.string.rule_introduce)); sourceEditList.add(new SourceEdit("ruleBookKind", bookSourceBean.getRuleBookKind(), R.string.rule_book_kind)); sourceEditList.add(new SourceEdit("ruleBookLastChapter", bookSourceBean.getRuleBookLastChapter(), R.string.rule_book_last_chapter)); sourceEditList.add(new SourceEdit("ruleChapterUrl", bookSourceBean.getRuleChapterUrl(), R.string.rule_chapter_list_url)); //目录页 sourceEditList.add(new SourceEdit("ruleChapterUrlNext", bookSourceBean.getRuleChapterUrlNext(), R.string.rule_chapter_list_url_next)); sourceEditList.add(new SourceEdit("ruleChapterList", bookSourceBean.getRuleChapterList(), R.string.rule_chapter_list)); sourceEditList.add(new SourceEdit("ruleChapterName", bookSourceBean.getRuleChapterName(), R.string.rule_chapter_name)); sourceEditList.add(new SourceEdit("ruleContentUrl", bookSourceBean.getRuleContentUrl(), R.string.rule_content_url)); sourceEditList.add(new SourceEdit("ruleChapterVip", bookSourceBean.getRuleChapterVip(), R.string.rule_vip)); sourceEditList.add(new SourceEdit("ruleChapterPay", bookSourceBean.getRuleChapterPay(), R.string.rule_pay)); //正文页 sourceEditList.add(new SourceEdit("ruleContentUrlNext", bookSourceBean.getRuleContentUrlNext(), R.string.rule_content_url_next)); sourceEditList.add(new SourceEdit("ruleBookContent", bookSourceBean.getRuleBookContent(), R.string.rule_book_content)); sourceEditList.add(new SourceEdit("ruleBookContentReplace", bookSourceBean.getRuleBookContentReplace(), R.string.rule_book_content_replace)); sourceEditList.add(new SourceEdit("httpUserAgent", bookSourceBean.getHttpUserAgent(), R.string.source_user_agent)); //发现 findEditList.add(new SourceEdit("ruleFindUrl", bookSourceBean.getRuleFindUrl(), R.string.rule_find_url)); findEditList.add(new SourceEdit("ruleFindList", bookSourceBean.getRuleFindList(), R.string.rule_find_list)); findEditList.add(new SourceEdit("ruleFindName", bookSourceBean.getRuleFindName(), R.string.rule_find_name)); findEditList.add(new SourceEdit("ruleFindAuthor", bookSourceBean.getRuleFindAuthor(), R.string.rule_find_author)); findEditList.add(new SourceEdit("ruleFindKind", bookSourceBean.getRuleFindKind(), R.string.rule_find_kind)); findEditList.add(new SourceEdit("ruleFindIntroduce", bookSourceBean.getRuleFindIntroduce(), R.string.rule_find_introduce)); findEditList.add(new SourceEdit("ruleFindLastChapter", bookSourceBean.getRuleFindLastChapter(), R.string.rule_find_last_chapter)); findEditList.add(new SourceEdit("ruleFindCoverUrl", bookSourceBean.getRuleFindCoverUrl(), R.string.rule_find_cover_url)); findEditList.add(new SourceEdit("ruleFindNoteUrl", bookSourceBean.getRuleFindNoteUrl(), R.string.rule_find_note_url)); if (showFind) { adapter.reSetData(findEditList); } else { adapter.reSetData(sourceEditList); } binding.cbIsAudio.setChecked(Objects.equals(bookSourceBean.getBookSourceType(), BookType.AUDIO)); binding.cbIsEnable.setChecked(bookSourceBean.getEnable()); } private void scanBookSource() { Intent intent = new Intent(this, QRCodeScanActivity.class); startActivityForResult(intent, REQUEST_QR); } private BookSourceBean getBookSource(boolean hasFind) { BookSourceBean bookSourceBeanN = new BookSourceBean(); for (SourceEdit sourceEdit : sourceEditList) { switch (sourceEdit.getKey()) { case "bookSourceUrl": bookSourceBeanN.setBookSourceUrl(sourceEdit.value); break; case "bookSourceName": bookSourceBeanN.setBookSourceName(sourceEdit.value); break; case "bookSourceGroup": bookSourceBeanN.setBookSourceGroup(sourceEdit.value); break; case "loginUrl": bookSourceBeanN.setLoginUrl(sourceEdit.value); break; case "loginUi": bookSourceBeanN.setLoginUi(sourceEdit.value); break; case "loginCheckJs": bookSourceBeanN.setLoginCheckJs(sourceEdit.value); break; case "ruleSearchUrl": bookSourceBeanN.setRuleSearchUrl(sourceEdit.value); break; case "ruleSearchList": bookSourceBeanN.setRuleSearchList(sourceEdit.value); break; case "ruleSearchName": bookSourceBeanN.setRuleSearchName(sourceEdit.value); break; case "ruleSearchAuthor": bookSourceBeanN.setRuleSearchAuthor(sourceEdit.value); break; case "ruleSearchKind": bookSourceBeanN.setRuleSearchKind(sourceEdit.value); break; case "ruleSearchIntroduce": bookSourceBeanN.setRuleSearchIntroduce(sourceEdit.value); break; case "ruleSearchLastChapter": bookSourceBeanN.setRuleSearchLastChapter(sourceEdit.value); break; case "ruleSearchCoverUrl": bookSourceBeanN.setRuleSearchCoverUrl(sourceEdit.value); break; case "ruleSearchNoteUrl": bookSourceBeanN.setRuleSearchNoteUrl(sourceEdit.value); break; case "ruleBookUrlPattern": bookSourceBeanN.setRuleBookUrlPattern(sourceEdit.value); break; case "ruleBookInfoInit": bookSourceBeanN.setRuleBookInfoInit(sourceEdit.value); break; case "ruleBookName": bookSourceBeanN.setRuleBookName(sourceEdit.value); break; case "ruleBookAuthor": bookSourceBeanN.setRuleBookAuthor(sourceEdit.value); break; case "ruleCoverUrl": bookSourceBeanN.setRuleCoverUrl(sourceEdit.value); break; case "ruleIntroduce": bookSourceBeanN.setRuleIntroduce(sourceEdit.value); break; case "ruleBookKind": bookSourceBeanN.setRuleBookKind(sourceEdit.value); break; case "ruleBookLastChapter": bookSourceBeanN.setRuleBookLastChapter(sourceEdit.value); break; case "ruleChapterUrl": bookSourceBeanN.setRuleChapterUrl(sourceEdit.value); break; case "ruleChapterUrlNext": bookSourceBeanN.setRuleChapterUrlNext(sourceEdit.value); break; case "ruleChapterList": bookSourceBeanN.setRuleChapterList(sourceEdit.value); break; case "ruleChapterName": bookSourceBeanN.setRuleChapterName(sourceEdit.value); break; case "ruleVip": bookSourceBeanN.setRuleChapterVip(sourceEdit.value); break; case "rulePay": bookSourceBeanN.setRuleChapterPay(sourceEdit.value); break; case "ruleContentUrl": bookSourceBeanN.setRuleContentUrl(sourceEdit.value); break; case "ruleContentUrlNext": bookSourceBeanN.setRuleContentUrlNext(sourceEdit.value); break; case "ruleBookContent": bookSourceBeanN.setRuleBookContent(sourceEdit.value); break; case "ruleBookContentReplace": bookSourceBeanN.setRuleBookContentReplace(sourceEdit.value); break; case "httpUserAgent": bookSourceBeanN.setHttpUserAgent(sourceEdit.value); break; } } if (hasFind) { for (SourceEdit sourceEdit : findEditList) { switch (sourceEdit.getKey()) { case "ruleFindUrl": bookSourceBeanN.setRuleFindUrl(sourceEdit.value); break; case "ruleFindList": bookSourceBeanN.setRuleFindList(sourceEdit.value); break; case "ruleFindName": bookSourceBeanN.setRuleFindName(sourceEdit.value); break; case "ruleFindAuthor": bookSourceBeanN.setRuleFindAuthor(sourceEdit.value); break; case "ruleFindKind": bookSourceBeanN.setRuleFindKind(sourceEdit.value); break; case "ruleFindIntroduce": bookSourceBeanN.setRuleFindIntroduce(sourceEdit.value); break; case "ruleFindLastChapter": bookSourceBeanN.setRuleFindLastChapter(sourceEdit.value); break; case "ruleFindCoverUrl": bookSourceBeanN.setRuleFindCoverUrl(sourceEdit.value); break; case "ruleFindNoteUrl": bookSourceBeanN.setRuleFindNoteUrl(sourceEdit.value); break; } } } bookSourceBeanN.setSerialNumber(serialNumber); bookSourceBeanN.setEnable(binding.cbIsEnable.isChecked()); bookSourceBeanN.setBookSourceType(binding.cbIsAudio.isChecked() ? BookType.AUDIO : null); return bookSourceBeanN; } @SuppressLint("SetWorldReadable") private void shareBookSource() { Single.create((SingleOnSubscribe) emitter -> { QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); Bitmap bitmap = QRCodeEncoder.syncEncodeQRCode(getBookSourceStr(true), 600); QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); emitter.onSuccess(bitmap); }).compose(RxUtils::toSimpleSingle) .subscribe(new MySingleObserver() { @Override public void onSuccess(Bitmap bitmap) { if (bitmap == null) { toast("书源文字太多,生成二维码失败"); return; } try { File file = new File(SourceEditActivity.this.getExternalCacheDir(), "bookSource.png"); FileOutputStream fOut = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); fOut.flush(); fOut.close(); //noinspection ResultOfMethodCallIgnored file.setReadable(true, false); Uri contentUri = FileProvider.getUriForFile(SourceEditActivity.this, BuildConfig.APPLICATION_ID + ".fileProvider", file); final Intent intent = new Intent(Intent.ACTION_SEND); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Intent.EXTRA_STREAM, contentUri); intent.setType("image/png"); startActivity(Intent.createChooser(intent, "分享书源")); } catch (Exception e) { toast(e.getLocalizedMessage()); } } }); } private void openRuleSummary() { try { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(getString(R.string.source_rule_url))); startActivity(intent); } catch (Exception e) { toast(R.string.can_not_open, ERROR); } } private void shareText(String title, String text) { try { Intent textIntent = new Intent(Intent.ACTION_SEND); textIntent.setType("text/plain"); textIntent.putExtra(Intent.EXTRA_TEXT, text); startActivity(Intent.createChooser(textIntent, title)); } catch (Exception e) { toast(R.string.can_not_share, ERROR); } } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(title); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_book_source_edit, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_save) { if (canSaveBookSource()) { mPresenter.saveSource(getBookSource(true), bookSourceBean) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { bookSourceBean = getBookSource(true); toast("保存成功"); setResult(RESULT_OK); finish(); } @Override public void onError(Throwable e) { toast(e.getLocalizedMessage()); } }); } } else if (id == R.id.action_login) { BookSourceBean bookSourceBean = getBookSource(true); if (!isEmpty(bookSourceBean.getLoginUrl())) { if (isEmpty(bookSourceBean.getLoginUi())) { SourceLoginActivity.startThis(this, getBookSource(true)); } else { SourceLoginDialog.Companion.start( getSupportFragmentManager(), bookSourceBean.getBookSourceUrl() ); } } else { toast(R.string.source_no_login); } } else if (id == R.id.action_copy_source) { mPresenter.copySource(getBookSourceStr(true)); } else if (id == R.id.action_copy_source_no_find) { mPresenter.copySource(getBookSourceStr(false)); } else if (id == R.id.action_paste_source) { mPresenter.pasteSource(); } else if (id == R.id.action_qr_code_camera) { scanBookSource(); } else if (id == R.id.action_share_it) { shareBookSource(); } else if (id == R.id.action_share_str) { shareText("Source Share", getBookSourceStr(true)); } else if (id == R.id.action_share_wifi) { ShareService.startThis(this, Collections.singletonList(getBookSource(true))); } else if (id == R.id.action_rule_summary) { openRuleSummary(); } else if (id == R.id.action_debug_source) { if (canSaveBookSource()) { mPresenter.saveSource(getBookSource(true), bookSourceBean) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { bookSourceBean = getBookSource(true); setResult(RESULT_OK); SourceDebugActivity.startThis(SourceEditActivity.this, getBookSource(true).getBookSourceUrl()); } @Override public void onError(Throwable e) { toast(e.getLocalizedMessage()); } }); } } else if (id == android.R.id.home) { SoftInputUtil.hideIMM(getCurrentFocus()); if (back()) { return true; } finish(); } return super.onOptionsItemSelected(item); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_QR && resultCode == RESULT_OK && null != data) { String result = data.getStringExtra("result"); Observable> observable = BookSourceManager.importSource(result); if (observable != null) { observable.subscribe(new MyObserver>() { @SuppressLint("DefaultLocale") @Override public void onNext(List bookSourceBeans) { if (bookSourceBeans.size() > 1) { toast(String.format("导入成功%d个书源, 显示第一个", bookSourceBeans.size())); setText(bookSourceBeans.get(0)); } else if (bookSourceBeans.size() == 1) { setText(bookSourceBeans.get(0)); } else { toast("未导入"); } } @Override public void onError(Throwable e) { toast(e.getLocalizedMessage()); } }); } else { toast("导入失败"); } } } @Override public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (back()) { return true; } } return super.onKeyDown(keyCode, keyEvent); } @Override protected void onDestroy() { super.onDestroy(); if (mSoftKeyboardTool != null) { mSoftKeyboardTool.dismiss(); } } private boolean back() { if (bookSourceBean == null) { bookSourceBean = new BookSourceBean(); } if (!getBookSource(true).equals(bookSourceBean)) { new AlertDialog.Builder(this) .setTitle(getString(R.string.exit)) .setMessage(getString(R.string.exit_no_save)) .setPositiveButton("是", (DialogInterface dialogInterface, int which) -> { }) .setNegativeButton("否", (DialogInterface dialogInterface, int which) -> finish()) .show(); return true; } return false; } @Override public void sendText(@NotNull String txt) { if (isEmpty(txt)) return; View view = getWindow().getDecorView().findFocus(); if (view instanceof EditText) { EditText editText = (EditText) view; int start = editText.getSelectionStart(); int end = editText.getSelectionEnd(); Editable edit = editText.getEditableText();//获取EditText的文字 if (start < 0 || start >= edit.length()) { edit.append(txt); } else { edit.replace(start, end, txt);//光标所在位置插入文字 } } } private void showKeyboardTopPopupWindow() { if (isFinishing()) return; if (mSoftKeyboardTool != null && mSoftKeyboardTool.isShowing()) { return; } if (mSoftKeyboardTool != null & !this.isFinishing()) { mSoftKeyboardTool.showAtLocation(binding.llContent, Gravity.BOTTOM, 0, 0); } } private void closePopupWindow() { if (mSoftKeyboardTool != null && mSoftKeyboardTool.isShowing()) { mSoftKeyboardTool.dismiss(); } } private class KeyboardOnGlobalChangeListener implements ViewTreeObserver.OnGlobalLayoutListener { @Override public void onGlobalLayout() { Rect rect = new Rect(); // 获取当前页面窗口的显示范围 getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); int screenHeight = SoftInputUtil.getScreenHeight(SourceEditActivity.this); int keyboardHeight = screenHeight - rect.bottom; // 输入法的高度 boolean preShowing = mIsSoftKeyBoardShowing; if (Math.abs(keyboardHeight) > screenHeight / 5) { mIsSoftKeyBoardShowing = true; // 超过屏幕五分之一则表示弹出了输入法 binding.recyclerView.setPadding(0, 0, 0, 100); showKeyboardTopPopupWindow(); } else { mIsSoftKeyBoardShowing = false; binding.recyclerView.setPadding(0, 0, 0, 0); if (preShowing) { closePopupWindow(); } } } } public class SourceEdit { private String key; private String value; private final int hint; SourceEdit(String key, String value, int hint) { this.key = key; this.value = value; this.hint = hint; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public int getHint() { return hint; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/SourceLoginActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.webkit.CookieManager; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.appcompat.app.ActionBar; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.CookieBean; import com.kunfei.bookshelf.databinding.ActivitySourceLoginBinding; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class SourceLoginActivity extends MBaseActivity { private ActivitySourceLoginBinding binding; private BookSourceBean bookSourceBean; private boolean checking = false; public static void startThis(Context context, BookSourceBean bookSourceBean) { if (TextUtils.isEmpty(bookSourceBean.getLoginUrl())) { return; } Intent intent = new Intent(context, SourceLoginActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); String key = String.valueOf(System.currentTimeMillis()); intent.putExtra("data_key", key); BitIntentDataManager.getInstance().putData(key, bookSourceBean.clone()); context.startActivity(intent); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } /** * 布局载入 setContentView() */ @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivitySourceLoginBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(); } /** * 数据初始化 */ @SuppressLint("SetJavaScriptEnabled") @Override protected void initData() { String key = this.getIntent().getStringExtra("data_key"); bookSourceBean = (BookSourceBean) BitIntentDataManager.getInstance().getData(key); WebSettings settings = binding.webView.getSettings(); settings.setSupportZoom(true); settings.setBuiltInZoomControls(true); settings.setDefaultTextEncodingName("UTF-8"); settings.setJavaScriptEnabled(true); CookieManager cookieManager = CookieManager.getInstance(); binding.webView.setWebViewClient(new WebViewClient() { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { String cookie = cookieManager.getCookie(url); DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(bookSourceBean.getBookSourceUrl(), cookie)); super.onPageStarted(view, url, favicon); } @Override public void onPageFinished(WebView view, String url) { String cookie = cookieManager.getCookie(url); DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(bookSourceBean.getBookSourceUrl(), cookie)); if (checking) finish(); else showSnackBar(binding.toolbar, getString(R.string.click_check_after_success)); super.onPageFinished(view, url); } }); binding.webView.loadUrl(bookSourceBean.getLoginUrl()); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(getString(R.string.login)); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_source_login, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_check) { if (checking) return super.onOptionsItemSelected(item); checking = true; showSnackBar(binding.toolbar, getString(R.string.check_host_cookie)); binding.webView.loadUrl(bookSourceBean.getBookSourceUrl()); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/ThemeSettingActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Context; import android.content.Intent; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.databinding.ActivitySettingsBinding; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.fragment.ThemeSettingsFragment; /** * Created by GKF on 2017/12/16. * 设置 */ public class ThemeSettingActivity extends MBaseActivity { private ActivitySettingsBinding binding; public static void startThis(Context context) { context.startActivity(new Intent(context, ThemeSettingActivity.class)); } @Override protected IPresenter initInjector() { return null; } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivitySettingsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); this.setSupportActionBar(binding.toolbar); setupActionBar(); ThemeSettingsFragment settingsFragment = new ThemeSettingsFragment(); getFragmentManager().beginTransaction() .replace(R.id.settingsFrameLayout, settingsFragment) .commit(); } @Override protected void initData() { } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.theme_setting); } } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } @Override protected void onDestroy() { super.onDestroy(); } @Override public void onBackPressed() { super.onBackPressed(); } @Override public void initImmersionBar() { super.initImmersionBar(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/TxtChapterRuleActivity.java ================================================ package com.kunfei.bookshelf.view.activity; import android.content.Context; import android.content.Intent; import android.view.Menu; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.snackbar.Snackbar; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.bean.TxtChapterRuleBean; import com.kunfei.bookshelf.databinding.ActivityRecyclerVewBinding; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.help.permission.Permissions; import com.kunfei.bookshelf.help.permission.PermissionsCompat; import com.kunfei.bookshelf.model.TxtChapterRuleManager; import com.kunfei.bookshelf.presenter.TxtChapterRulePresenter; import com.kunfei.bookshelf.presenter.contract.TxtChapterRuleContract; import com.kunfei.bookshelf.utils.RealPathUtil; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.TxtChapterRuleAdapter; import com.kunfei.bookshelf.widget.filepicker.picker.FilePicker; import com.kunfei.bookshelf.widget.modialog.TxtChapterRuleDialog; import kotlin.Unit; public class TxtChapterRuleActivity extends MBaseActivity implements TxtChapterRuleContract.View { private final int requestImport = 102; private ActivityRecyclerVewBinding binding; private TxtChapterRuleAdapter adapter; private boolean selectAll = true; public static void startThis(Context context) { Intent intent = new Intent(context, TxtChapterRuleActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } @Override protected TxtChapterRuleContract.Presenter initInjector() { return new TxtChapterRulePresenter(); } @Override protected void onCreateActivity() { getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this)); binding = ActivityRecyclerVewBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); } @Override protected void initData() { this.setSupportActionBar(binding.toolbar); setupActionBar(); initRecyclerView(); refresh(); } //设置ToolBar private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.txt_chapter_regex); } } // 添加菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_replace_rule_activity, menu); return super.onCreateOptionsMenu(menu); } //菜单 @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_add_replace_rule) { editChapterRule(null); } else if (id == R.id.action_select_all) { selectAllDataS(); } else if (id == R.id.action_import) { selectReplaceRuleFile(); } else if (id == R.id.action_import_onLine) { } else if (id == R.id.action_del_all) { mPresenter.delData(adapter.getData()); } else if (id == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } private void initRecyclerView() { binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new TxtChapterRuleAdapter(this); binding.recyclerView.setAdapter(adapter); ItemTouchCallback itemTouchCallback = new ItemTouchCallback(); itemTouchCallback.setOnItemTouchCallbackListener(adapter.getItemTouchCallbackListener()); itemTouchCallback.setDragEnable(true); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback); itemTouchHelper.attachToRecyclerView(binding.recyclerView); } public void editChapterRule(TxtChapterRuleBean txtChapterRuleBean) { TxtChapterRuleDialog.builder(this, txtChapterRuleBean) .setPositiveButton(txtChapterRuleBean1 -> { if (txtChapterRuleBean != null) { TxtChapterRuleManager.del(txtChapterRuleBean); } TxtChapterRuleManager.save(txtChapterRuleBean1); refresh(); }) .show(); } public void upDateSelectAll() { selectAll = true; for (TxtChapterRuleBean ruleBean : adapter.getData()) { if (ruleBean.getEnable() == null || !ruleBean.getEnable()) { selectAll = false; break; } } } private void selectAllDataS() { for (TxtChapterRuleBean ruleBean : adapter.getData()) { ruleBean.setEnable(!selectAll); } adapter.notifyDataSetChanged(); selectAll = !selectAll; TxtChapterRuleManager.save(adapter.getData()); } public void delData(TxtChapterRuleBean ruleBean) { mPresenter.delData(ruleBean); } public void saveDataS() { mPresenter.saveData(adapter.getData()); } @Override public void refresh() { adapter.resetDataS(TxtChapterRuleManager.getAll()); } @Override public Snackbar getSnackBar(String msg, int length) { return Snackbar.make(binding.llContent, msg, length); } private void selectReplaceRuleFile() { new PermissionsCompat.Builder(this) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.get_storage_per) .onGranted((requestCode) -> { FilePicker filePicker = new FilePicker(TxtChapterRuleActivity.this, FilePicker.FILE); filePicker.setBackgroundColor(getResources().getColor(R.color.background)); filePicker.setTopBackgroundColor(getResources().getColor(R.color.background)); filePicker.setItemHeight(30); filePicker.setAllowExtensions(getResources().getStringArray(R.array.text_suffix)); filePicker.setOnFilePickListener(s -> mPresenter.importDataSLocal(s)); filePicker.show(); filePicker.getSubmitButton().setText(R.string.sys_file_picker); filePicker.getSubmitButton().setOnClickListener(view -> { filePicker.dismiss(); selectFileSys(); }); return Unit.INSTANCE; }) .request(); } private void selectFileSys() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/*");//设置类型 startActivityForResult(intent, requestImport); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case requestImport: if (data != null) { mPresenter.importDataSLocal(RealPathUtil.getPath(this, data.getData())); } break; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/WebViewActivity.kt ================================================ package com.kunfei.bookshelf.view.activity import android.R import android.annotation.SuppressLint import android.view.MenuItem import com.kunfei.basemvplib.BitIntentDataManager import com.kunfei.basemvplib.impl.IPresenter import com.kunfei.bookshelf.base.MBaseActivity import com.kunfei.bookshelf.databinding.ActivityWebViewBinding import com.kunfei.bookshelf.utils.theme.ThemeStore class WebViewActivity : MBaseActivity() { val binding by lazy { ActivityWebViewBinding.inflate(layoutInflater) } override fun initInjector(): IPresenter? { return null } override fun onCreateActivity() { window.decorView.setBackgroundColor(ThemeStore.backgroundColor(this)) setContentView(binding.root) setSupportActionBar(binding.toolbar) setupActionBar() } @SuppressLint("SetJavaScriptEnabled") override fun initData() { val settings = binding.webView.settings settings.setSupportZoom(true) settings.builtInZoomControls = true settings.defaultTextEncodingName = "UTF-8" settings.javaScriptEnabled = true val url = intent.getStringExtra("url") val header = BitIntentDataManager.getInstance().getData(url) as? Map url?.let { if (header == null) { binding.webView.loadUrl(url) } else { binding.webView.loadUrl(url, header) } } } //设置ToolBar private fun setupActionBar() { val actionBar = supportActionBar if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true) actionBar.title = intent.getStringExtra("title") } } //菜单 override fun onOptionsItemSelected(item: MenuItem): Boolean { val id = item.itemId if (id == R.id.home) { finish() } return super.onOptionsItemSelected(item) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/WelcomeActivity.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.activity; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Intent; import android.os.AsyncTask; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseActivity; import com.kunfei.bookshelf.databinding.ActivityWelcomeBinding; import com.kunfei.bookshelf.presenter.ReadBookPresenter; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class WelcomeActivity extends MBaseActivity { private ActivityWelcomeBinding binding; @Override protected IPresenter initInjector() { return null; } @Override protected void onCreateActivity() { // 避免从桌面启动程序后,会重新实例化入口类的activity if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { finish(); return; } binding = ActivityWelcomeBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); AsyncTask.execute(DbHelper::getDaoSession); binding.tvGzh.setTextColor(ThemeStore.accentColor(this)); binding.ivBg.setColorFilter(ThemeStore.accentColor(this)); ValueAnimator welAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(800); welAnimator.setStartDelay(500); welAnimator.addUpdateListener(animation -> { float alpha = (Float) animation.getAnimatedValue(); binding.ivBg.setAlpha(alpha); }); welAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { if (preferences.getBoolean(getString(R.string.pk_default_read), false)) { startReadActivity(); } else { startBookshelfActivity(); } finish(); } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); welAnimator.start(); } private void startBookshelfActivity() { startActivityByAnim(new Intent(this, MainActivity.class), android.R.anim.fade_in, android.R.anim.fade_out); } private void startReadActivity() { Intent intent = new Intent(this, ReadBookActivity.class); intent.putExtra("openFrom", ReadBookPresenter.OPEN_FROM_APP); startActivity(intent); } @Override protected void initData() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/activity/WelcomeBookActivity.java ================================================ package com.kunfei.bookshelf.view.activity; public class WelcomeBookActivity extends WelcomeActivity { } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/BookShelfAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo; import java.util.HashSet; import java.util.List; public interface BookShelfAdapter { void setArrange(boolean isArrange); void selectAll(); ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener(); List getBooks(); void replaceAll(List newDataS, String bookshelfPx); void refreshBook(String noteUrl); void setItemClickListener(OnItemClickListenerTwo itemClickListener); HashSet getSelected(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/BookShelfGridAdapter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.adapter; import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo; import com.kunfei.bookshelf.widget.BadgeView; import com.kunfei.bookshelf.widget.RotateLoading; import com.kunfei.bookshelf.widget.image.CoverImageView; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; public class BookShelfGridAdapter extends RecyclerView.Adapter implements BookShelfAdapter { private boolean isArrange; private List books; private OnItemClickListenerTwo itemClickListener; private String bookshelfPx; private Activity activity; private HashSet selectList = new HashSet<>(); private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() { @Override public void onSwiped(int adapterPosition) { } @Override public boolean onMove(int srcPosition, int targetPosition) { BookShelfBean shelfBean = books.get(srcPosition); books.remove(srcPosition); books.add(targetPosition, shelfBean); notifyItemMoved(srcPosition, targetPosition); int start = srcPosition; int end = targetPosition; if (start > end) { start = targetPosition; end = srcPosition; } notifyItemRangeChanged(start, end - start + 1); return true; } }; public BookShelfGridAdapter(Activity activity) { this.activity = activity; books = new ArrayList<>(); } @Override public void setArrange(boolean isArrange) { selectList.clear(); this.isArrange = isArrange; notifyDataSetChanged(); } @Override public void selectAll() { if (selectList.size() == books.size()) { selectList.clear(); } else { for (BookShelfBean bean : books) { selectList.add(bean.getNoteUrl()); } } notifyDataSetChanged(); itemClickListener.onClick(null, 0); } @Override public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() { return itemTouchCallbackListener; } @Override public void refreshBook(String noteUrl) { for (int i = 0; i < books.size(); i++) { if (Objects.equals(books.get(i).getNoteUrl(), noteUrl)) { notifyItemChanged(i); } } } @Override public int getItemCount() { //如果不为0,按正常的流程跑 return books.size(); } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bookshelf_grid, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, @SuppressLint("RecyclerView") int index) { BookShelfBean bookShelfBean = books.get(index); BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean(); if (isArrange) { holder.vwSelect.setVisibility(View.VISIBLE); if (selectList.contains(bookShelfBean.getNoteUrl())) { holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light); } else { holder.vwSelect.setBackgroundColor(Color.TRANSPARENT); } holder.vwSelect.setOnClickListener(v -> { if (selectList.contains(bookShelfBean.getNoteUrl())) { selectList.remove(bookShelfBean.getNoteUrl()); holder.vwSelect.setBackgroundColor(Color.TRANSPARENT); } else { selectList.add(bookShelfBean.getNoteUrl()); holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light); } itemClickListener.onClick(v, index); }); } else { holder.vwSelect.setVisibility(View.VISIBLE); } holder.tvName.setText(bookInfoBean.getName()); holder.tvName.setBackgroundColor(ThemeStore.backgroundColor(activity)); if (!activity.isFinishing()) { holder.ivCover.load(bookShelfBean.getCoverPath(), bookShelfBean.getName(), bookShelfBean.getAuthor()); } holder.ivCover.setOnClickListener(v -> { if (itemClickListener != null) itemClickListener.onClick(v, index); }); holder.tvName.setOnClickListener(view -> { if (itemClickListener != null) { itemClickListener.onLongClick(view, index); } }); if (!Objects.equals(bookshelfPx, "2")) { holder.ivCover.setOnLongClickListener(v -> { if (itemClickListener != null) { itemClickListener.onLongClick(v, index); } return true; }); } else if (bookShelfBean.getSerialNumber() != index) { bookShelfBean.setSerialNumber(index); new Thread() { public void run() { DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean); } }.start(); } if (bookShelfBean.isLoading()) { holder.bvUnread.setVisibility(View.INVISIBLE); holder.rotateLoading.setVisibility(View.VISIBLE); holder.rotateLoading.start(); } else { holder.bvUnread.setBadgeCount(bookShelfBean.getUnreadChapterNum()); holder.bvUnread.setHighlight(bookShelfBean.getHasUpdate()); holder.rotateLoading.setVisibility(View.INVISIBLE); holder.rotateLoading.stop(); } } @Override public void setItemClickListener(OnItemClickListenerTwo itemClickListener) { this.itemClickListener = itemClickListener; } @Override public synchronized void replaceAll(List newDataS, String bookshelfPx) { this.bookshelfPx = bookshelfPx; selectList.clear(); if (null != newDataS && newDataS.size() > 0) { BookshelfHelp.order(newDataS, bookshelfPx); books = newDataS; } else { books.clear(); } notifyDataSetChanged(); if (isArrange) { itemClickListener.onClick(null, 0); } } @Override public List getBooks() { return books; } @Override public HashSet getSelected() { return selectList; } static class MyViewHolder extends RecyclerView.ViewHolder { CoverImageView ivCover; TextView tvName; BadgeView bvUnread; RotateLoading rotateLoading; View vwSelect; MyViewHolder(View itemView) { super(itemView); ivCover = itemView.findViewById(R.id.iv_cover); tvName = itemView.findViewById(R.id.tv_name); bvUnread = itemView.findViewById(R.id.bv_unread); rotateLoading = itemView.findViewById(R.id.rl_loading); rotateLoading.setLoadingColor(ThemeStore.accentColor(itemView.getContext())); vwSelect = itemView.findViewById(R.id.vw_select); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/BookShelfListAdapter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.adapter; import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Color; import android.os.AsyncTask; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookInfoBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo; import com.kunfei.bookshelf.widget.BadgeView; import com.kunfei.bookshelf.widget.RotateLoading; import com.kunfei.bookshelf.widget.image.CoverImageView; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; public class BookShelfListAdapter extends RecyclerView.Adapter implements BookShelfAdapter { private boolean isArrange; private Activity activity; private List books; private OnItemClickListenerTwo itemClickListener; private String bookshelfPx; private HashSet selectList = new HashSet<>(); private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() { @Override public void onSwiped(int adapterPosition) { } @Override public boolean onMove(int srcPosition, int targetPosition) { Collections.swap(books, srcPosition, targetPosition); notifyItemMoved(srcPosition, targetPosition); notifyItemChanged(srcPosition); notifyItemChanged(targetPosition); return true; } }; public BookShelfListAdapter(Activity activity) { this.activity = activity; books = new ArrayList<>(); } @Override public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() { return itemTouchCallbackListener; } @Override public void setArrange(boolean isArrange) { selectList.clear(); this.isArrange = isArrange; notifyDataSetChanged(); } @Override public void selectAll() { if (selectList.size() == books.size()) { selectList.clear(); } else { for (BookShelfBean bean : books) { selectList.add(bean.getNoteUrl()); } } notifyDataSetChanged(); itemClickListener.onClick(null, 0); } @Override public void refreshBook(String noteUrl) { for (int i = 0; i < books.size(); i++) { if (Objects.equals(books.get(i).getNoteUrl(), noteUrl)) { notifyItemChanged(i); } } } @Override public int getItemCount() { //如果不为0,按正常的流程跑 return books.size(); } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bookshelf_list, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, @SuppressLint("RecyclerView") int index) { final BookShelfBean bookShelfBean = books.get(index); holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity)); if (isArrange) { if (selectList.contains(bookShelfBean.getNoteUrl())) { holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light); } else { holder.vwSelect.setBackgroundColor(Color.TRANSPARENT); } holder.vwSelect.setVisibility(View.VISIBLE); holder.vwSelect.setOnClickListener(v -> { if (selectList.contains(bookShelfBean.getNoteUrl())) { selectList.remove(bookShelfBean.getNoteUrl()); holder.vwSelect.setBackgroundColor(Color.TRANSPARENT); } else { selectList.add(bookShelfBean.getNoteUrl()); holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light); } itemClickListener.onClick(v, index); }); } else { holder.vwSelect.setVisibility(View.GONE); } BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean(); if (!activity.isFinishing()) { holder.ivCover.load(bookShelfBean.getCoverPath(), bookShelfBean.getName(), bookShelfBean.getAuthor()); } holder.tvName.setText(bookInfoBean.getName()); holder.tvAuthor.setText(bookInfoBean.getAuthor()); holder.tvRead.setText(bookShelfBean.getDurChapterName()); holder.tvLast.setText(bookShelfBean.getLastChapterName()); holder.ivCover.setOnClickListener(v -> { if (itemClickListener != null) itemClickListener.onClick(v, index); }); holder.ivCover.setOnLongClickListener(v -> { if (itemClickListener != null) { itemClickListener.onLongClick(v, index); } return true; }); holder.flContent.setOnClickListener(v -> { if (itemClickListener != null) itemClickListener.onClick(v, index); }); if (!Objects.equals(bookshelfPx, "2")) { holder.flContent.setOnLongClickListener(view -> { if (itemClickListener != null) { itemClickListener.onLongClick(view, index); } return true; }); } else { holder.ivCover.setOnClickListener(view -> { if (itemClickListener != null) { itemClickListener.onLongClick(view, index); } }); } if (Objects.equals(bookshelfPx, "2") && bookShelfBean.getSerialNumber() != index) { bookShelfBean.setSerialNumber(index); AsyncTask.execute(() -> DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean)); } if (bookShelfBean.isLoading()) { holder.bvUnread.setVisibility(View.INVISIBLE); holder.rotateLoading.setVisibility(View.VISIBLE); holder.rotateLoading.start(); } else { holder.bvUnread.setBadgeCount(bookShelfBean.getUnreadChapterNum()); holder.bvUnread.setHighlight(bookShelfBean.getHasUpdate()); holder.rotateLoading.setVisibility(View.INVISIBLE); holder.rotateLoading.stop(); } } @Override public void setItemClickListener(OnItemClickListenerTwo itemClickListener) { this.itemClickListener = itemClickListener; } @Override public synchronized void replaceAll(List newDataS, String bookshelfPx) { this.bookshelfPx = bookshelfPx; selectList.clear(); if (null != newDataS && newDataS.size() > 0) { BookshelfHelp.order(newDataS, bookshelfPx); books = newDataS; } else { books.clear(); } notifyDataSetChanged(); if (isArrange) { itemClickListener.onClick(null, 0); } } @Override public List getBooks() { return books; } @Override public HashSet getSelected() { return selectList; } static class MyViewHolder extends RecyclerView.ViewHolder { ViewGroup flContent; CoverImageView ivCover; BadgeView bvUnread; TextView tvName; TextView tvAuthor; TextView tvRead; TextView tvLast; RotateLoading rotateLoading; View vwSelect; MyViewHolder(View itemView) { super(itemView); flContent = itemView.findViewById(R.id.cv_content); ivCover = itemView.findViewById(R.id.iv_cover); bvUnread = itemView.findViewById(R.id.bv_unread); tvName = itemView.findViewById(R.id.tv_name); tvRead = itemView.findViewById(R.id.tv_read); tvLast = itemView.findViewById(R.id.tv_last); tvAuthor = itemView.findViewById(R.id.tv_author); rotateLoading = itemView.findViewById(R.id.rl_loading); rotateLoading.setLoadingColor(ThemeStore.accentColor(itemView.getContext())); vwSelect = itemView.findViewById(R.id.vw_select); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/BookSourceAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.activity.BookSourceActivity; import com.kunfei.bookshelf.view.activity.SourceEditActivity; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Created by GKF on 2017/12/22. * 书源Adapter */ public class BookSourceAdapter extends RecyclerView.Adapter { private List dataList; private List allDataList; private BookSourceActivity activity; private int index; private int sort; private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() { @Override public void onSwiped(int adapterPosition) { } @Override public boolean onMove(int srcPosition, int targetPosition) { Collections.swap(dataList, srcPosition, targetPosition); notifyItemMoved(srcPosition, targetPosition); notifyItemChanged(srcPosition); notifyItemChanged(targetPosition); activity.saveDate(dataList); return true; } }; public BookSourceAdapter(BookSourceActivity activity) { this.activity = activity; dataList = new ArrayList<>(); } public void resetDataS(List bookSourceBeanList) { this.dataList = bookSourceBeanList; notifyDataSetChanged(); activity.upDateSelectAll(); activity.upSearchView(dataList.size()); activity.upGroupMenu(); } private void setAllDataList(List bookSourceBeanList) { this.allDataList = bookSourceBeanList; notifyDataSetChanged(); activity.upDateSelectAll(); } public List getDataList() { return dataList; } public List getSelectDataList() { List selectDataS = new ArrayList<>(); for (BookSourceBean data : dataList) { if (data.getEnable()) { selectDataS.add(data); } } return selectDataS; } public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() { return itemTouchCallbackListener; } public void setSort(int sort) { this.sort = sort; } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_book_source, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity)); if (sort != 2) { holder.topView.setVisibility(View.VISIBLE); } else { holder.topView.setVisibility(View.GONE); } if (TextUtils.isEmpty(dataList.get(position).getBookSourceGroup())) { holder.cbView.setText(dataList.get(position).getBookSourceName()); } else { holder.cbView.setText(String.format("%s (%s)", dataList.get(position).getBookSourceName(), dataList.get(position).getBookSourceGroup())); } holder.cbView.setChecked(dataList.get(position).getEnable()); holder.cbView.setOnClickListener((View view) -> { dataList.get(position).setEnable(holder.cbView.isChecked()); activity.saveDate(dataList.get(position)); activity.upDateSelectAll(); }); holder.editView.setOnClickListener(view -> SourceEditActivity.startThis(activity, dataList.get(position))); holder.delView.setOnClickListener(view -> { activity.delBookSource(dataList.get(position)); dataList.remove(position); activity.upSearchView(dataList.size()); notifyDataSetChanged(); }); holder.topView.setOnClickListener(view -> { setAllDataList(BookSourceManager.getAllBookSource()); BookSourceBean moveData = dataList.get(position); dataList.remove(position); notifyItemRemoved(position); dataList.add(0, moveData); notifyItemInserted(0); if (sort == 1) { int maxWeight = allDataList.get(0).getWeight(); moveData.setWeight(maxWeight + 1); } if (dataList.size() != allDataList.size()) { index = allDataList.indexOf(moveData); allDataList.remove(index); allDataList.add(0, moveData); activity.saveDate(allDataList); } else { activity.saveDate(dataList); } }); } @Override public int getItemCount() { return dataList.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { CheckBox cbView; ImageView editView; ImageView delView; ImageView topView; MyViewHolder(View itemView) { super(itemView); cbView = itemView.findViewById(R.id.cb_book_source); editView = itemView.findViewById(R.id.iv_edit_source); delView = itemView.findViewById(R.id.iv_del_source); topView = itemView.findViewById(R.id.iv_top_source); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/BookmarkAdapter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookmarkBean; import com.kunfei.bookshelf.utils.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; public class BookmarkAdapter extends RecyclerView.Adapter { private BookShelfBean bookShelfBean; private OnItemClickListener itemClickListener; private List allBookmark = new ArrayList<>(); private List bookmarkBeans = new ArrayList<>(); private boolean isSearch = false; public BookmarkAdapter(BookShelfBean bookShelfBean, @NonNull OnItemClickListener itemClickListener) { this.bookShelfBean = bookShelfBean; this.itemClickListener = itemClickListener; } public void setAllBookmark(List allBookmark) { this.allBookmark = allBookmark; notifyDataSetChanged(); } public void search(final String key) { bookmarkBeans.clear(); if (Objects.equals(key, "")) { isSearch = false; notifyDataSetChanged(); } else { Observable.create((ObservableOnSubscribe) emitter -> { for (BookmarkBean bookmarkBean : allBookmark) { if (bookmarkBean.getChapterName().contains(key)) { bookmarkBeans.add(bookmarkBean); } else if (bookmarkBean.getContent().contains(key)) { bookmarkBeans.add(bookmarkBean); } } emitter.onNext(true); emitter.onComplete(); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { isSearch = true; notifyDataSetChanged(); } @Override public void onError(Throwable e) { } }); } } @NonNull @Override public ThisViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ThisViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chapter_list, parent, false)); } @Override public void onBindViewHolder(@NonNull ThisViewHolder holder, final int position) { } @Override public void onBindViewHolder(@NonNull ThisViewHolder holder, int position, @NonNull List payloads) { int realPosition = holder.getLayoutPosition(); if (realPosition == getItemCount() - 1) { holder.line.setVisibility(View.GONE); } else { holder.line.setVisibility(View.VISIBLE); } BookmarkBean bookmarkBean = isSearch ? bookmarkBeans.get(realPosition) : allBookmark.get(realPosition); holder.tvName.setText(StringUtils.isTrimEmpty(bookmarkBean.getContent()) ? bookmarkBean.getChapterName() : bookmarkBean.getContent()); holder.llName.setOnClickListener(v -> { if (itemClickListener != null) { itemClickListener.itemClick(bookmarkBean.getChapterIndex(), bookmarkBean.getPageIndex()); } }); holder.llName.setOnLongClickListener(view -> { if (itemClickListener != null) { itemClickListener.itemLongClick(bookmarkBean); } return true; }); } @Override public int getItemCount() { if (bookShelfBean == null) return 0; else { if (isSearch) { return bookmarkBeans.size(); } return allBookmark.size(); } } static class ThisViewHolder extends RecyclerView.ViewHolder { private TextView tvName; private View line; private View llName; ThisViewHolder(View itemView) { super(itemView); tvName = itemView.findViewById(R.id.tv_name); line = itemView.findViewById(R.id.v_line); llName = itemView.findViewById(R.id.ll_name); } } public interface OnItemClickListener { void itemClick(int index, int page); void itemLongClick(BookmarkBean bookmarkBean); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/ChangeSourceAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter; import java.util.ArrayList; import java.util.List; import static android.text.TextUtils.isEmpty; /** * Created by GKF on 2017/12/22. * 书源Adapter */ public class ChangeSourceAdapter extends RefreshRecyclerViewAdapter { private List allBookBeans; private CallBack callBack; public ChangeSourceAdapter(Boolean needLoadMore) { super(needLoadMore); allBookBeans = new ArrayList<>(); } public void addSourceAdapter(SearchBookBean value) { allBookBeans.add(value); notifyDataSetChanged(); } public void addAllSourceAdapter(List value) { allBookBeans.addAll(value); notifyDataSetChanged(); } public void reSetSourceAdapter() { allBookBeans.clear(); notifyDataSetChanged(); } public void removeData(SearchBookBean searchBookBean) { DbHelper.getDaoSession().getSearchBookBeanDao().delete(searchBookBean); allBookBeans.remove(searchBookBean); notifyDataSetChanged(); } public void setCallBack(CallBack callBack) { this.callBack = callBack; } public List getSearchBookBeans() { return allBookBeans; } @Override public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_change_source, parent, false)); } @Override public void onBindIViewHolder(RecyclerView.ViewHolder holder, int position) { MyViewHolder myViewHolder = (MyViewHolder) holder; myViewHolder.bind(allBookBeans.get(position), callBack); } @Override public int getIViewType(int position) { return 0; } @Override public int getICount() { return allBookBeans.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { LinearLayout llContent; TextView tvBookSource; TextView tvLastChapter; ImageView ivChecked; MyViewHolder(View itemView) { super(itemView); llContent = itemView.findViewById(R.id.ll_content); tvBookSource = itemView.findViewById(R.id.tv_source_name); tvLastChapter = itemView.findViewById(R.id.tv_lastChapter); ivChecked = itemView.findViewById(R.id.iv_checked); } public void bind(SearchBookBean searchBookBean, CallBack callBack) { tvBookSource.setText(searchBookBean.getOrigin()); if (isEmpty(searchBookBean.getLastChapter())) { tvLastChapter.setText(R.string.no_last_chapter); } else { tvLastChapter.setText(searchBookBean.getLastChapter()); } if (searchBookBean.getIsCurrentSource()) { ivChecked.setVisibility(View.VISIBLE); } else { ivChecked.setVisibility(View.INVISIBLE); } llContent.setOnClickListener(view -> { if (callBack != null) { callBack.changeTo(searchBookBean); } }); llContent.setOnLongClickListener(view -> { if (callBack != null) { callBack.showMenu(view, searchBookBean); } return true; }); } } public interface CallBack { void changeTo(SearchBookBean searchBookBean); void showMenu(View view, SearchBookBean searchBookBean); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/ChapterListAdapter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.utils.theme.ThemeStore; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; public class ChapterListAdapter extends RecyclerView.Adapter { private BookShelfBean bookShelfBean; private OnItemClickListener itemClickListener; private List allChapter; private List bookChapterBeans = new ArrayList<>(); private int index = 0; private boolean isSearch = false; private int normalColor; private int highlightColor; public ChapterListAdapter(BookShelfBean bookShelfBean, List allChapter, @NonNull OnItemClickListener itemClickListener) { this.bookShelfBean = bookShelfBean; this.allChapter = allChapter; this.itemClickListener = itemClickListener; highlightColor = ThemeStore.accentColor(MApplication.getInstance()); } public void upChapter(int index) { if (allChapter.size() > index) { notifyItemChanged(index, 0); } } public void search(final String key) { bookChapterBeans.clear(); if (Objects.equals(key, "")) { isSearch = false; notifyDataSetChanged(); } else { Observable.create((ObservableOnSubscribe) emitter -> { for (BookChapterBean bookChapterBean : allChapter) { if (bookChapterBean.getDurChapterName().contains(key)) { bookChapterBeans.add(bookChapterBean); } } emitter.onNext(true); emitter.onComplete(); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onNext(Boolean aBoolean) { isSearch = true; notifyDataSetChanged(); } @Override public void onError(Throwable e) { } }); } } @NonNull @Override public ThisViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { normalColor = ThemeStore.textColorSecondary(parent.getContext()); return new ThisViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chapter_list, parent, false)); } @Override public void onBindViewHolder(@NonNull ThisViewHolder holder, final int position) { } @Override public void onBindViewHolder(@NonNull ThisViewHolder holder, int position, @NonNull List payloads) { int realPosition = holder.getLayoutPosition(); if (realPosition == getItemCount() - 1) { holder.line.setVisibility(View.GONE); } else { holder.line.setVisibility(View.VISIBLE); } if (payloads.size() > 0) { holder.tvName.setSelected(true); holder.tvName.getPaint().setFakeBoldText(true); return; } BookChapterBean bookChapterBean = isSearch ? bookChapterBeans.get(realPosition) : allChapter.get(realPosition); if (bookChapterBean.getDurChapterIndex() == index) { holder.tvName.setTextColor(highlightColor); } else { holder.tvName.setTextColor(normalColor); } holder.tvName.setText(bookChapterBean.getDisplayTitle(holder.tvName.getContext())); if (Objects.equals(bookShelfBean.getTag(), BookShelfBean.LOCAL_TAG) || bookChapterBean.getHasCache(bookShelfBean.getBookInfoBean())) { holder.tvName.setSelected(true); holder.tvName.getPaint().setFakeBoldText(true); } else { holder.tvName.setSelected(false); holder.tvName.getPaint().setFakeBoldText(false); } holder.llName.setOnClickListener(v -> { setIndex(realPosition); itemClickListener.itemClick(bookChapterBean.getDurChapterIndex(), 0); }); } @Override public int getItemCount() { if (bookShelfBean == null || allChapter == null) return 0; else { if (isSearch) { return bookChapterBeans.size(); } return allChapter.size(); } } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; notifyItemChanged(this.index, 0); } static class ThisViewHolder extends RecyclerView.ViewHolder { private TextView tvName; private View line; private View llName; ThisViewHolder(View itemView) { super(itemView); tvName = itemView.findViewById(R.id.tv_name); line = itemView.findViewById(R.id.v_line); llName = itemView.findViewById(R.id.ll_name); } } public interface OnItemClickListener { void itemClick(int index, int page); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/ChoiceBookAdapter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.adapter; import android.app.Activity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookKindBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.widget.image.CoverImageView; import com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter; import java.util.ArrayList; import java.util.List; import static com.kunfei.bookshelf.utils.StringUtils.isTrimEmpty; public class ChoiceBookAdapter extends RefreshRecyclerViewAdapter { private Activity activity; private ArrayList searchBooks; private Callback callback; public ChoiceBookAdapter(Activity activity) { super(true); this.activity = activity; searchBooks = new ArrayList<>(); } @Override public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_search_book, parent, false)); } @Override public void onBindIViewHolder(final RecyclerView.ViewHolder holder, final int position) { MyViewHolder myViewHolder = (MyViewHolder) holder; if (position >= searchBooks.size()) return; SearchBookBean book = searchBooks.get(position); if (!activity.isFinishing()) { myViewHolder.ivCover.load(book.getCoverUrl(), book.getName(), book.getCoverUrl()); } String title = book.getName(); String author = book.getAuthor(); if (author != null && author.trim().length() > 0) title = String.format("%s (%s)", title, author); myViewHolder.tvName.setText(title); BookKindBean bookKindBean = new BookKindBean(book.getKind()); if (isTrimEmpty(bookKindBean.getKind())) { myViewHolder.tvKind.setVisibility(View.GONE); } else { myViewHolder.tvKind.setVisibility(View.VISIBLE); myViewHolder.tvKind.setText(bookKindBean.getKind()); } if (isTrimEmpty(bookKindBean.getWordsS())) { myViewHolder.tvWords.setVisibility(View.GONE); } else { myViewHolder.tvWords.setVisibility(View.VISIBLE); myViewHolder.tvWords.setText(bookKindBean.getWordsS()); } if (isTrimEmpty(bookKindBean.getState())) { myViewHolder.tvState.setVisibility(View.GONE); } else { myViewHolder.tvState.setVisibility(View.VISIBLE); myViewHolder.tvState.setText(bookKindBean.getState()); } //来源 if (isTrimEmpty(book.getOrigin())) { myViewHolder.tvOrigin.setVisibility(View.GONE); } else { myViewHolder.tvOrigin.setVisibility(View.VISIBLE); myViewHolder.tvOrigin.setText(activity.getString(R.string.origin_format, searchBooks.get(position).getOrigin())); } //最新章节 if (isTrimEmpty(book.getLastChapter())) { myViewHolder.tvLasted.setVisibility(View.GONE); } else { myViewHolder.tvLasted.setText(book.getLastChapter()); myViewHolder.tvLasted.setVisibility(View.VISIBLE); } //简介 if (isTrimEmpty(book.getIntroduce())) { myViewHolder.tvIntroduce.setVisibility(View.GONE); } else { myViewHolder.tvIntroduce.setText(StringUtils.formatHtml(searchBooks.get(position).getIntroduce())); myViewHolder.tvIntroduce.setVisibility(View.VISIBLE); } myViewHolder.flContent.setOnClickListener(v -> { if (callback != null) callback.clickItem(myViewHolder.ivCover, position, book); }); } @Override public int getIViewType(int position) { return 0; } @Override public int getICount() { return searchBooks.size(); } public void setCallback(Callback callback) { this.callback = callback; } public void addAll(List newData) { if (newData != null && newData.size() > 0) { int position = getICount(); if (newData.size() > 0) { searchBooks.addAll(newData); } notifyItemInserted(position); notifyItemRangeChanged(position, newData.size()); } } public void replaceAll(List newData) { searchBooks.clear(); if (newData != null && newData.size() > 0) { searchBooks.addAll(newData); } notifyDataSetChanged(); } public interface Callback { void clickItem(View animView, int position, SearchBookBean searchBookBean); } static class MyViewHolder extends RecyclerView.ViewHolder { ViewGroup flContent; CoverImageView ivCover; TextView tvName; TextView tvState; TextView tvWords; TextView tvKind; TextView tvLasted; TextView tvOrigin; TextView tvIntroduce; MyViewHolder(View itemView) { super(itemView); flContent = itemView.findViewById(R.id.fl_content); ivCover = itemView.findViewById(R.id.iv_cover); tvName = itemView.findViewById(R.id.tv_name); tvState = itemView.findViewById(R.id.tv_state); tvWords = itemView.findViewById(R.id.tv_words); tvLasted = itemView.findViewById(R.id.tv_lasted); tvKind = itemView.findViewById(R.id.tv_kind); tvOrigin = itemView.findViewById(R.id.tv_origin); tvIntroduce = itemView.findViewById(R.id.tv_introduce); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/DownloadAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.graphics.PorterDuff; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.DownloadBookBean; import com.kunfei.bookshelf.service.DownloadService; import com.kunfei.bookshelf.view.activity.DownloadActivity; import com.kunfei.bookshelf.widget.image.CoverImageView; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; public class DownloadAdapter extends RecyclerView.Adapter { private DownloadActivity activity; private List data; private final Object mLock = new Object(); public DownloadAdapter(DownloadActivity activity) { this.activity = activity; data = new ArrayList<>(); } public void upDataS(List dataS) { synchronized (mLock) { this.data.clear(); if (dataS != null) { this.data.addAll(dataS); Collections.sort(this.data); } } if (dataS != null) { notifyDataSetChanged(); } } public void upData(DownloadBookBean data) { int index = -1; synchronized (mLock) { if (data != null && !this.data.isEmpty()) { index = this.data.indexOf(data); if (index >= 0) { this.data.set(index, data); } } } if (index >= 0) { notifyItemChanged(index, data.getWaitingCount()); } } public void removeData(DownloadBookBean data) { int index = -1; synchronized (mLock) { if (data != null && !this.data.isEmpty()) { index = this.data.indexOf(data); if (index >= 0) { this.data.remove(index); } } } if (index >= 0) { notifyItemRemoved(index); } } public void addData(DownloadBookBean data) { synchronized (mLock) { if (data != null) { this.data.add(data); } } if (data != null) { notifyItemInserted(this.data.size() - 1); } } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_download, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) { } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position, @NonNull List payloads) { final DownloadBookBean item = data.get(holder.getLayoutPosition()); if (!payloads.isEmpty()) { holder.tvName.setText(String.format(Locale.getDefault(), "%s(正在下载)", item.getName())); holder.tvDownload.setText(activity.getString(R.string.un_download, (Integer) payloads.get(0))); } else { holder.ivDel.getDrawable().mutate(); holder.ivDel.getDrawable().setColorFilter(activity.getResources().getColor(R.color.tv_text_default), PorterDuff.Mode.SRC_ATOP); holder.ivCover.load(item.getCoverUrl(), item.getName(), null); if (item.getSuccessCount() > 0) { holder.tvName.setText(String.format(Locale.getDefault(), "%s(正在下载)", item.getName())); } else { holder.tvName.setText(String.format(Locale.getDefault(), "%s(等待下载)", item.getName())); } holder.tvDownload.setText(activity.getString(R.string.un_download, item.getDownloadCount() - item.getSuccessCount())); holder.ivDel.setOnClickListener(view -> DownloadService.removeDownload(activity, item.getNoteUrl())); } } @Override public int getItemCount() { return data.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { CoverImageView ivCover; TextView tvName; TextView tvDownload; ImageView ivDel; MyViewHolder(View itemView) { super(itemView); ivCover = itemView.findViewById(R.id.iv_cover); tvName = itemView.findViewById(R.id.tv_name); tvDownload = itemView.findViewById(R.id.tv_download); ivDel = itemView.findViewById(R.id.iv_delete); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/FileSystemAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.view.adapter.base.BaseListAdapter; import com.kunfei.bookshelf.view.adapter.base.IViewHolder; import com.kunfei.bookshelf.view.adapter.view.FileHolder; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Created by newbiechen on 17-5-27. */ public class FileSystemAdapter extends BaseListAdapter { //记录item是否被选中的Map private HashMap mCheckMap = new HashMap<>(); private int mCheckedCount = 0; @Override protected IViewHolder createViewHolder(int viewType) { return new FileHolder(mCheckMap); } @Override public void refreshItems(List list) { mCheckMap.clear(); for (File file : list) { mCheckMap.put(file, false); } super.refreshItems(list); } @Override public void addItem(File value) { mCheckMap.put(value, false); super.addItem(value); } @Override public void addItem(int index, File value) { mCheckMap.put(value, false); super.addItem(index, value); } @Override public void addItems(List values) { for (File file : values) { mCheckMap.put(file, false); } super.addItems(values); } @Override public void removeItem(File value) { mCheckMap.remove(value); super.removeItem(value); } @Override public void removeItems(List value) { //删除在HashMap中的文件 for (File file : value) { mCheckMap.remove(file); //因为,能够被移除的文件,肯定是选中的 --mCheckedCount; } //删除列表中的文件 super.removeItems(value); } //设置点击切换 public void setCheckedItem(int pos) { File file = getItem(pos); if (isFileLoaded(file.getAbsolutePath())) return; boolean isSelected = mCheckMap.get(file); if (isSelected) { mCheckMap.put(file, false); --mCheckedCount; } else { mCheckMap.put(file, true); ++mCheckedCount; } notifyDataSetChanged(); } public void setCheckedAll(boolean isChecked) { Set> entrys = mCheckMap.entrySet(); mCheckedCount = 0; for (Map.Entry entry : entrys) { //必须是文件,必须没有被收藏 if (entry.getKey().isFile() && !isFileLoaded(entry.getKey().getAbsolutePath())) { entry.setValue(isChecked); //如果选中,则增加点击的数量 if (isChecked) { ++mCheckedCount; } } } notifyDataSetChanged(); } private boolean isFileLoaded(String id) { //如果是已加载的文件,则点击事件无效。 return BookshelfHelp.getBook(id) != null; } public int getCheckableCount() { List files = getItems(); int count = 0; for (File file : files) { if (!isFileLoaded(file.getAbsolutePath()) && file.isFile()) ++count; } return count; } public boolean getItemIsChecked(int pos) { File file = getItem(pos); return mCheckMap.get(file); } public List getCheckedFiles() { List files = new ArrayList<>(); Set> entrys = mCheckMap.entrySet(); for (Map.Entry entry : entrys) { if (entry.getValue()) { files.add(entry.getKey()); } } return files; } public int getCheckedCount() { return mCheckedCount; } public HashMap getCheckMap() { return mCheckMap; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/FindKindAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.appcompat.widget.AppCompatImageView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.FindKindBean; import com.kunfei.bookshelf.bean.FindKindGroupBean; import com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandAbleViewHolder; import com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandableRecyclerAdapter; import com.kunfei.bookshelf.widget.recycler.expandable.bean.GroupItem; import com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData; import java.util.List; /** * Created by GKF on 2017/12/22. * 书源Adapter */ public class FindKindAdapter extends BaseExpandableRecyclerAdapter { public FindKindAdapter(Context ctx, List datas) { super(ctx, datas); } /** * return groupView */ @Override public View getGroupView(ViewGroup parent) { return LayoutInflater.from(parent.getContext()).inflate(R.layout.item_find1_group, parent, false); } /** * return childView */ @Override public View getChildView(ViewGroup parent) { return LayoutInflater.from(parent.getContext()).inflate(R.layout.item_find1_kind, parent, false); } /** * return instance */ @Override public MyViewHolder createRealViewHolder(Context ctx, View view, int viewType) { return new MyViewHolder(ctx, view, viewType); } /** * onBind groupData to groupView */ @Override public void onBindGroupHolder(MyViewHolder holder, int groupPos, int position, FindKindGroupBean groupData) { holder.textView.setText(groupData.getGroupName()); GroupItem item = getAllDatas().get(groupPos).getGroupItem(); if (item.isExpand()) { holder.imageView.setImageResource(R.drawable.ic_expand_less_24dp); } else { holder.imageView.setImageResource(R.drawable.ic_expand_more_24dp); } } /** * onBind childData to childView */ @Override public void onBindChildpHolder(MyViewHolder holder, int groupPos, int childPos, int position, FindKindBean childData) { holder.textView.setText(childData.getKindName()); } public class MyViewHolder extends BaseExpandAbleViewHolder { TextView textView; AppCompatImageView imageView; public MyViewHolder(Context ctx, View itemView, int viewType) { super(ctx, itemView, viewType); textView = itemView.findViewById(R.id.tv_kind_name); if (viewType == VIEW_TYPE_PARENT) { imageView = itemView.findViewById(R.id.iv_group); } } /** * return ChildView root layout id */ @Override public int getChildViewResId() { return R.id.ll_content; } /** * return GroupView root layout id */ @Override public int getGroupViewResId() { return R.id.ll_content; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/FindLeftAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.FindKindGroupBean; import com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData; import java.util.ArrayList; import java.util.List; public class FindLeftAdapter extends RecyclerView.Adapter { private Context context; private int showIndex = 0; private List data = new ArrayList<>(); private OnClickListener onClickListener; public FindLeftAdapter(Context context, OnClickListener onClickListener) { this.context = context; this.onClickListener = onClickListener; } public void setData(List data) { this.data.clear(); this.data.addAll(data); notifyDataSetChanged(); } public void upShowIndex(int showIndex) { if (showIndex != this.showIndex) { int oldIndex = this.showIndex; this.showIndex = showIndex; notifyItemChanged(oldIndex); notifyItemChanged(this.showIndex); } } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { return new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_find_left, viewGroup, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, @SuppressLint("RecyclerView") int i) { FindKindGroupBean groupBean = (FindKindGroupBean) data.get(i).getGroupData(); myViewHolder.tvSourceName.setText(groupBean.getGroupName()); if (i == showIndex) { myViewHolder.tvSourceName.setBackgroundColor(context.getResources().getColor(R.color.transparent30)); } else { myViewHolder.tvSourceName.setBackgroundColor(Color.TRANSPARENT); } myViewHolder.tvSourceName.setOnClickListener(v -> { if (onClickListener != null) { int oldIndex = showIndex; showIndex = i; notifyItemChanged(oldIndex); notifyItemChanged(showIndex); onClickListener.click(showIndex); } }); } @Override public int getItemCount() { return data.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { TextView tvSourceName; public MyViewHolder(@NonNull View itemView) { super(itemView); tvSourceName = itemView.findViewById(R.id.tv_source_name); } } public interface OnClickListener { void click(int pos); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/FindRightAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.content.Context; import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.FindKindBean; import com.kunfei.bookshelf.bean.FindKindGroupBean; import com.kunfei.bookshelf.view.activity.ChoiceBookActivity; import com.kunfei.bookshelf.widget.recycler.expandable.OnRecyclerViewListener; import com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData; import com.kunfei.bookshelf.widget.recycler.sectioned.SectionedRecyclerViewAdapter; import java.util.ArrayList; import java.util.List; public class FindRightAdapter extends SectionedRecyclerViewAdapter { private List data = new ArrayList<>(); private LayoutInflater inflater; private Context context; private OnRecyclerViewListener.OnItemLongClickListener onItemLongClickListener; public FindRightAdapter(Context context, OnRecyclerViewListener.OnItemLongClickListener onItemLongClickListener) { this.context = context; this.onItemLongClickListener = onItemLongClickListener; inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } public void setData(List data) { this.data.clear(); this.data.addAll(data); notifyDataSetChanged(); } @Override protected int getSectionCount() { return data.size(); } @Override protected int getItemCountForSection(int section) { return data.get(section).getChildList().size(); } @Override protected boolean hasFooterInSection(int section) { return false; } @Override protected HeaderHolder onCreateSectionHeaderViewHolder(ViewGroup parent, int viewType) { return new HeaderHolder(inflater.inflate(R.layout.item_find2_header_view, parent, false)); } @Override protected RecyclerView.ViewHolder onCreateSectionFooterViewHolder(ViewGroup parent, int viewType) { return null; } @Override protected DescHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { return new DescHolder(inflater.inflate(R.layout.item_find2_childer_view, parent, false)); } @Override protected void onBindSectionHeaderViewHolder(HeaderHolder holder, int section) { RecyclerViewData recyclerViewData = data.get(section); holder.tv_source_name.setText(((FindKindGroupBean) recyclerViewData.getGroupData()).getGroupName()); holder.tv_source_name.setOnLongClickListener(v -> { if (onItemLongClickListener != null) { onItemLongClickListener.onGroupItemLongClick(section, section, holder.tv_source_name); } return true; }); } @Override protected void onBindSectionFooterViewHolder(RecyclerView.ViewHolder holder, int section) { } @Override protected void onBindItemViewHolder(DescHolder holder, int section, int position) { try { FindKindBean kindBean = (FindKindBean) data.get(section).getChild(position); holder.tv_item.setHorizontallyScrolling(false); holder.tv_item.setText(kindBean.getKindName()); holder.tv_item.setOnClickListener(view -> { Intent intent = new Intent(context, ChoiceBookActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("url", kindBean.getKindUrl()); intent.putExtra("title", kindBean.getKindName()); intent.putExtra("tag", kindBean.getTag()); context.startActivity(intent); }); } catch (Exception e) { e.printStackTrace(); } } public List getData() { return data; } static class HeaderHolder extends RecyclerView.ViewHolder { TextView tv_source_name; HeaderHolder(View itemView) { super(itemView); tv_source_name = itemView.findViewById(R.id.tv_source_name); } } static class DescHolder extends RecyclerView.ViewHolder { TextView tv_item; DescHolder(View view) { super(view); tv_item = view.findViewById(R.id.tv_item); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/ReplaceRuleAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.activity.ReplaceRuleActivity; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Created by GKF on 2017/12/22. * 书源Adapter */ public class ReplaceRuleAdapter extends RecyclerView.Adapter { private List data; private ReplaceRuleActivity activity; private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() { @Override public void onSwiped(int adapterPosition) { } @Override public boolean onMove(int srcPosition, int targetPosition) { Collections.swap(data, srcPosition, targetPosition); notifyItemMoved(srcPosition, targetPosition); notifyItemChanged(srcPosition); notifyItemChanged(targetPosition); activity.saveDataS(); return true; } }; public ReplaceRuleAdapter(ReplaceRuleActivity activity) { this.activity = activity; data = new ArrayList<>(); } public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() { return itemTouchCallbackListener; } public void resetDataS(List dataList) { this.data.clear(); this.data.addAll(dataList); notifyDataSetChanged(); activity.upDateSelectAll(); } public List getData() { return data; } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_replace_rule, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity)); holder.checkBox.setText(data.get(position).getReplaceSummary()); holder.checkBox.setChecked(data.get(position).getEnable()); holder.checkBox.setOnClickListener((View view) -> { data.get(position).setEnable(holder.checkBox.isChecked()); activity.upDateSelectAll(); activity.saveDataS(); }); holder.editView.setOnClickListener(view -> activity.editReplaceRule(data.get(position))); holder.delView.setOnClickListener(view -> { activity.delData(data.get(position)); data.remove(position); notifyDataSetChanged(); }); } @Override public int getItemCount() { return data.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { CheckBox checkBox; ImageView editView; ImageView delView; MyViewHolder(View itemView) { super(itemView); checkBox = itemView.findViewById(R.id.cb_replace_rule); editView = itemView.findViewById(R.id.iv_edit); delView = itemView.findViewById(R.id.iv_del); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/SearchBookAdapter.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.adapter; import android.annotation.SuppressLint; import android.app.Activity; import android.os.AsyncTask; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookKindBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.adapter.base.BaseListAdapter; import com.kunfei.bookshelf.widget.image.CoverImageView; import com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import static com.kunfei.bookshelf.utils.StringUtils.isTrimEmpty; public class SearchBookAdapter extends RefreshRecyclerViewAdapter { private WeakReference activityRef; private List searchBooks; private BaseListAdapter.OnItemClickListener itemClickListener; public SearchBookAdapter(Activity activity) { super(true); this.activityRef = new WeakReference<>(activity); searchBooks = new ArrayList<>(); } @Override public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_search_book, parent, false)); } @SuppressLint("DefaultLocale") @Override public void onBindIViewHolder(final RecyclerView.ViewHolder holder, final int position) { Activity activity = activityRef.get(); holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity)); MyViewHolder myViewHolder = (MyViewHolder) holder; myViewHolder.flContent.setOnClickListener(v -> { if (itemClickListener != null) itemClickListener.onItemClick(v, position); }); SearchBookBean book = searchBooks.get(position); if (!activity.isFinishing()) { myViewHolder.ivCover.load(book.getCoverUrl(), book.getName(), book.getAuthor()); } myViewHolder.tvName.setText(String.format("%s (%s)", book.getName(), book.getAuthor())); BookKindBean bookKindBean = new BookKindBean(book.getKind()); if (isTrimEmpty(bookKindBean.getKind())) { myViewHolder.tvKind.setVisibility(View.GONE); } else { myViewHolder.tvKind.setVisibility(View.VISIBLE); myViewHolder.tvKind.setText(bookKindBean.getKind()); } if (isTrimEmpty(bookKindBean.getWordsS())) { myViewHolder.tvWords.setVisibility(View.GONE); } else { myViewHolder.tvWords.setVisibility(View.VISIBLE); myViewHolder.tvWords.setText(bookKindBean.getWordsS()); } if (isTrimEmpty(bookKindBean.getState())) { myViewHolder.tvState.setVisibility(View.GONE); } else { myViewHolder.tvState.setVisibility(View.VISIBLE); myViewHolder.tvState.setText(bookKindBean.getState()); } //来源 if (isTrimEmpty(book.getOrigin())) { myViewHolder.tvOrigin.setVisibility(View.GONE); } else { myViewHolder.tvOrigin.setVisibility(View.VISIBLE); myViewHolder.tvOrigin.setText(activity.getString(R.string.origin_format, book.getOrigin())); } //最新章节 if (isTrimEmpty(book.getLastChapter())) { myViewHolder.tvLasted.setVisibility(View.GONE); } else { myViewHolder.tvLasted.setText(book.getLastChapter()); myViewHolder.tvLasted.setVisibility(View.VISIBLE); } //简介 if (isTrimEmpty(book.getIntroduce())) { myViewHolder.tvIntroduce.setVisibility(View.GONE); } else { myViewHolder.tvIntroduce.setText(StringUtils.formatHtml(book.getIntroduce())); myViewHolder.tvIntroduce.setVisibility(View.VISIBLE); } myViewHolder.tvOriginNum.setText(String.format("共%d个源", book.getOriginNum())); } @Override public int getIViewType(int position) { return 0; } @Override public int getICount() { return searchBooks.size(); } public void setItemClickListener(BaseListAdapter.OnItemClickListener itemClickListener) { this.itemClickListener = itemClickListener; } public synchronized void upData(DataAction action, List newDataS) { switch (action) { case ADD: searchBooks = newDataS; notifyDataSetChanged(); break; case CLEAR: if (!searchBooks.isEmpty()) { try { Glide.with(activityRef.get()).onDestroy(); } catch (Exception ignored) { } searchBooks.clear(); notifyDataSetChanged(); } break; } } public synchronized void addAll(List newDataS, String keyWord) { List copyDataS = new ArrayList<>(searchBooks); if (newDataS != null && newDataS.size() > 0) { saveData(newDataS); List searchBookBeansAdd = new ArrayList<>(); if (copyDataS.size() == 0) { copyDataS.addAll(newDataS); } else { //存在 for (SearchBookBean temp : newDataS) { boolean hasSame = false; for (int i = 0, size = copyDataS.size(); i < size; i++) { SearchBookBean searchBook = copyDataS.get(i); if (TextUtils.equals(temp.getName(), searchBook.getName()) && TextUtils.equals(temp.getAuthor(), searchBook.getAuthor())) { hasSame = true; searchBook.addOriginUrl(temp.getTag()); break; } } if (!hasSame) { searchBookBeansAdd.add(temp); } } //添加 for (SearchBookBean temp : searchBookBeansAdd) { if (TextUtils.equals(keyWord, temp.getName())) { for (int i = 0; i < copyDataS.size(); i++) { SearchBookBean searchBook = copyDataS.get(i); if (!TextUtils.equals(keyWord, searchBook.getName())) { copyDataS.add(i, temp); break; } } } else if (TextUtils.equals(keyWord, temp.getAuthor())) { for (int i = 0; i < copyDataS.size(); i++) { SearchBookBean searchBook = copyDataS.get(i); if (!TextUtils.equals(keyWord, searchBook.getName()) && !TextUtils.equals(keyWord, searchBook.getAuthor())) { copyDataS.add(i, temp); break; } } } else { copyDataS.add(temp); } } } Activity activity = activityRef.get(); if (activity != null) { activity.runOnUiThread(() -> upData(DataAction.ADD, copyDataS)); } } } private void saveData(List data) { AsyncTask.execute(() -> DbHelper.getDaoSession().getSearchBookBeanDao().insertOrReplaceInTx(data)); } public SearchBookBean getItemData(int pos) { return searchBooks.get(pos); } static class MyViewHolder extends RecyclerView.ViewHolder { ViewGroup flContent; CoverImageView ivCover; TextView tvName; TextView tvState; TextView tvWords; TextView tvKind; TextView tvLasted; TextView tvOrigin; TextView tvOriginNum; TextView tvIntroduce; MyViewHolder(View itemView) { super(itemView); flContent = itemView.findViewById(R.id.fl_content); ivCover = itemView.findViewById(R.id.iv_cover); tvName = itemView.findViewById(R.id.tv_name); tvState = itemView.findViewById(R.id.tv_state); tvWords = itemView.findViewById(R.id.tv_words); tvLasted = itemView.findViewById(R.id.tv_lasted); tvKind = itemView.findViewById(R.id.tv_kind); tvOrigin = itemView.findViewById(R.id.tv_origin); tvOriginNum = itemView.findViewById(R.id.tv_origin_num); tvIntroduce = itemView.findViewById(R.id.tv_introduce); } } public enum DataAction { ADD, CLEAR } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/SearchBookshelfAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookInfoBean; import java.util.ArrayList; import java.util.List; public class SearchBookshelfAdapter extends RecyclerView.Adapter { private List beans = new ArrayList<>(); private CallBack callBack; public SearchBookshelfAdapter(CallBack callBack) { this.callBack = callBack; } public void setItems(List beans) { this.beans.clear(); this.beans.addAll(beans); notifyDataSetChanged(); } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_search_history, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { holder.textView.setText(beans.get(position).getName()); holder.itemView.setOnClickListener(v -> callBack.openBookInfo(beans.get(position))); } @Override public int getItemCount() { return beans.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { TextView textView; public MyViewHolder(@NonNull View itemView) { super(itemView); textView = itemView.findViewById(R.id.tv); } } public interface CallBack { void openBookInfo(BookInfoBean bookInfoBean); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/SourceDebugAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; import com.kunfei.bookshelf.R; import java.util.ArrayList; import java.util.List; public class SourceDebugAdapter extends Adapter { private Context context; private List data = new ArrayList<>(); public SourceDebugAdapter(Context context) { this.context = context; } public void clearData() { data.clear(); notifyDataSetChanged(); } public void addData(String msg) { data.add(msg); notifyItemChanged(data.size() - 1); } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_source_debug, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { if (holder.textView.getTag(R.id.tag1) == null) { View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { holder.textView.setCursorVisible(false); holder.textView.setCursorVisible(true); } @Override public void onViewDetachedFromWindow(View v) { } }; holder.textView.addOnAttachStateChangeListener(listener); holder.textView.setTag(R.id.tag1, listener); } holder.textView.setText(data.get(position)); } @Override public int getItemCount() { return data.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { TextView textView; public MyViewHolder(@NonNull View itemView) { super(itemView); textView = itemView.findViewById(R.id.tv); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/SourceEditAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.content.Context; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatEditText; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; import com.google.android.material.textfield.TextInputLayout; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.view.activity.SourceEditActivity; import java.util.ArrayList; import java.util.List; public class SourceEditAdapter extends Adapter { private Context context; private List data = new ArrayList<>(); public SourceEditAdapter(Context context) { this.context = context; } public void reSetData(List data) { this.data = data; notifyDataSetChanged(); } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_source_edit, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { if (holder.editText.getTag(R.id.tag1) == null) { View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { holder.editText.setCursorVisible(false); holder.editText.setCursorVisible(true); holder.editText.setFocusable(true); holder.editText.setFocusableInTouchMode(true); } @Override public void onViewDetachedFromWindow(View v) { } }; holder.editText.addOnAttachStateChangeListener(listener); holder.editText.setTag(R.id.tag1, listener); } if (holder.editText.getTag(R.id.tag2) != null && holder.editText.getTag(R.id.tag2) instanceof TextWatcher) { holder.editText.removeTextChangedListener((TextWatcher) holder.editText.getTag(R.id.tag2)); } holder.editText.setText(data.get(position).getValue()); holder.textInputLayout.setHint(context.getString(data.get(position).getHint())); TextWatcher textWatcher = 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) { data.get(position).setValue(s == null ? null : s.toString()); } }; holder.editText.addTextChangedListener(textWatcher); holder.editText.setTag(R.id.tag2, textWatcher); } @Override public int getItemCount() { return data.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { TextInputLayout textInputLayout; AppCompatEditText editText; public MyViewHolder(@NonNull View itemView) { super(itemView); textInputLayout = itemView.findViewById(R.id.textInputLayout); editText = itemView.findViewById(R.id.editText); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/TxtChapterRuleAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.TxtChapterRuleBean; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.activity.TxtChapterRuleActivity; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Created by GKF on 2017/12/22. * 书源Adapter */ public class TxtChapterRuleAdapter extends RecyclerView.Adapter { private List data; private TxtChapterRuleActivity activity; private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() { @Override public void onSwiped(int adapterPosition) { } @Override public boolean onMove(int srcPosition, int targetPosition) { Collections.swap(data, srcPosition, targetPosition); notifyItemMoved(srcPosition, targetPosition); notifyItemChanged(srcPosition); notifyItemChanged(targetPosition); activity.saveDataS(); return true; } }; public TxtChapterRuleAdapter(TxtChapterRuleActivity activity) { this.activity = activity; data = new ArrayList<>(); } public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() { return itemTouchCallbackListener; } public void resetDataS(List dataList) { this.data.clear(); this.data.addAll(dataList); notifyDataSetChanged(); activity.upDateSelectAll(); } public List getData() { return data; } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_replace_rule, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity)); holder.checkBox.setText(data.get(position).getName()); holder.checkBox.setChecked(data.get(position).getEnable()); holder.checkBox.setOnClickListener((View view) -> { data.get(position).setEnable(holder.checkBox.isChecked()); activity.upDateSelectAll(); activity.saveDataS(); }); holder.editView.setOnClickListener(view -> activity.editChapterRule(data.get(position))); holder.delView.setOnClickListener(view -> { activity.delData(data.get(position)); data.remove(position); notifyDataSetChanged(); }); } @Override public int getItemCount() { return data.size(); } static class MyViewHolder extends RecyclerView.ViewHolder { CheckBox checkBox; ImageView editView; ImageView delView; MyViewHolder(View itemView) { super(itemView); checkBox = itemView.findViewById(R.id.cb_replace_rule); editView = itemView.findViewById(R.id.iv_edit); delView = itemView.findViewById(R.id.iv_del); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/base/BaseListAdapter.java ================================================ package com.kunfei.bookshelf.view.adapter.base; import android.annotation.SuppressLint; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Created by newbiechen on 17-3-21. */ public abstract class BaseListAdapter extends RecyclerView.Adapter { private static final String TAG = "BaseListAdapter"; /*common statement*/ protected final List mList = new ArrayList<>(); protected OnItemClickListener mClickListener; protected OnItemLongClickListener mLongClickListener; /************************abstract area************************/ protected abstract IViewHolder createViewHolder(int viewType); /*************************rewrite logic area***************************************/ @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { IViewHolder viewHolder = createViewHolder(viewType); View view = viewHolder.createItemView(parent); //初始化 return new BaseViewHolder<>(view, viewHolder); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { //防止别人直接使用RecyclerView.ViewHolder调用该方法 if (!(holder instanceof BaseViewHolder)) throw new IllegalArgumentException("The ViewHolder item must extend BaseViewHolder"); @SuppressWarnings({"rawtypes", "unchecked"}) IViewHolder iHolder = ((BaseViewHolder) holder).holder; iHolder.onBind(getItem(position), position); //设置点击事件 holder.itemView.setOnClickListener((v) -> { if (mClickListener != null) { mClickListener.onItemClick(v, position); } //adapter监听点击事件 iHolder.onClick(); onItemClick(v, position); }); //设置长点击事件 holder.itemView.setOnLongClickListener( (v) -> { boolean isClicked = false; if (mLongClickListener != null) { isClicked = mLongClickListener.onItemLongClick(v, position); } //Adapter监听长点击事件 onItemLongClick(v, position); return isClicked; } ); } @Override public int getItemCount() { return mList.size(); } protected void onItemClick(View v, int pos) { } protected void onItemLongClick(View v, int pos) { } /******************************public area***********************************/ public void setOnItemClickListener(OnItemClickListener mListener) { this.mClickListener = mListener; } public void setOnItemLongClickListener(OnItemLongClickListener mListener) { this.mLongClickListener = mListener; } @SuppressLint("NotifyDataSetChanged") public void addItem(T value) { mList.add(value); notifyDataSetChanged(); } @SuppressLint("NotifyDataSetChanged") public void addItem(int index, T value) { mList.add(index, value); notifyDataSetChanged(); } @SuppressLint("NotifyDataSetChanged") public void addItems(List values) { mList.addAll(values); notifyDataSetChanged(); } @SuppressLint("NotifyDataSetChanged") public void removeItem(T value) { mList.remove(value); notifyDataSetChanged(); } @SuppressLint("NotifyDataSetChanged") public void removeItems(List value) { mList.removeAll(value); notifyDataSetChanged(); } public T getItem(int position) { return mList.get(position); } public List getItems() { return Collections.unmodifiableList(mList); } public int getItemSize() { return mList.size(); } public void refreshItems(List list) { mList.clear(); mList.addAll(list); notifyDataSetChanged(); } public void clear() { mList.clear(); } /***************************inner class area***********************************/ public interface OnItemClickListener { void onItemClick(View view, int pos); } public interface OnItemLongClickListener { boolean onItemLongClick(View view, int pos); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/base/BaseViewHolder.java ================================================ package com.kunfei.bookshelf.view.adapter.base; import android.view.View; import androidx.recyclerview.widget.RecyclerView; /** * Created by newbiechen on 17-5-17. */ public class BaseViewHolder extends RecyclerView.ViewHolder { public IViewHolder holder; public BaseViewHolder(View itemView, IViewHolder holder) { super(itemView); this.holder = holder; holder.initView(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/base/IViewHolder.java ================================================ package com.kunfei.bookshelf.view.adapter.base; import android.view.View; import android.view.ViewGroup; /** * Created by newbiechen on 17-5-17. */ public interface IViewHolder { View createItemView(ViewGroup parent); void initView(); void onBind(T data, int pos); void onClick(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/base/OnItemClickListenerTwo.java ================================================ package com.kunfei.bookshelf.view.adapter.base; import android.view.View; public interface OnItemClickListenerTwo { void onClick(View view, int index); void onLongClick(View view, int index); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/base/ViewHolderImpl.java ================================================ package com.kunfei.bookshelf.view.adapter.base; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; /** * Created by newbiechen on 17-5-17. */ public abstract class ViewHolderImpl implements IViewHolder { private View view; private Context context; /****************************************************/ protected abstract int getItemLayoutId(); @Override public View createItemView(ViewGroup parent) { view = LayoutInflater.from(parent.getContext()) .inflate(getItemLayoutId(), parent, false); context = parent.getContext(); return view; } @SuppressWarnings("unchecked") protected V findById(int id) { return (V) view.findViewById(id); } protected Context getContext() { return context; } protected View getItemView() { return view; } @Override public void onClick() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/adapter/view/FileHolder.java ================================================ package com.kunfei.bookshelf.view.adapter.view; import android.view.View; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.constant.AppConstant; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.FileHelp; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.view.adapter.base.ViewHolderImpl; import java.io.File; import java.util.HashMap; /** * Created by newbiechen on 17-5-27. */ public class FileHolder extends ViewHolderImpl { private ImageView mIvIcon; private CheckBox mCbSelect; private TextView mTvName; private LinearLayout mLlBrief; private TextView mTvTag; private TextView mTvSize; private TextView mTvDate; private TextView mTvSubCount; private HashMap mSelectedMap; public FileHolder(HashMap selectedMap) { mSelectedMap = selectedMap; } @Override public void initView() { mIvIcon = findById(R.id.file_iv_icon); mCbSelect = findById(R.id.file_cb_select); mTvName = findById(R.id.file_tv_name); mLlBrief = findById(R.id.file_ll_brief); mTvTag = findById(R.id.file_tv_tag); mTvSize = findById(R.id.file_tv_size); mTvDate = findById(R.id.file_tv_date); mTvSubCount = findById(R.id.file_tv_sub_count); } @Override public void onBind(File data, int pos) { //判断是文件还是文件夹 if (data.isDirectory()) { setFolder(data); } else { setFile(data); } mCbSelect.setClickable(false); } private void setFile(File file) { //选择 if (BookshelfHelp.getBook(file.getAbsolutePath()) != null) { mIvIcon.setImageResource(R.drawable.ic_book_has); mIvIcon.setVisibility(View.VISIBLE); mCbSelect.setVisibility(View.GONE); } else { boolean isSelected = mSelectedMap.get(file); mCbSelect.setChecked(isSelected); mIvIcon.setVisibility(View.GONE); mCbSelect.setVisibility(View.VISIBLE); } mLlBrief.setVisibility(View.VISIBLE); mTvSubCount.setVisibility(View.GONE); mTvName.setText(file.getName()); mTvTag.setText(file.getName().substring(file.getName().lastIndexOf(".") + 1).toUpperCase()); mTvSize.setText(FileHelp.getFileSize(file.length())); mTvDate.setText(StringUtils.dateConvert(file.lastModified(), AppConstant.FORMAT_FILE_DATE)); } public void setFolder(File folder) { //图片 mIvIcon.setVisibility(View.VISIBLE); mCbSelect.setVisibility(View.GONE); mIvIcon.setImageResource(R.drawable.ic_folder); //名字 mTvName.setText(folder.getName()); //介绍 mLlBrief.setVisibility(View.GONE); mTvSubCount.setVisibility(View.VISIBLE); mTvSubCount.setText(getContext().getString(R.string.nb_file_sub_count, folder.list().length)); } @Override protected int getItemLayoutId() { return R.layout.item_file; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/dialog/SourceLoginDialog.kt ================================================ package com.kunfei.bookshelf.view.dialog import android.os.Bundle import android.text.InputType import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.EditText import android.widget.TextView import androidx.core.os.bundleOf import androidx.core.view.setPadding import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import com.google.android.material.textfield.TextInputLayout import com.kunfei.bookshelf.R import com.kunfei.bookshelf.databinding.DialogLoginBinding import com.kunfei.bookshelf.model.BookSourceManager import com.kunfei.bookshelf.utils.* import com.kunfei.bookshelf.utils.theme.ThemeStore import com.kunfei.bookshelf.utils.viewbindingdelegate.viewBinding import io.reactivex.Single import io.reactivex.SingleObserver import io.reactivex.disposables.Disposable import org.jetbrains.anko.sdk27.listeners.onClick class SourceLoginDialog : DialogFragment() { companion object { fun start(fragmentManager: FragmentManager, sourceUrl: String) { SourceLoginDialog().apply { arguments = bundleOf( Pair("sourceUrl", sourceUrl) ) }.show(fragmentManager, "sourceLoginDialog") } } val binding by viewBinding(DialogLoginBinding::bind) override fun onStart() { super.onStart() dialog?.window?.setLayout( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.dialog_login, container) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.toolBar.setBackgroundColor(ThemeStore.primaryColor(requireContext())) binding.toolBar.title = getString(R.string.login) val sourceUrl = arguments?.getString("sourceUrl") val source = BookSourceManager.getBookSourceByUrl(sourceUrl) source ?: let { dismiss() return } binding.toolBar.title = getString(R.string.login_source, source.bookSourceName) val loginInfo = source.loginInfoMap val loginUi = GSON.fromJsonArray(source.loginUi) loginUi?.forEachIndexed { index, rowUi -> when (rowUi.type) { "text" -> layoutInflater.inflate(R.layout.item_source_edit, binding.root, false) .let { binding.listView.addView(it) it.id = index (it as TextInputLayout).hint = rowUi.name it.findViewById(R.id.editText).apply { setText(loginInfo?.get(rowUi.name)) } } "password" -> layoutInflater.inflate(R.layout.item_source_edit, binding.root, false) .let { binding.listView.addView(it) it.id = index (it as TextInputLayout).hint = rowUi.name it.findViewById(R.id.editText).apply { inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT setText(loginInfo?.get(rowUi.name)) } } "button" -> layoutInflater.inflate( R.layout.item_find2_childer_view, binding.root, false ) .let { binding.listView.addView(it) it.id = index (it as TextView).let { textView -> textView.text = rowUi.name textView.setPadding(DensityUtil.dp2px(requireContext(), 16f)) } it.onClick { if (rowUi.action.isAbsUrl()) { context?.openUrl(rowUi.action!!) } } } } } binding.toolBar.inflateMenu(R.menu.menu_source_login) binding.toolBar.setOnMenuItemClickListener { when (it.itemId) { R.id.action_check -> { val loginData = hashMapOf() loginUi?.forEachIndexed { index, rowUi -> when (rowUi.type) { "text", "password" -> { val value = binding.listView.findViewById(index) .findViewById(R.id.editText).text?.toString() loginData[rowUi.name] = value } } } source.putLoginInfo(loginData) Single.create { emitter -> source.loginUrl?.let { loginUrl -> emitter.onSuccess(source.evalJS(loginUrl).toString()) } ?: let { emitter.onError(Throwable("")) } }.compose(RxUtils::toSimpleSingle) .subscribe(object : SingleObserver { override fun onSubscribe(d: Disposable) { } override fun onSuccess(t: String) { dismiss() } override fun onError(e: Throwable) { } }) } } return@setOnMenuItemClickListener true } } data class RowUi( var name: String, var type: String, var action: String? ) } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/BaseFileFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.base.MBaseFragment; import com.kunfei.bookshelf.view.adapter.FileSystemAdapter; import java.io.File; import java.util.List; /** * Created by newbiechen on 17-7-10. * FileSystemActivity的基础Fragment类 */ public abstract class BaseFileFragment extends MBaseFragment { protected FileSystemAdapter mAdapter; protected OnFileCheckedListener mListener; protected boolean isCheckedAll; public void setChecked(boolean checked) { isCheckedAll = checked; } //当前fragment是否全选 public boolean isCheckedAll() { return isCheckedAll; } //设置当前列表为全选 public void setCheckedAll(boolean checkedAll) { if (mAdapter == null) return; isCheckedAll = checkedAll; mAdapter.setCheckedAll(checkedAll); } //获取被选中的数量 public int getCheckedCount() { if (mAdapter == null) return 0; return mAdapter.getCheckedCount(); } //获取被选中的文件列表 public List getCheckedFiles() { return mAdapter != null ? mAdapter.getCheckedFiles() : null; } //获取文件的总数 public int getFileCount() { return mAdapter != null ? mAdapter.getItemCount() : 0; } //获取可点击的文件的数量 public int getCheckableCount() { if (mAdapter == null) return 0; return mAdapter.getCheckableCount(); } /** * 删除选中的文件 */ public void deleteCheckedFiles() { //删除选中的文件 List files = getCheckedFiles(); //删除显示的文件列表 mAdapter.removeItems(files); //删除选中的文件 for (File file : files) { if (file.exists()) { //noinspection ResultOfMethodCallIgnored file.delete(); } } } //设置文件点击监听事件 public void setOnFileCheckedListener(OnFileCheckedListener listener) { mListener = listener; } //文件点击监听 public interface OnFileCheckedListener { void onItemCheckedChange(boolean isChecked); void onCategoryChanged(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/BookListFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; import com.kunfei.basemvplib.BitIntentDataManager; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseFragment; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.databinding.FragmentBookListBinding; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.ItemTouchCallback; import com.kunfei.bookshelf.presenter.BookDetailPresenter; import com.kunfei.bookshelf.presenter.BookListPresenter; import com.kunfei.bookshelf.presenter.ReadBookPresenter; import com.kunfei.bookshelf.presenter.contract.BookListContract; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.activity.BookDetailActivity; import com.kunfei.bookshelf.view.activity.ReadBookActivity; import com.kunfei.bookshelf.view.adapter.BookShelfAdapter; import com.kunfei.bookshelf.view.adapter.BookShelfGridAdapter; import com.kunfei.bookshelf.view.adapter.BookShelfListAdapter; import com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo; import java.util.List; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; public class BookListFragment extends MBaseFragment implements BookListContract.View { private CallbackValue callbackValue; private FragmentBookListBinding binding; private String bookPx; private boolean resumed = false; private boolean isRecreate; private int group; private BookShelfAdapter bookShelfAdapter; @Override public void onCreate(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { resumed = savedInstanceState.getBoolean("resumed"); } super.onCreate(savedInstanceState); } @Override protected View createView(LayoutInflater inflater, ViewGroup container) { binding = FragmentBookListBinding.inflate(inflater, container, false); return binding.getRoot(); } @Override protected BookListContract.Presenter initInjector() { return new BookListPresenter(); } @Override protected void initData() { callbackValue = (CallbackValue) getActivity(); bookPx = preferences.getString(getString(R.string.pk_bookshelf_px), "0"); isRecreate = callbackValue != null && callbackValue.isRecreate(); } @Override protected void bindView() { super.bindView(); int bookshelfLayout = preferences.getInt("bookshelfLayout", 0); if (bookshelfLayout == 0) { binding.rvBookshelf.setLayoutManager(new LinearLayoutManager(getContext())); bookShelfAdapter = new BookShelfListAdapter(getActivity()); } else { binding.rvBookshelf.setLayoutManager(new GridLayoutManager(getContext(), bookshelfLayout + 2)); bookShelfAdapter = new BookShelfGridAdapter(getActivity()); } binding.rvBookshelf.setAdapter((RecyclerView.Adapter) bookShelfAdapter); binding.refreshLayout.setColorSchemeColors(ThemeStore.accentColor(MApplication.getInstance())); } @Override protected void firstRequest() { group = preferences.getInt("bookshelfGroup", 0); boolean needRefresh = preferences.getBoolean(getString(R.string.pk_auto_refresh), false) && !isRecreate && NetworkUtils.isNetWorkAvailable() && group != 2; mPresenter.queryBookShelf(needRefresh, group); } @Override protected void bindEvent() { binding.refreshLayout.setOnRefreshListener(() -> { mPresenter.queryBookShelf(NetworkUtils.isNetWorkAvailable(), group); if (!NetworkUtils.isNetWorkAvailable()) { Toast.makeText(getContext(), R.string.network_connection_unavailable, Toast.LENGTH_SHORT).show(); } binding.refreshLayout.setRefreshing(false); }); ItemTouchCallback itemTouchCallback = new ItemTouchCallback(); itemTouchCallback.setSwipeRefreshLayout(binding.refreshLayout); itemTouchCallback.setViewPager(callbackValue.getViewPager()); if (bookPx.equals("2")) { itemTouchCallback.setDragEnable(true); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback); itemTouchHelper.attachToRecyclerView(binding.rvBookshelf); } else { itemTouchCallback.setDragEnable(false); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback); itemTouchHelper.attachToRecyclerView(binding.rvBookshelf); } bookShelfAdapter.setItemClickListener(getAdapterListener()); itemTouchCallback.setOnItemTouchCallbackListener(bookShelfAdapter.getItemTouchCallbackListener()); binding.ivBack.setOnClickListener(v -> setArrange(false)); binding.ivDel.setOnClickListener(v -> { if (bookShelfAdapter.getSelected().size() == bookShelfAdapter.getBooks().size()) { AlertDialog alertDialog = new AlertDialog.Builder(requireContext()) .setTitle(R.string.delete) .setMessage(getString(R.string.sure_del_all_book)) .setPositiveButton(R.string.yes, (dialog, which) -> delSelect()) .setNegativeButton(R.string.no, null) .show(); ATH.setAlertDialogTint(alertDialog); } else { delSelect(); } }); binding.ivSelectAll.setOnClickListener(v -> bookShelfAdapter.selectAll()); } private OnItemClickListenerTwo getAdapterListener() { return new OnItemClickListenerTwo() { @Override public void onClick(View view, int index) { if (binding.actionBar.getVisibility() == View.VISIBLE) { upSelectCount(); return; } BookShelfBean bookShelfBean = bookShelfAdapter.getBooks().get(index); Intent intent = new Intent(getContext(), ReadBookActivity.class); intent.putExtra("openFrom", ReadBookPresenter.OPEN_FROM_APP); String key = String.valueOf(System.currentTimeMillis()); String bookKey = "book" + key; intent.putExtra("bookKey", bookKey); BitIntentDataManager.getInstance().putData(bookKey, bookShelfBean.clone()); startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out); } @Override public void onLongClick(View view, int index) { BookShelfBean bookShelfBean = bookShelfAdapter.getBooks().get(index); String key = String.valueOf(System.currentTimeMillis()); BitIntentDataManager.getInstance().putData(key, bookShelfBean.clone()); Intent intent = new Intent(getActivity(), BookDetailActivity.class); intent.putExtra("openFrom", BookDetailPresenter.FROM_BOOKSHELF); intent.putExtra("data_key", key); intent.putExtra("noteUrl", bookShelfBean.getNoteUrl()); startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out); } }; } @Override public void onResume() { super.onResume(); if (resumed) { resumed = false; stopBookShelfRefreshAnim(); } } @Override public void onPause() { resumed = true; super.onPause(); } private void stopBookShelfRefreshAnim() { if (bookShelfAdapter.getBooks() != null && bookShelfAdapter.getBooks().size() > 0) { for (BookShelfBean bookShelfBean : bookShelfAdapter.getBooks()) { if (bookShelfBean.isLoading()) { bookShelfBean.setLoading(false); refreshBook(bookShelfBean.getNoteUrl()); } } } } @Override public void refreshBookShelf(List bookShelfBeanList) { bookShelfAdapter.replaceAll(bookShelfBeanList, bookPx); if (bookShelfBeanList.size() > 0) { binding.viewEmpty.rlEmptyView.setVisibility(View.GONE); } else { binding.viewEmpty.tvEmpty.setText(R.string.bookshelf_empty); binding.viewEmpty.rlEmptyView.setVisibility(View.VISIBLE); } } @Override public void refreshBook(String noteUrl) { bookShelfAdapter.refreshBook(noteUrl); } @Override public void updateGroup(Integer group) { this.group = group; } @Override public SharedPreferences getPreferences() { return preferences; } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } public void setArrange(boolean isArrange) { if (bookShelfAdapter != null) { bookShelfAdapter.setArrange(isArrange); if (isArrange) { binding.actionBar.setVisibility(View.VISIBLE); upSelectCount(); } else { binding.actionBar.setVisibility(View.GONE); } } } @SuppressLint("DefaultLocale") private void upSelectCount() { binding.tvSelectCount.setText(String.format("%d/%d", bookShelfAdapter.getSelected().size(), bookShelfAdapter.getBooks().size())); } private void delSelect() { Single.create((SingleOnSubscribe) emitter -> { for (String noteUrl : bookShelfAdapter.getSelected()) { BookshelfHelp.removeFromBookShelf(BookshelfHelp.getBook(noteUrl)); } bookShelfAdapter.getSelected().clear(); emitter.onSuccess(true); }).compose(RxUtils::toSimpleSingle) .subscribe(new MySingleObserver() { @Override public void onSuccess(Boolean aBoolean) { mPresenter.queryBookShelf(false, group); } }); } public interface CallbackValue { boolean isRecreate(); int getGroup(); ViewPager getViewPager(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/BookmarkFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import com.hwangjr.rxbus.RxBus; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.base.MBaseFragment; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookmarkBean; import com.kunfei.bookshelf.bean.OpenChapterBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.FragmentBookmarkListBinding; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.view.activity.ChapterListActivity; import com.kunfei.bookshelf.view.adapter.BookmarkAdapter; import com.kunfei.bookshelf.widget.modialog.BookmarkDialog; import java.util.List; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; public class BookmarkFragment extends MBaseFragment { private FragmentBookmarkListBinding binding; private BookShelfBean bookShelf; private List bookmarkBeanList; private BookmarkAdapter adapter; @Override protected View createView(LayoutInflater inflater, ViewGroup container) { binding = FragmentBookmarkListBinding.inflate(inflater, container, false); return binding.getRoot(); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); RxBus.get().register(this); } /** * 数据初始化 */ @Override protected void initData() { super.initData(); if (getFatherActivity() != null) { bookShelf = getFatherActivity().getBookShelf(); } } /** * 控件绑定 */ @Override protected void bindView() { super.bindView(); adapter = new BookmarkAdapter(bookShelf, new BookmarkAdapter.OnItemClickListener() { @Override public void itemClick(int index, int page) { if (index != bookShelf.getDurChapter()) { RxBus.get().post(RxBusTag.SKIP_TO_CHAPTER, new OpenChapterBean(index, page)); } if (getFatherActivity() != null) { getFatherActivity().searchViewCollapsed(); getFatherActivity().finish(); } } @Override public void itemLongClick(BookmarkBean bookmarkBean) { if (getFatherActivity() != null) { getFatherActivity().searchViewCollapsed(); } showBookmark(bookmarkBean); } }); binding.rvList.setLayoutManager(new LinearLayoutManager(getActivity())); binding.rvList.setAdapter(adapter); } @Override protected void firstRequest() { super.firstRequest(); Single.create((SingleOnSubscribe) emitter -> { if (bookShelf != null) { bookmarkBeanList = BookshelfHelp.getBookmarkList(bookShelf.getBookInfoBean().getName()); emitter.onSuccess(true); } else { emitter.onSuccess(false); } }).compose(RxUtils::toSimpleSingle) .subscribe(new MySingleObserver() { @Override public void onSuccess(Boolean aBoolean) { if (aBoolean) { adapter.setAllBookmark(bookmarkBeanList); } } }); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; RxBus.get().unregister(this); } public void startSearch(String key) { adapter.search(key); } private void showBookmark(BookmarkBean bookmarkBean) { BookmarkDialog.builder(getContext(), bookmarkBean, false) .setPositiveButton(new BookmarkDialog.Callback() { @Override public void saveBookmark(BookmarkBean bookmarkBean) { DbHelper.getDaoSession().getBookmarkBeanDao().insertOrReplace(bookmarkBean); adapter.notifyDataSetChanged(); } @Override public void delBookmark(BookmarkBean bookmarkBean) { // Log.d("delBookmark","before="+bookmarkBeanList.size()); DbHelper.getDaoSession().getBookmarkBeanDao().delete(bookmarkBean); // Log.d("delBookmark","after="+bookmarkBeanList.size()); bookmarkBeanList = BookshelfHelp.getBookmarkList(bookShelf.getBookInfoBean().getName()); // Log.d("delBookmark","fine="+bookmarkBeanList.size()); adapter.setAllBookmark(bookmarkBeanList); // adapter.notifyDataSetChanged(); } @Override public void openChapter(int chapterIndex, int pageIndex) { RxBus.get().post(RxBusTag.OPEN_BOOK_MARK, bookmarkBean); if (getFatherActivity() != null) { getFatherActivity().finish(); } } }).show(); } private ChapterListActivity getFatherActivity() { return (ChapterListActivity) getActivity(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/ChapterListFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.base.MBaseFragment; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.OpenChapterBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.databinding.FragmentChapterListBinding; import com.kunfei.bookshelf.view.activity.ChapterListActivity; import com.kunfei.bookshelf.view.adapter.ChapterListAdapter; import java.util.List; import java.util.Locale; public class ChapterListFragment extends MBaseFragment { private FragmentChapterListBinding binding; private ChapterListAdapter chapterListAdapter; private LinearLayoutManager layoutManager; private BookShelfBean bookShelf; private List chapterBeanList; private boolean isChapterReverse; @Override protected View createView(LayoutInflater inflater, ViewGroup container) { binding = FragmentChapterListBinding.inflate(inflater, container, false); return binding.getRoot(); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); RxBus.get().register(this); } /** * 数据初始化 */ @Override protected void initData() { super.initData(); if (getFatherActivity() != null) { bookShelf = getFatherActivity().getBookShelf(); chapterBeanList = getFatherActivity().getChapterBeanList(); } isChapterReverse = preferences.getBoolean("isChapterReverse", false); } /** * 控件绑定 */ @Override protected void bindView() { super.bindView(); binding.rvList.setLayoutManager(layoutManager = new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, isChapterReverse)); binding.rvList.setItemAnimator(null); chapterListAdapter = new ChapterListAdapter(bookShelf, chapterBeanList, (index, page) -> { if (index != bookShelf.getDurChapter()) { RxBus.get().post(RxBusTag.SKIP_TO_CHAPTER, new OpenChapterBean(index, page)); } if (getFatherActivity() != null) { getFatherActivity().searchViewCollapsed(); getFatherActivity().finish(); } }); if (bookShelf != null) { binding.rvList.setAdapter(chapterListAdapter); updateIndex(bookShelf.getDurChapter()); updateChapterInfo(); } } /** * 事件触发绑定 */ @Override protected void bindEvent() { super.bindEvent(); binding.tvCurrentChapterInfo.setOnClickListener(view -> layoutManager.scrollToPositionWithOffset(bookShelf.getDurChapter(), 0)); binding.ivChapterTop.setOnClickListener(v -> binding.rvList.scrollToPosition(0)); binding.ivChapterBottom.setOnClickListener(v -> { if (chapterListAdapter.getItemCount() > 0) { binding.rvList.scrollToPosition(chapterListAdapter.getItemCount() - 1); } }); } public void startSearch(String key) { chapterListAdapter.search(key); } private void updateIndex(int durChapter) { chapterListAdapter.setIndex(durChapter); layoutManager.scrollToPositionWithOffset(durChapter, 0); } private void updateChapterInfo() { if (bookShelf != null) { if (chapterListAdapter.getItemCount() == 0) { binding.tvCurrentChapterInfo.setText(bookShelf.getDurChapterName()); } else { binding.tvCurrentChapterInfo.setText(String.format(Locale.getDefault(), "%s (%d/%d章)", bookShelf.getDurChapterName(), bookShelf.getDurChapter() + 1, bookShelf.getChapterListSize())); } } } private ChapterListActivity getFatherActivity() { return (ChapterListActivity) getActivity(); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; RxBus.get().unregister(this); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.CHAPTER_CHANGE)}) public void chapterChange(BookContentBean bookContentBean) { if (bookShelf != null && bookShelf.getNoteUrl().equals(bookContentBean.getNoteUrl())) { chapterListAdapter.upChapter(bookContentBean.getDurChapterIndex()); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/FileCategoryFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import android.graphics.PorterDuff; import android.os.Environment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.LinearLayoutManager; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.databinding.FragmentFileCategoryBinding; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.FileHelp; import com.kunfei.bookshelf.utils.FileStack; import com.kunfei.bookshelf.utils.FileUtils; import com.kunfei.bookshelf.view.adapter.FileSystemAdapter; import com.kunfei.bookshelf.widget.itemdecoration.DividerItemDecoration; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; public class FileCategoryFragment extends BaseFileFragment { private static final String TAG = "FileCategoryFragment"; private FragmentFileCategoryBinding binding; private FileStack mFileStack; private String rootFilePath; @Override protected View createView(LayoutInflater inflater, ViewGroup container) { binding = FragmentFileCategoryBinding.inflate(inflater, container, false); return binding.getRoot(); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } @Override protected void bindView() { super.bindView(); mFileStack = new FileStack(); setUpAdapter(); } private void setUpAdapter() { mAdapter = new FileSystemAdapter(); binding.fileCategoryRvContent.setLayoutManager(new LinearLayoutManager(getContext())); binding.fileCategoryRvContent.addItemDecoration(new DividerItemDecoration(getContext())); binding.fileCategoryRvContent.setAdapter(mAdapter); setTextViewIconColor(binding.fileCategoryTvBackLast); } @Override protected void bindEvent() { super.bindEvent(); mAdapter.setOnItemClickListener( (view, pos) -> { File file = mAdapter.getItem(pos); if (file.isDirectory()) { //保存当前信息。 FileStack.FileSnapshot snapshot = new FileStack.FileSnapshot(); snapshot.filePath = binding.fileCategoryTvPath.getText().toString(); snapshot.files = new ArrayList<>(mAdapter.getItems()); snapshot.scrollOffset = binding.fileCategoryRvContent.computeVerticalScrollOffset(); mFileStack.push(snapshot); //切换下一个文件 toggleFileTree(file); } else { //如果是已加载的文件,则点击事件无效。 String id = mAdapter.getItem(pos).getAbsolutePath(); if (BookshelfHelp.getBook(id) != null) { return; } //点击选中 mAdapter.setCheckedItem(pos); //反馈 if (mListener != null) { mListener.onItemCheckedChange(mAdapter.getItemIsChecked(pos)); } } } ); binding.fileCategoryTvBackLast.setOnClickListener(v -> { FileStack.FileSnapshot snapshot = mFileStack.pop(); int oldScrollOffset = binding.fileCategoryRvContent.computeHorizontalScrollOffset(); if (snapshot == null) return; binding.fileCategoryTvPath.setText(snapshot.filePath); mAdapter.refreshItems(snapshot.files); binding.fileCategoryRvContent.scrollBy(0, snapshot.scrollOffset - oldScrollOffset); //反馈 if (mListener != null) { mListener.onCategoryChanged(); } } ); binding.tvSd.setOnClickListener(v -> { if (getContext() != null) { List list = FileUtils.getStorageData(getContext()); if (list != null) { String[] filePathS = list.toArray(new String[0]); AlertDialog dialog = new AlertDialog.Builder(getContext()) .setTitle(R.string.select_sd_file) .setSingleChoiceItems(filePathS, 0, (dialogInterface, i) -> { upRootFile(filePathS[i]); dialogInterface.dismiss(); }) .create(); dialog.show(); } } }); } @Override protected void firstRequest() { super.firstRequest(); upRootFile(Environment.getExternalStorageDirectory().getPath()); } private void upRootFile(String rootFilePath) { this.rootFilePath = rootFilePath; toggleFileTree(new File(rootFilePath)); } private void setTextViewIconColor(TextView textView) { // textView.getCompoundDrawables()[0].mutate(); try { textView.getCompoundDrawables()[0].setColorFilter(getResources().getColor(R.color.tv_text_default), PorterDuff.Mode.SRC_ATOP); } catch (Exception ignored) { } } private void toggleFileTree(File file) { //路径名 binding.fileCategoryTvPath.setText(file.getPath().replace(rootFilePath, "")); //获取数据 File[] files = file.listFiles(new SimpleFileFilter()); //转换成List List rootFiles = Arrays.asList(files); //排序 Collections.sort(rootFiles, new FileComparator()); //加入 mAdapter.refreshItems(rootFiles); //反馈 if (mListener != null) { mListener.onCategoryChanged(); } } @Override public int getFileCount() { int count = 0; Set> entrys = mAdapter.getCheckMap().entrySet(); for (Map.Entry entry : entrys) { if (!entry.getKey().isDirectory()) { ++count; } } return count; } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } public static class FileComparator implements Comparator { @Override public int compare(File o1, File o2) { if (o1.isDirectory() && o2.isFile()) { return -1; } if (o2.isDirectory() && o1.isFile()) { return 1; } return o1.getName().compareToIgnoreCase(o2.getName()); } } public static class SimpleFileFilter implements FileFilter { @Override public boolean accept(File pathname) { if (pathname.getName().startsWith(".")) { return false; } //文件夹内部数量为0 if (pathname.isDirectory() && (pathname.list() == null || pathname.list().length == 0)) { return false; } //文件内容为空,或者不以txt为开头 return pathname.isDirectory() || (pathname.length() != 0 && (pathname.getName().toLowerCase().endsWith(FileHelp.SUFFIX_TXT) || pathname.getName().toLowerCase().endsWith(FileHelp.SUFFIX_EPUB))); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/FindBookFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import static android.app.Activity.RESULT_OK; import android.content.Context; import android.content.Intent; import android.graphics.PointF; import android.os.Bundle; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.PopupMenu; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.MBaseFragment; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.FindKindBean; import com.kunfei.bookshelf.bean.FindKindGroupBean; import com.kunfei.bookshelf.databinding.FragmentBookFindBinding; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.presenter.FindBookPresenter; import com.kunfei.bookshelf.presenter.contract.FindBookContract; import com.kunfei.bookshelf.utils.ACache; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.view.activity.ChoiceBookActivity; import com.kunfei.bookshelf.view.activity.SourceEditActivity; import com.kunfei.bookshelf.view.adapter.FindKindAdapter; import com.kunfei.bookshelf.view.adapter.FindLeftAdapter; import com.kunfei.bookshelf.view.adapter.FindRightAdapter; import com.kunfei.bookshelf.widget.recycler.expandable.OnRecyclerViewListener; import com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData; import com.kunfei.bookshelf.widget.recycler.sectioned.GridSpacingItemDecoration; import com.kunfei.bookshelf.widget.recycler.sectioned.SectionedSpanSizeLookup; import java.util.ArrayList; import java.util.List; public class FindBookFragment extends MBaseFragment implements FindBookContract.View, OnRecyclerViewListener.OnItemClickListener, OnRecyclerViewListener.OnItemLongClickListener { private FragmentBookFindBinding binding; private FindLeftAdapter findLeftAdapter; private FindRightAdapter findRightAdapter; private FindKindAdapter findKindAdapter; private LinearLayoutManager leftLayoutManager; private RecyclerView.LayoutManager rightLayoutManager; private List data = new ArrayList<>(); @Override protected View createView(LayoutInflater inflater, ViewGroup container) { binding = FragmentBookFindBinding.inflate(inflater, container, false); return binding.getRoot(); } @Override protected FindBookContract.Presenter initInjector() { return new FindBookPresenter(); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return super.onCreateView(inflater, container, savedInstanceState); } @Override protected void bindView() { super.bindView(); binding.rvFindRight.addItemDecoration(new GridSpacingItemDecoration(10)); binding.refreshLayout.setColorSchemeColors(ThemeStore.accentColor(MApplication.getInstance())); binding.refreshLayout.setOnRefreshListener(() -> { refreshData(); binding.refreshLayout.setRefreshing(false); }); leftLayoutManager = new LinearLayoutManager(getContext()); initRecyclerView(); } /** * 首次逻辑操作 */ @Override protected void firstRequest() { super.firstRequest(); refreshData(); } public void refreshData() { if (mPresenter != null) { mPresenter.initData(); } } @Override public void upData(List group) { this.data = group; upStyle(); upUI(); } public void upStyle() { if (binding.emptyView.rlEmptyView == null) return; initRecyclerView(); if (isFlexBox()) { findRightAdapter.setData(data); findLeftAdapter.setData(data); } else { findKindAdapter.setAllDatas(data); } upUI(); } public void upUI() { if (binding.emptyView.rlEmptyView == null) return; if (data.size() == 0) { binding.emptyView.tvEmpty.setText(R.string.no_find); binding.emptyView.rlEmptyView.setVisibility(View.VISIBLE); } else { binding.emptyView.rlEmptyView.setVisibility(View.GONE); } if (isFlexBox()) { binding.emptyView.rlEmptyView.setVisibility(View.GONE); if (data.size() <= 1 | !showLeftView()) { binding.rvFindLeft.setVisibility(View.GONE); binding.vwDivider.setVisibility(View.GONE); } else { binding.rvFindLeft.setVisibility(View.VISIBLE); binding.vwDivider.setVisibility(View.VISIBLE); } } } private boolean isFlexBox() { return preferences.getBoolean("findTypeIsFlexBox", true); } private boolean showLeftView() { return preferences.getBoolean("showFindLeftView", true); } private void initRecyclerView() { if (binding.rvFindRight == null) return; if (isFlexBox()) { findKindAdapter = null; findLeftAdapter = new FindLeftAdapter(getActivity(), pos -> { int counts = 0; for (int i = 0; i < pos; i++) { //position 为点击的position counts += findRightAdapter.getData().get(i).getChildList().size(); } ((ScrollLinearLayoutManger) rightLayoutManager).scrollToPositionWithOffset(counts + pos, 0); }); binding.rvFindLeft.setLayoutManager(leftLayoutManager); binding.rvFindLeft.setAdapter(findLeftAdapter); findRightAdapter = new FindRightAdapter(requireActivity(), this); //设置header rightLayoutManager = new ScrollLinearLayoutManger(getActivity(), 3); ((ScrollLinearLayoutManger) rightLayoutManager).setSpanSizeLookup(new SectionedSpanSizeLookup(findRightAdapter, (ScrollLinearLayoutManger) rightLayoutManager)); binding.rvFindRight.setLayoutManager(rightLayoutManager); binding.rvFindRight.setLayoutManager(rightLayoutManager); binding.rvFindRight.setItemViewCacheSize(10); binding.rvFindRight.setItemAnimator(null); binding.rvFindRight.setHasFixedSize(true); binding.rvFindRight.setAdapter(findRightAdapter); } else { rightLayoutManager = new LinearLayoutManager(getContext()); binding.rvFindLeft.setVisibility(View.GONE); binding.vwDivider.setVisibility(View.GONE); findLeftAdapter = null; findRightAdapter = null; findKindAdapter = new FindKindAdapter(getContext(), new ArrayList<>()); findKindAdapter.setOnItemClickListener(this); findKindAdapter.setOnItemLongClickListener(this); findKindAdapter.setCanExpandAll(false); binding.rvFindRight.setLayoutManager(rightLayoutManager); binding.rvFindRight.setAdapter(findKindAdapter); } } @Override public void onGroupItemClick(int position, int groupPosition, View view) { } @Override public void onChildItemClick(int position, int groupPosition, int childPosition, View view) { FindKindBean kindBean = (FindKindBean) findKindAdapter.getAllDatas().get(groupPosition).getChild(childPosition); Intent intent = new Intent(getContext(), ChoiceBookActivity.class); intent.putExtra("url", kindBean.getKindUrl()); intent.putExtra("title", kindBean.getKindName()); intent.putExtra("tag", kindBean.getTag()); startActivityByAnim(intent, view, "sharedView", android.R.anim.fade_in, android.R.anim.fade_out); } @Override public void onGroupItemLongClick(int position, int groupPosition, View view) { if (getActivity() == null) return; FindKindGroupBean groupBean; if (isFlexBox()) { groupBean = (FindKindGroupBean) findRightAdapter.getData().get(groupPosition).getGroupData(); } else { groupBean = (FindKindGroupBean) findKindAdapter.getAllDatas().get(groupPosition).getGroupData(); } BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(groupBean.getGroupTag()); if (sourceBean == null) { return; } PopupMenu popupMenu = new PopupMenu(getContext(), view); popupMenu.getMenu().add(Menu.NONE, R.id.menu_edit, Menu.NONE, R.string.edit); popupMenu.getMenu().add(Menu.NONE, R.id.menu_top, Menu.NONE, R.string.to_top); popupMenu.getMenu().add(Menu.NONE, R.id.menu_del, Menu.NONE, R.string.delete); popupMenu.getMenu().add(Menu.NONE, R.id.menu_clear, Menu.NONE, R.string.clear_cache); popupMenu.setOnMenuItemClickListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.menu_edit) { SourceEditActivity.startThis(this, sourceBean); } else if (itemId == R.id.menu_top) { BookSourceManager.toTop(sourceBean) .subscribe(new MySingleObserver() { @Override public void onSuccess(Boolean aBoolean) { refreshData(); } }); } else if (itemId == R.id.menu_del) { BookSourceManager.removeBookSource(sourceBean); refreshData(); } else if (itemId == R.id.menu_clear) { ACache.get(getActivity(), "findCache").remove(sourceBean.getBookSourceUrl()); } return true; }); popupMenu.show(); } @Override public void onChildItemLongClick(int position, int groupPosition, int childPosition, View view) { } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == SourceEditActivity.EDIT_SOURCE) { refreshData(); } } } @SuppressWarnings("unused") public static class ScrollLinearLayoutManger extends GridLayoutManager { public ScrollLinearLayoutManger(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public ScrollLinearLayoutManger(Context context, int spanCount) { super(context, spanCount); } public ScrollLinearLayoutManger(Context context, int spanCount, int orientation, boolean reverseLayout) { super(context, spanCount, orientation, reverseLayout); } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { RecyclerView.SmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext()); smoothScroller.setTargetPosition(position); startSmoothScroll(smoothScroller); } private class CenterSmoothScroller extends LinearSmoothScroller { CenterSmoothScroller(Context context) { super(context); } @Nullable @Override public PointF computeScrollVectorForPosition(int targetPosition) { return ScrollLinearLayoutManger.this.computeScrollVectorForPosition(targetPosition); } @Override public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) { return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2); } protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return 0.2f; } @Override protected int getVerticalSnapPreference() { return SNAP_TO_START; } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/LocalBookFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.recyclerview.widget.LinearLayoutManager; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.bookshelf.databinding.FragmentLocalBookBinding; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.media.MediaStoreHelper; import com.kunfei.bookshelf.view.adapter.FileSystemAdapter; import com.kunfei.bookshelf.widget.itemdecoration.DividerItemDecoration; /** * Created by newbiechen on 17-5-27. * 本地书籍 */ public class LocalBookFragment extends BaseFileFragment { private FragmentLocalBookBinding binding; @Override protected View createView(LayoutInflater inflater, ViewGroup container) { binding = FragmentLocalBookBinding.inflate(inflater, container, false); return binding.getRoot(); } /** * P层绑定 若无则返回null; */ @Override protected IPresenter initInjector() { return null; } @Override protected void bindView() { super.bindView(); setUpAdapter(); } private void setUpAdapter() { mAdapter = new FileSystemAdapter(); if (getContext() != null) { binding.localBookRvContent.setLayoutManager(new LinearLayoutManager(getContext())); binding.localBookRvContent.addItemDecoration(new DividerItemDecoration(getContext())); binding.localBookRvContent.setAdapter(mAdapter); } } @Override protected void bindEvent() { super.bindEvent(); mAdapter.setOnItemClickListener( (view, pos) -> { //如果是已加载的文件,则点击事件无效。 String id = mAdapter.getItem(pos).getAbsolutePath(); if (BookshelfHelp.getBook(id) != null) { return; } //点击选中 mAdapter.setCheckedItem(pos); //反馈 if (mListener != null) { mListener.onItemCheckedChange(mAdapter.getItemIsChecked(pos)); } } ); } @Override protected void firstRequest() { super.firstRequest(); if (getActivity() != null) { MediaStoreHelper.getAllBookFile(getActivity(), (files) -> { if (files.isEmpty()) { binding.refreshLayout.showEmpty(); } else { mAdapter.refreshItems(files); binding.refreshLayout.showFinish(); //反馈 if (mListener != null) { mListener.onCategoryChanged(); } } }); } } @Override public void onDestroy() { super.onDestroy(); binding = null; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/SettingsFragment.kt ================================================ @file:Suppress("DEPRECATION") package com.kunfei.bookshelf.view.fragment import android.content.Intent import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.os.Bundle import android.preference.ListPreference import android.preference.Preference import android.preference.PreferenceFragment import android.preference.PreferenceScreen import com.hwangjr.rxbus.RxBus import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.R import com.kunfei.bookshelf.constant.RxBusTag import com.kunfei.bookshelf.help.BookshelfHelp import com.kunfei.bookshelf.help.FileHelp import com.kunfei.bookshelf.help.ProcessTextHelp import com.kunfei.bookshelf.help.permission.Permissions import com.kunfei.bookshelf.help.permission.PermissionsCompat import com.kunfei.bookshelf.help.storage.BackupRestoreUi.selectBackupFolder import com.kunfei.bookshelf.service.WebService import com.kunfei.bookshelf.utils.FileUtils import com.kunfei.bookshelf.view.activity.SettingActivity import com.kunfei.bookshelf.widget.filepicker.picker.FilePicker import org.jetbrains.anko.alert import org.jetbrains.anko.noButton import org.jetbrains.anko.okButton /** * Created by GKF on 2017/12/16. * 设置 */ @Suppress("DEPRECATION") class SettingsFragment : PreferenceFragment(), OnSharedPreferenceChangeListener { private var settingActivity: SettingActivity? = null private lateinit var bookshelfPxKey: String private lateinit var downloadPathKey: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) preferenceManager.sharedPreferencesName = "CONFIG" settingActivity = activity as? SettingActivity settingActivity?.setupActionBar(getString(R.string.setting)) addPreferencesFromResource(R.xml.pref_settings) val sharedPreferences = preferenceManager.sharedPreferences val editor = sharedPreferences.edit() val processTextEnabled = ProcessTextHelp.isProcessTextEnabled() editor.putBoolean("process_text", processTextEnabled) if (sharedPreferences.getString(getString(R.string.pk_download_path), "") == "") { editor.putString(getString(R.string.pk_download_path), FileHelp.getCachePath()) } editor.apply() bookshelfPxKey = getString(R.string.pk_bookshelf_px) downloadPathKey = getString(R.string.pk_download_path) upPreferenceSummary(bookshelfPxKey, sharedPreferences.getString(bookshelfPxKey, "0")) upPreferenceSummary(downloadPathKey, MApplication.downloadPath) upPreferenceSummary("backupPath", sharedPreferences.getString("backupPath", null)) preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) } override fun onDestroy() { super.onDestroy() preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { when (key) { bookshelfPxKey -> { upPreferenceSummary(key, sharedPreferences.getString(key, "0")) RxBus.get().post(RxBusTag.RECREATE, true) } "behaviorMain" -> RxBus.get().post(RxBusTag.RECREATE, true) "process_text" -> ProcessTextHelp.setProcessTextEnable(sharedPreferences.getBoolean("process_text", true)) "webPort" -> WebService.upHttpServer(activity) "backupPath" -> upPreferenceSummary(key, sharedPreferences.getString(key, null)) downloadPathKey -> upPreferenceSummary(downloadPathKey, MApplication.downloadPath) } if (key == bookshelfPxKey || key == "behaviorMain") { RxBus.get().post(RxBusTag.RECREATE, true) } else if (key == "process_text") { ProcessTextHelp.setProcessTextEnable(sharedPreferences.getBoolean("process_text", true)) } else if (key == "webPort") { WebService.upHttpServer(activity) } } private fun upPreferenceSummary(preferenceKey: String, value: String?) { val preference = findPreference(preferenceKey) ?: return if (preference is ListPreference) { val index = preference.findIndexOfValue(value) // Set the summary to reflect the new value. preference.summary = if (index >= 0) preference.entries[index] else null } else { preference.summary = value } } override fun onPreferenceTreeClick(preferenceScreen: PreferenceScreen, preference: Preference): Boolean { when (preference.key) { getString(R.string.pk_download_path) -> { selectDownloadPath(preference) } "backupPath" -> { selectBackupFolder(activity) } "webDavSetting" -> { val webDavSettingsFragment = WebDavSettingsFragment() fragmentManager.beginTransaction().replace(R.id.settingsFrameLayout, webDavSettingsFragment, "webDavSettings").commit() } "clearCache" -> { alert { titleResource = R.string.clear_cache messageResource = R.string.sure_del_download_book okButton { BookshelfHelp.clearCaches(true) } noButton { BookshelfHelp.clearCaches(false) } }.show() } } return super.onPreferenceTreeClick(preferenceScreen, preference) } private fun selectDownloadPath(preference: Preference) { PermissionsCompat.Builder(activity) .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE) .rationale(R.string.set_download_per) .onGranted { val picker = FilePicker(activity, FilePicker.DIRECTORY) picker.setBackgroundColor(resources.getColor(R.color.background)) picker.setTopBackgroundColor(resources.getColor(R.color.background)) picker.setRootPath(preference.summary.toString()) picker.setItemHeight(30) picker.setOnFilePickListener { currentPath: String -> if (!currentPath.contains(FileUtils.getSdCardPath())) { MApplication.getInstance().setDownloadPath(null) } else { MApplication.getInstance().setDownloadPath(currentPath) } preference.summary = MApplication.downloadPath } picker.show() picker.cancelButton.setText(R.string.restore_default) picker.cancelButton.setOnClickListener { picker.dismiss() MApplication.getInstance().setDownloadPath(null) preference.summary = MApplication.downloadPath } } .request() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { super.onActivityResult(requestCode, resultCode, data) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/ThemeSettingsFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import androidx.appcompat.app.AlertDialog; import com.hwangjr.rxbus.RxBus; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.help.LauncherIcon; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.view.activity.ThemeSettingActivity; import java.util.Objects; /** * Created by GKF on 2017/12/16. * 设置 */ public class ThemeSettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private ThemeSettingActivity settingActivity; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getPreferenceManager().setSharedPreferencesName("CONFIG"); addPreferencesFromResource(R.xml.pref_settings_theme); settingActivity = (ThemeSettingActivity) this.getActivity(); SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); sharedPreferences.edit().putString("launcher_icon", LauncherIcon.getInUseIcon()).apply(); } private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (Preference preference, Object value) -> { String stringValue = value.toString(); if (preference instanceof ListPreference) { ListPreference listPreference = (ListPreference) preference; int index = listPreference.findIndexOfValue(stringValue); // Set the summary to reflect the new value. preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null); } else { // For all other preferences, set the summary to the value's preference.setSummary(stringValue); } return true; }; private static void bindPreferenceSummaryToValue(Preference preference) { // Set the listener to watch for value changes. preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, preference.getContext().getSharedPreferences("CONFIG", Context.MODE_PRIVATE).getString(preference.getKey(), "")); } @Override public void onResume() { super.onResume(); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { AlertDialog alertDialog; switch (key) { case "launcher_icon": LauncherIcon.ChangeIcon(sharedPreferences.getString("launcher_icon", getString(R.string.icon_main))); break; case "behaviorMain": RxBus.get().post(RxBusTag.RECREATE, true); break; case "E-InkMode": MApplication.getInstance().upEInkMode(); break; case "immersionStatusBar": case "navigationBarColorChange": settingActivity.initImmersionBar(); RxBus.get().post(RxBusTag.IMMERSION_CHANGE, true); break; case "colorPrimary": case "colorAccent": case "colorBackground": if (!ColorUtils.isColorLight(sharedPreferences.getInt("colorBackground", settingActivity.getResources().getColor(R.color.md_grey_100)))) { alertDialog = new AlertDialog.Builder(getActivity()) .setTitle("白天背景太暗") .setMessage("将会恢复默认背景?") .setPositiveButton(R.string.ok, (dialog, which) -> { settingActivity.preferences.edit().putInt("colorBackground", settingActivity.getResources().getColor(R.color.md_grey_100)).apply(); upTheme(false); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> upTheme(false)) .show(); ATH.setAlertDialogTint(alertDialog); } else { upTheme(false); } break; case "colorPrimaryNight": case "colorAccentNight": case "colorBackgroundNight": if (ColorUtils.isColorLight(sharedPreferences.getInt("colorBackgroundNight", settingActivity.getResources().getColor(R.color.md_grey_800)))) { alertDialog = new AlertDialog.Builder(getActivity()) .setTitle("夜间背景太亮") .setMessage("将会恢复默认背景?") .setPositiveButton(R.string.ok, (dialog, which) -> { settingActivity.preferences.edit().putInt("colorBackgroundNight", settingActivity.getResources().getColor(R.color.md_grey_800)).apply(); upTheme(true); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> upTheme(true)) .show(); ATH.setAlertDialogTint(alertDialog); } else { upTheme(true); } break; } } private void upTheme(boolean isNightTheme) { if (settingActivity.isNightTheme() == isNightTheme) { MApplication.getInstance().upThemeStore(); RxBus.get().post(RxBusTag.RECREATE, true); new Handler(Looper.getMainLooper()).postDelayed(() -> { if (getActivity() != null) { getActivity().recreate(); } }, 300); } } @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { if (Objects.equals(preference.getKey(), "defaultTheme")) { AlertDialog alertDialog = new AlertDialog.Builder(getActivity()) .setTitle("恢复默认主题") .setMessage("是否确认恢复?") .setPositiveButton(R.string.ok, (dialog, which) -> { settingActivity.preferences.edit() .putInt("colorPrimary", settingActivity.getResources().getColor(R.color.md_grey_100)) .putInt("colorAccent", settingActivity.getResources().getColor(R.color.md_pink_600)) .putInt("colorBackground", settingActivity.getResources().getColor(R.color.md_grey_100)) .putInt("colorPrimaryNight", settingActivity.getResources().getColor(R.color.md_grey_800)) .putInt("colorAccentNight", settingActivity.getResources().getColor(R.color.md_pink_800)) .putInt("colorBackgroundNight", settingActivity.getResources().getColor(R.color.md_grey_800)) .apply(); MApplication.getInstance().upThemeStore(); RxBus.get().post(RxBusTag.RECREATE, true); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { }) .show(); ATH.setAlertDialogTint(alertDialog); } return super.onPreferenceTreeClick(preferenceScreen, preference); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/fragment/WebDavSettingsFragment.java ================================================ package com.kunfei.bookshelf.view.fragment; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import android.text.TextUtils; import android.widget.Toast; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.observer.MySingleObserver; import com.kunfei.bookshelf.help.FileHelp; import com.kunfei.bookshelf.help.ProcessTextHelp; import com.kunfei.bookshelf.help.storage.BackupRestoreUi; import com.kunfei.bookshelf.help.storage.WebDavHelp; import com.kunfei.bookshelf.view.activity.SettingActivity; import java.util.ArrayList; import java.util.Objects; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; import static com.kunfei.bookshelf.constant.AppConstant.DEFAULT_WEB_DAV_URL; /** * Created by GKF on 2017/12/16. * 设置 */ public class WebDavSettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private SettingActivity settingActivity; private CompositeDisposable compositeDisposable = new CompositeDisposable(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getPreferenceManager().setSharedPreferencesName("CONFIG"); settingActivity = (SettingActivity) this.getActivity(); settingActivity.setupActionBar("WebDav设置"); SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); SharedPreferences.Editor editor = sharedPreferences.edit(); boolean processTextEnabled = ProcessTextHelp.isProcessTextEnabled(); editor.putBoolean("process_text", processTextEnabled); if (Objects.equals(sharedPreferences.getString(getString(R.string.pk_download_path), ""), "")) { editor.putString(getString(R.string.pk_download_path), FileHelp.getCachePath()); } editor.apply(); addPreferencesFromResource(R.xml.pref_settings_web_dav); bindPreferenceSummaryToValue(findPreference("web_dav_url")); bindPreferenceSummaryToValue(findPreference("web_dav_account")); bindPreferenceSummaryToValue(findPreference("web_dav_password")); } private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (Preference preference, Object value) -> { String stringValue = value.toString(); if (preference.getKey().equals("web_dav_url")) { if (TextUtils.isEmpty(stringValue)) { preference.setSummary(DEFAULT_WEB_DAV_URL); } else { preference.setSummary(stringValue); } } else if (preference.getKey().equals("web_dav_account")) { if (TextUtils.isEmpty(stringValue)) { preference.setSummary("输入你的WebDav账号"); } else { preference.setSummary(stringValue); } } else if (preference.getKey().equals("web_dav_password")) { if (TextUtils.isEmpty(stringValue)) { preference.setSummary("输入你的WebDav授权密码"); } else { preference.setSummary("************"); } } else if (preference instanceof ListPreference) { ListPreference listPreference = (ListPreference) preference; int index = listPreference.findIndexOfValue(stringValue); // Set the summary to reflect the new value. preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null); } else { // For all other preferences, set the summary to the value's preference.setSummary(stringValue); } return true; }; private static void bindPreferenceSummaryToValue(Preference preference) { // Set the listener to watch for value changes. preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, preference.getContext().getSharedPreferences("CONFIG", Context.MODE_PRIVATE).getString(preference.getKey(), "")); } @Override public void onResume() { super.onResume(); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { } @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { if (preference.getKey().equals("web_dav_restore")) { restore(); } return super.onPreferenceTreeClick(preferenceScreen, preference); } private void restore() { 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(getActivity(), strings, BackupRestoreUi.INSTANCE)) { Toast.makeText(getActivity(), "没有备份", Toast.LENGTH_SHORT).show(); } } }); } @Override public void onDestroy() { compositeDisposable.dispose(); super.onDestroy(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/CheckAddShelfPop.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.popupwindow; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.PopupWindow; import android.widget.TextView; import androidx.annotation.NonNull; import com.kunfei.bookshelf.R; public class CheckAddShelfPop extends PopupWindow { private Context mContext; private View view; private OnItemClickListener itemClick; private String bookName; @SuppressLint("InflateParams") public CheckAddShelfPop(Context context, @NonNull String bookName, @NonNull OnItemClickListener itemClick) { super(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); mContext = context; this.bookName = bookName; this.itemClick = itemClick; view = LayoutInflater.from(mContext).inflate(R.layout.mo_dialog_two, null); this.setContentView(view); initView(); setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.shape_pop_checkaddshelf_bg)); setFocusable(true); setTouchable(true); } private void initView() { TextView tvBookName = view.findViewById(R.id.tv_msg); tvBookName.setText(mContext.getString(R.string.check_add_bookshelf, bookName)); TextView tvExit = view.findViewById(R.id.tv_cancel); tvExit.setText("退出阅读"); tvExit.setOnClickListener(v -> { dismiss(); itemClick.clickExit(); }); TextView tvAddShelf = view.findViewById(R.id.tv_done); tvAddShelf.setText("放入书架"); tvAddShelf.setOnClickListener(v -> itemClick.clickAddShelf()); } public interface OnItemClickListener { void clickExit(); void clickAddShelf(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/KeyboardToolPop.kt ================================================ package com.kunfei.bookshelf.view.popupwindow import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import android.widget.PopupWindow import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.kunfei.bookshelf.base.adapter.ItemViewHolder import com.kunfei.bookshelf.base.adapter.RecyclerAdapter import com.kunfei.bookshelf.databinding.ItemTextBinding import com.kunfei.bookshelf.databinding.PopupKeyboardToolBinding import org.jetbrains.anko.sdk27.listeners.onClick class KeyboardToolPop( context: Context, private val chars: List, val callBack: CallBack? ) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) { private val binding = PopupKeyboardToolBinding.inflate(LayoutInflater.from(context)) init { isTouchable = true isOutsideTouchable = false isFocusable = false inputMethodMode = INPUT_METHOD_NEEDED //解决遮盖输入法 contentView = binding.root initRecyclerView() } private fun initRecyclerView() = with(contentView) { val adapter = Adapter(context) binding.recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) binding.recyclerView.adapter = adapter adapter.setItems(chars) } inner class Adapter(context: Context) : RecyclerAdapter(context) { override fun getViewBinding(parent: ViewGroup): ItemTextBinding { return ItemTextBinding.inflate(inflater, parent, false) } override fun convert(holder: ItemViewHolder, binding: ItemTextBinding, item: String, payloads: MutableList) { with(binding) { textView.text = item root.onClick { callBack?.sendText(item) } } } override fun registerListener(holder: ItemViewHolder, binding: ItemTextBinding) { } } interface CallBack { fun sendText(text: String) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/MediaPlayerPop.java ================================================ package com.kunfei.bookshelf.view.popupwindow; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.LayoutInflater; import android.widget.FrameLayout; import android.widget.SeekBar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.bumptech.glide.request.RequestOptions; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.databinding.PopMediaPlayerBinding; import com.kunfei.bookshelf.help.BlurTransformation; import com.kunfei.bookshelf.help.glide.ImageLoader; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.TimeUtils; import com.kunfei.bookshelf.utils.theme.MaterialValueHelper; import com.kunfei.bookshelf.utils.theme.ThemeStore; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Locale; public class MediaPlayerPop extends FrameLayout { @SuppressLint("ConstantLocale") private final DateFormat timeFormat = new SimpleDateFormat("mm:ss", Locale.getDefault()); private PopMediaPlayerBinding binding = PopMediaPlayerBinding.inflate(LayoutInflater.from(getContext()), this, true); private int primaryTextColor; private Callback callback; public MediaPlayerPop(@NonNull Context context) { super(context); init(context); } public MediaPlayerPop(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } public MediaPlayerPop(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public MediaPlayerPop(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context); } private void init(Context context) { binding.getRoot().setBackgroundColor(ThemeStore.primaryColor(context)); binding.vwBg.setOnClickListener(null); primaryTextColor = MaterialValueHelper.getPrimaryTextColor(context, ColorUtils.isColorLight(ThemeStore.primaryColor(context))); setColor(binding.ivSkipPrevious.getDrawable()); setColor(binding.ivSkipNext.getDrawable()); setColor(binding.ivChapter.getDrawable()); setColor(binding.ivTimer.getDrawable()); binding.seekBar.setEnabled(false); binding.seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { if (callback != null) { callback.onStopTrackingTouch(seekBar.getProgress()); } } }); } private void setColor(Drawable drawable) { drawable.mutate(); drawable.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_ATOP); } public void setCallback(Callback callback) { this.callback = callback; } public void setSeekBarEnable(boolean enable) { binding.seekBar.setEnabled(enable); } public void upAudioSize(int audioSize) { binding.seekBar.setMax(audioSize); binding.tvAllTime.setText(TimeUtils.millis2String(audioSize, timeFormat)); } public void upAudioDur(int audioDur) { binding.seekBar.setProgress(audioDur); binding.tvDurTime.setText(TimeUtils.millis2String(audioDur, timeFormat)); } public void setIvCoverBgClickListener(OnClickListener onClickListener) { binding.ivCoverBg.setOnClickListener(onClickListener); } public void setPlayClickListener(OnClickListener onClickListener) { binding.fabPlayStop.setOnClickListener(onClickListener); } public void setPrevClickListener(OnClickListener onClickListener) { binding.ivSkipPrevious.setOnClickListener(onClickListener); } public void setNextClickListener(OnClickListener onClickListener) { binding.ivSkipNext.setOnClickListener(onClickListener); } public void setIvTimerClickListener(OnClickListener onClickListener) { binding.ivTimer.setOnClickListener(onClickListener); } public void setIvChapterClickListener(OnClickListener onClickListener) { binding.ivChapter.setOnClickListener(onClickListener); } public void setFabReadAloudImage(int id) { binding.fabPlayStop.setImageResource(id); } public void setCover(String coverPath) { ImageLoader.INSTANCE.load(getContext(), coverPath) .apply(new RequestOptions().dontAnimate().diskCacheStrategy(DiskCacheStrategy.RESOURCE).centerCrop() .placeholder(R.drawable.image_cover_default)) .into(binding.ivCover); ImageLoader.INSTANCE.load(getContext(), coverPath) .transition(DrawableTransitionOptions.withCrossFade(1500)) .thumbnail(defaultCover()) .centerCrop() .apply(RequestOptions.bitmapTransform(new BlurTransformation(getContext(), 25))) .into(binding.ivCoverBg); } private RequestBuilder defaultCover() { return ImageLoader.INSTANCE.load(getContext(), R.drawable.image_cover_default) .apply(RequestOptions.bitmapTransform(new BlurTransformation(getContext(), 25))); } public interface Callback { void onStopTrackingTouch(int dur); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/MoreSettingPop.kt ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.popupwindow import android.content.Context import android.content.DialogInterface import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.CompoundButton import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog import com.hwangjr.rxbus.RxBus import com.kunfei.bookshelf.R import com.kunfei.bookshelf.constant.RxBusTag import com.kunfei.bookshelf.databinding.PopMoreSettingBinding import com.kunfei.bookshelf.help.ReadBookControl import com.kunfei.bookshelf.utils.theme.ATH import com.kunfei.bookshelf.widget.modialog.PageKeyDialog import org.jetbrains.anko.sdk27.listeners.onClick class MoreSettingPop : FrameLayout { private val readBookControl = ReadBookControl.getInstance() private var callback: Callback? = null private val binding = PopMoreSettingBinding.inflate(LayoutInflater.from(context), this, true) constructor(context: Context) : super(context) { init(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { init(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { binding.vwBg.setOnClickListener(null) } fun setListener(callback: Callback) { this.callback = callback initData() bindEvent() } private fun bindEvent() { setOnClickListener { this.visibility = View.GONE } binding.sbImmersionStatusBar.setOnCheckedChangeListener { compoundButton: CompoundButton, b: Boolean -> if (compoundButton.isPressed) { readBookControl.immersionStatusBar = b callback?.upBar() RxBus.get().post(RxBusTag.RECREATE, true) } } binding.sbLightNovelParagraph.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.lightNovelParagraph = isChecked callback?.recreate() } } binding.sbHideStatusBar.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.hideStatusBar = isChecked callback?.recreate() } } binding.sbToLh.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.toLh = isChecked callback?.recreate() } } binding.sbHideNavigationBar.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.hideNavigationBar = isChecked initData() callback?.recreate() } } binding.swVolumeNextPage.setOnCheckedChangeListener { compoundButton: CompoundButton, b: Boolean -> if (compoundButton.isPressed) { readBookControl.canKeyTurn = b upView() } } binding.swReadAloudKey.setOnCheckedChangeListener { compoundButton: CompoundButton, b: Boolean -> if (compoundButton.isPressed) { readBookControl.aloudCanKeyTurn = b } } binding.sbClick.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.canClickTurn = isChecked upView() } } binding.sbClickAllNext.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.clickAllNext = isChecked } } binding.sbShowTitle.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.showTitle = isChecked callback?.refreshPage() } } binding.sbShowTimeBattery.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.showTimeBattery = isChecked callback?.refreshPage() } } binding.sbShowLine.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.showLine = isChecked callback?.refreshPage() } } binding.llScreenTimeOut.setOnClickListener { val dialog = AlertDialog.Builder(context) .setTitle(context.getString(R.string.keep_light)) .setSingleChoiceItems( context.resources.getStringArray(R.array.screen_time_out), readBookControl.screenTimeOut ) { dialogInterface: DialogInterface, i: Int -> readBookControl.screenTimeOut = i upScreenTimeOut(i) callback?.keepScreenOnChange(i) dialogInterface.dismiss() } .create() dialog.show() ATH.setAlertDialogTint(dialog) } binding.llJFConvert.setOnClickListener { val dialog = AlertDialog.Builder(context) .setTitle(context.getString(R.string.jf_convert)) .setSingleChoiceItems(context.resources.getStringArray(R.array.convert_s), readBookControl.textConvert) { dialogInterface: DialogInterface, i: Int -> readBookControl.textConvert = i upFConvert(i) dialogInterface.dismiss() callback?.refreshPage() } .create() dialog.show() ATH.setAlertDialogTint(dialog) } binding.llScreenDirection.setOnClickListener { val dialog = AlertDialog.Builder(context) .setTitle(context.getString(R.string.screen_direction)) .setSingleChoiceItems(context.resources.getStringArray(R.array.screen_direction_list_title), readBookControl.screenDirection) { dialogInterface: DialogInterface, i: Int -> readBookControl.screenDirection = i upScreenDirection(i) dialogInterface.dismiss() callback?.recreate() } .create() dialog.show() ATH.setAlertDialogTint(dialog) } binding.llNavigationBarColor.setOnClickListener { val dialog = AlertDialog.Builder(context) .setTitle(context.getString(R.string.re_navigation_bar_color)) .setSingleChoiceItems(context.resources.getStringArray(R.array.NavBarColors), readBookControl.navBarColor) { dialogInterface: DialogInterface, i: Int -> readBookControl.navBarColor = i upNavBarColor(i) dialogInterface.dismiss() callback?.recreate() } .create() dialog.show() ATH.setAlertDialogTint(dialog) } binding.sbSelectText.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> if (buttonView.isPressed) { readBookControl.isCanSelectText = isChecked } } binding.llClickKeyCode.onClick { PageKeyDialog(context).show() } } private fun initData() { upScreenDirection(readBookControl.screenDirection) upScreenTimeOut(readBookControl.screenTimeOut) upFConvert(readBookControl.textConvert) upNavBarColor(readBookControl.navBarColor) binding.sbImmersionStatusBar.isChecked = readBookControl.immersionStatusBar binding.swVolumeNextPage.isChecked = readBookControl.canKeyTurn binding.swReadAloudKey.isChecked = readBookControl.aloudCanKeyTurn binding.sbLightNovelParagraph.isChecked = readBookControl.lightNovelParagraph; binding.sbHideStatusBar.isChecked = readBookControl.hideStatusBar binding.sbToLh.isChecked = readBookControl.toLh binding.sbHideNavigationBar.isChecked = readBookControl.hideNavigationBar binding.sbClick.isChecked = readBookControl.canClickTurn binding.sbClickAllNext.isChecked = readBookControl.clickAllNext binding.sbShowTitle.isChecked = readBookControl.showTitle binding.sbShowTimeBattery.isChecked = readBookControl.showTimeBattery binding.sbShowLine.isChecked = readBookControl.showLine binding.sbSelectText.isChecked = readBookControl.isCanSelectText upView() } private fun upView() { if (readBookControl.hideStatusBar) { binding.sbShowTimeBattery.isEnabled = true binding.sbToLh.isEnabled = true } else { binding.sbShowTimeBattery.isEnabled = false binding.sbToLh.isEnabled = false } binding.swReadAloudKey.isEnabled = readBookControl.canKeyTurn binding.sbClickAllNext.isEnabled = readBookControl.canClickTurn if (readBookControl.hideNavigationBar) { binding.llNavigationBarColor.isEnabled = false binding.reNavBarColorVal.isEnabled = false } else { binding.llNavigationBarColor.isEnabled = true binding.reNavBarColorVal.isEnabled = true } } private fun upScreenTimeOut(screenTimeOut: Int) { binding.tvScreenTimeOut.text = context.resources.getStringArray(R.array.screen_time_out)[screenTimeOut] } private fun upFConvert(fConvert: Int) { binding.tvJFConvert.text = context.resources.getStringArray(R.array.convert_s)[fConvert] } private fun upScreenDirection(screenDirection: Int) { val screenDirectionListTitle = context.resources.getStringArray(R.array.screen_direction_list_title) if (screenDirection >= screenDirectionListTitle.size) { binding.tvScreenDirection.text = screenDirectionListTitle[0] } else { binding.tvScreenDirection.text = screenDirectionListTitle[screenDirection] } } private fun upNavBarColor(nColor: Int) { binding.reNavBarColorVal.text = context.resources.getStringArray(R.array.NavBarColors)[nColor] } interface Callback { fun upBar() fun keepScreenOnChange(keepScreenOn: Int) fun recreate() fun refreshPage() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadAdjustMarginPop.kt ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.popupwindow import android.app.Activity import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout import android.widget.SeekBar import android.widget.TextView import com.kunfei.bookshelf.databinding.PopReadAdjustMarginBinding import com.kunfei.bookshelf.help.ReadBookControl import org.jetbrains.anko.sdk27.listeners.onClick class ReadAdjustMarginPop : FrameLayout { val binding = PopReadAdjustMarginBinding.inflate(LayoutInflater.from(context), this, true) private var activity: Activity? = null private val readBookControl = ReadBookControl.getInstance() private var callback: Callback? = null 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 { binding.vwBg.setOnClickListener(null) } fun setListener(activity: Activity, callback: Callback) { this.activity = activity this.callback = callback initData(0) bindEvent() } fun show() { initData(0) } private fun initData(flag: Int) { if (flag == 0) { // 字距 setSeekBarView(binding.hpbMrF, binding.tvHpbMrF, -0.5f, 0.5f, readBookControl.textLetterSpacing, 100) // 行距 setSeekBarView(binding.hpbMrRm, binding.tvHpbMrRm, 0.5f, 3.0f, readBookControl.lineMultiplier, 10) // 段距 setSeekBarView(binding.hpbMrDm, binding.tvHpbMrDm, 1.0f, 5.0f, readBookControl.paragraphSize, 10) } if (flag == 0 || flag == 1) { // 正文边距 setSeekBarView(binding.hpbMrZT, binding.tvHpbMrZT, 0, 100, readBookControl.paddingTop) setSeekBarView(binding.hpbMrZL, binding.tvHpbMrZL, 0, 100, readBookControl.paddingLeft) setSeekBarView(binding.hpbMrZR, binding.tvHpbMrZR, 0, 100, readBookControl.paddingRight) setSeekBarView(binding.hpbMrZB, binding.tvHpbMrZB, 0, 100, readBookControl.paddingBottom) } if (flag == 0 || flag == 2) { // Tip边距 setSeekBarView(binding.hpbMrTT, binding.tvHpbMrTT, 0, 100, readBookControl.tipPaddingTop) setSeekBarView(binding.hpbMrTL, binding.tvHpbMrTL, 0, 100, readBookControl.tipPaddingLeft) setSeekBarView(binding.hpbMrTR, binding.tvHpbMrTR, 0, 100, readBookControl.tipPaddingRight) setSeekBarView(binding.hpbMrTB, binding.tvHpbMrTB, 0, 100, readBookControl.tipPaddingBottom) } } private fun bindEvent() = with(binding) { //字距调节 hpbMrF.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) { readBookControl.textLetterSpacing = i / 100.0f - 0.5f tvHpbMrF.text = String.format("%.2f", readBookControl.textLetterSpacing) callback?.upTextSize() } override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStopTrackingTouch(seekBar: SeekBar) {} }) ivMrFAdd.onClick { hpbMrF.progress = hpbMrF.progress + 1 } ivMrFRemove.onClick { hpbMrF.progress = hpbMrF.progress - 1 } //行距调节 hpbMrRm.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) { readBookControl.lineMultiplier = i / 10.0f + 0.5f tvHpbMrRm.text = String.format("%.1f", readBookControl.lineMultiplier) callback?.upTextSize() } override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStopTrackingTouch(seekBar: SeekBar) {} }) ivMrRmAdd.onClick { hpbMrRm.progress = hpbMrRm.progress + 1 } ivMrRmRemove.onClick { hpbMrRm.progress = hpbMrRm.progress - 1 } //段距调节 hpbMrDm.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) { readBookControl.paragraphSize = i / 10.0f + 1.0f tvHpbMrDm.text = String.format("%.1f", readBookControl.paragraphSize) callback?.upTextSize() } override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStopTrackingTouch(seekBar: SeekBar) {} }) ivMrDmAdd.onClick { hpbMrDm.progress = hpbMrDm.progress + 1 } ivMrDmRemove.onClick { hpbMrDm.progress = hpbMrDm.progress - 1 } //段距调节 val pdChange = object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) { var flag = 1 when { seekBar === hpbMrZT -> readBookControl.paddingTop = i seekBar === hpbMrZL -> readBookControl.paddingLeft = i seekBar === hpbMrZR -> readBookControl.paddingRight = i seekBar === hpbMrZB -> readBookControl.paddingBottom = i else -> { flag = 2 when { seekBar === hpbMrTT -> readBookControl.tipPaddingTop = i seekBar === hpbMrTL -> readBookControl.tipPaddingLeft = i seekBar === hpbMrTR -> readBookControl.tipPaddingRight = i else -> readBookControl.tipPaddingBottom = i } } } initData(flag) callback?.upMargin() } override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStopTrackingTouch(seekBar: SeekBar) {} } hpbMrZT.setOnSeekBarChangeListener(pdChange) hpbMrZL.setOnSeekBarChangeListener(pdChange) hpbMrZR.setOnSeekBarChangeListener(pdChange) hpbMrZB.setOnSeekBarChangeListener(pdChange) hpbMrTT.setOnSeekBarChangeListener(pdChange) hpbMrTL.setOnSeekBarChangeListener(pdChange) hpbMrTR.setOnSeekBarChangeListener(pdChange) hpbMrTB.setOnSeekBarChangeListener(pdChange) ivMrZTAdd.onClick { hpbMrZT.progress = hpbMrZT.progress + 1 } ivMrZTRemove.onClick { hpbMrZT.progress = hpbMrZT.progress - 1 } ivMrZLAdd.onClick { hpbMrZL.progress = hpbMrZL.progress + 1 } ivMrZLRemove.onClick { hpbMrZL.progress = hpbMrZL.progress - 1 } ivMrZRAdd.onClick { hpbMrZR.progress = hpbMrZR.progress + 1 } ivMrZRRemove.onClick { hpbMrZR.progress = hpbMrZR.progress - 1 } ivMrZBAdd.onClick { hpbMrZB.progress = hpbMrZB.progress + 1 } ivMrZBRemove.onClick { hpbMrZB.progress = hpbMrZB.progress - 1 } ivMrTTAdd.onClick { hpbMrTT.progress = hpbMrTT.progress + 1 } ivMrTTRemove.onClick { hpbMrTT.progress = hpbMrTT.progress - 1 } ivMrTLAdd.onClick { hpbMrTL.progress = hpbMrTL.progress + 1 } ivMrTLRemove.onClick { hpbMrTL.progress = hpbMrTL.progress - 1 } ivMrTRAdd.onClick { hpbMrTR.progress = hpbMrTR.progress + 1 } ivMrTRRemove.onClick { hpbMrTR.progress = hpbMrTR.progress - 1 } ivMrTBAdd.onClick { hpbMrTB.progress = hpbMrTB.progress + 1 } ivMrTBRemove.onClick { hpbMrTB.progress = hpbMrTB.progress - 1 } } private fun setSeekBarView(hpb: SeekBar, tv: TextView?, min: Float, max: Float, value: Float, p: Int) { val a = (min * p).toInt() val b = (max * p).toInt() - a hpb.max = b hpb.progress = (value * p).toInt() - a when { p >= 100 -> tv?.text = String.format("%.2f", value) p >= 10 -> tv?.text = String.format("%.1f", value) else -> tv?.text = String.format("%.0f", value) } } private fun setSeekBarView(hpb: SeekBar, tv: TextView, min: Int, max: Int, value: Int) { hpb.max = max - min hpb.progress = value - min tv.text = String.format("%d", value) } interface Callback { fun upTextSize() fun upMargin() fun refresh() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadAdjustPop.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.popupwindow; import android.app.Activity; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.SeekBar; import com.kunfei.bookshelf.databinding.PopReadAdjustBinding; import com.kunfei.bookshelf.help.ReadBookControl; public class ReadAdjustPop extends FrameLayout { private PopReadAdjustBinding binding = PopReadAdjustBinding.inflate(LayoutInflater.from(getContext()), this, true); private Activity activity; private ReadBookControl readBookControl = ReadBookControl.getInstance(); private Callback callback; public ReadAdjustPop(Context context) { super(context); init(context); } public ReadAdjustPop(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public ReadAdjustPop(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { binding.vwBg.setOnClickListener(null); } public void setListener(Activity activity, Callback callback) { this.activity = activity; this.callback = callback; initData(); bindEvent(); initLight(); } public void show() { initLight(); } private void initData() { binding.scbTtsFollowSys.setChecked(readBookControl.isSpeechRateFollowSys()); binding.hpbTtsSpeechRate.setEnabled(!readBookControl.isSpeechRateFollowSys()); //CPM范围设置 每分钟阅读200字到2000字 默认500字/分钟 binding.hpbClick.setMax(readBookControl.maxCPM - readBookControl.minCPM); binding.hpbClick.setProgress(readBookControl.getCPM()); binding.tvAutoPage.setText(String.format("%sCPM", readBookControl.getCPM())); binding.hpbTtsSpeechRate.setProgress(readBookControl.getSpeechRate() - 5); } private void bindEvent() { //亮度调节 binding.llFollowSys.setOnClickListener(v -> { binding.scbFollowSys.setChecked(!binding.scbFollowSys.isChecked(), true); }); binding.scbFollowSys.setOnCheckedChangeListener((checkBox, isChecked) -> { readBookControl.setLightFollowSys(isChecked); if (isChecked) { //跟随系统 binding.hpbLight.setEnabled(false); setScreenBrightness(); } else { //不跟随系统 binding.hpbLight.setEnabled(true); setScreenBrightness(readBookControl.getLight()); } }); binding.hpbLight.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { if (!readBookControl.getLightFollowSys()) { readBookControl.setLight(i); setScreenBrightness(i); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); //自动翻页阅读速度(CPM) binding.hpbClick.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { binding.tvAutoPage.setText(String.format("%sCPM", i + readBookControl.minCPM)); readBookControl.setCPM(i + readBookControl.minCPM); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); //朗读语速调节 binding.llTtsSpeechRate.setOnClickListener(v -> { binding.scbTtsFollowSys.setChecked(!binding.scbTtsFollowSys.isChecked(), true); }); binding.scbTtsFollowSys.setOnCheckedChangeListener((checkBox, isChecked) -> { if (isChecked) { //跟随系统 binding.hpbTtsSpeechRate.setEnabled(false); readBookControl.setSpeechRateFollowSys(true); if (callback != null) { callback.speechRateFollowSys(); } } else { //不跟随系统 binding.hpbTtsSpeechRate.setEnabled(true); readBookControl.setSpeechRateFollowSys(false); if (callback != null) { callback.changeSpeechRate(readBookControl.getSpeechRate()); } } }); binding.hpbTtsSpeechRate.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { readBookControl.setSpeechRate(seekBar.getProgress() + 5); if (callback != null) { callback.changeSpeechRate(readBookControl.getSpeechRate()); } } }); } public void setScreenBrightness() { WindowManager.LayoutParams params = activity.getWindow().getAttributes(); params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; activity.getWindow().setAttributes(params); } public void setScreenBrightness(int value) { if (value < 1) value = 1; WindowManager.LayoutParams params = activity.getWindow().getAttributes(); params.screenBrightness = value * 1.0f / 255f; activity.getWindow().setAttributes(params); } public void initLight() { binding.hpbLight.setProgress(readBookControl.getLight()); binding.scbFollowSys.setChecked(readBookControl.getLightFollowSys()); if (!readBookControl.getLightFollowSys()) { setScreenBrightness(readBookControl.getLight()); } } public interface Callback { void changeSpeechRate(int speechRate); void speechRateFollowSys(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadBottomMenu.java ================================================ package com.kunfei.bookshelf.view.popupwindow; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.SeekBar; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.databinding.PopReadMenuBinding; import com.kunfei.bookshelf.service.ReadAloudService; public class ReadBottomMenu extends FrameLayout { private PopReadMenuBinding binding = PopReadMenuBinding.inflate(LayoutInflater.from(getContext()), this, true); private Callback callback; public ReadBottomMenu(Context context) { super(context); init(context); } public ReadBottomMenu(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public ReadBottomMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { binding.vwBg.setOnClickListener(null); binding.vwNavigationBar.setOnClickListener(null); } public void setNavigationBarHeight(int height) { ViewGroup.LayoutParams layoutParams = binding.vwNavigationBar.getLayoutParams(); layoutParams.height = height; binding.vwNavigationBar.setLayoutParams(layoutParams); } public void setListener(Callback callback) { this.callback = callback; bindEvent(); } private void bindEvent() { binding.llReadAloudTimer.setOnClickListener(view -> callback.dismiss()); binding.llFloatingButton.setOnClickListener(view -> callback.dismiss()); //阅读进度 binding.hpbReadProgress.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { callback.skipToPage(seekBar.getProgress()); } }); //朗读定时 binding.fabReadAloudTimer.setOnClickListener(view -> ReadAloudService.setTimer(getContext(), 10)); //朗读 binding.fabReadAloud.setOnClickListener(view -> callback.onMediaButton()); //长按停止朗读 binding.fabReadAloud.setOnLongClickListener(view -> { if (ReadAloudService.running) { callback.toast(R.string.aloud_stop); ReadAloudService.stop(getContext()); } else { callback.toast(R.string.read_aloud); } return true; }); //自动翻页 binding.fabAutoPage.setOnClickListener(view -> callback.autoPage()); binding.fabAutoPage.setOnLongClickListener(view -> { callback.toast(R.string.auto_next_page); return true; }); //替换 binding.fabReplaceRule.setOnClickListener(view -> callback.openReplaceRule()); binding.fabReplaceRule.setOnLongClickListener(view -> { callback.toast(R.string.replace_rule_title); return true; }); //夜间模式 binding.fabNightTheme.setOnClickListener(view -> callback.setNightTheme()); binding.fabNightTheme.setOnLongClickListener(view -> { callback.toast(R.string.night_theme); return true; }); //上一章 binding.tvPre.setOnClickListener(view -> callback.skipPreChapter()); //下一章 binding.tvNext.setOnClickListener(view -> callback.skipNextChapter()); //目录 binding.llCatalog.setOnClickListener(view -> callback.openChapterList()); //调节 binding.llAdjust.setOnClickListener(view -> callback.openAdjust()); //界面 binding.llFont.setOnClickListener(view -> callback.openReadInterface()); //设置 binding.llSetting.setOnClickListener(view -> callback.openMoreSetting()); binding.tvReadAloudTimer.setOnClickListener(null); } public void setFabReadAloudImage(int id) { binding.fabReadAloud.setImageResource(id); } public void setReadAloudTimer(boolean visibility) { if (visibility) { binding.llReadAloudTimer.setVisibility(VISIBLE); } else { binding.llReadAloudTimer.setVisibility(GONE); } } public void setReadAloudTimer(String text) { binding.tvReadAloudTimer.setText(text); } public void setFabReadAloudText(String text) { binding.fabReadAloud.setContentDescription(text); } public SeekBar getReadProgress() { return binding.hpbReadProgress; } public void setTvPre(boolean enable) { binding.tvPre.setEnabled(enable); } public void setTvNext(boolean enable) { binding.tvNext.setEnabled(enable); } public void setAutoPage(boolean autoPage) { if (autoPage) { binding.fabAutoPage.setImageResource(R.drawable.ic_auto_page_stop); binding.fabAutoPage.setContentDescription(getContext().getString(R.string.auto_next_page_stop)); } else { binding.fabAutoPage.setImageResource(R.drawable.ic_auto_page); binding.fabAutoPage.setContentDescription(getContext().getString(R.string.auto_next_page)); } } public void setFabNightTheme(boolean isNightTheme) { if (isNightTheme) { binding.fabNightTheme.setImageResource(R.drawable.ic_daytime); } else { binding.fabNightTheme.setImageResource(R.drawable.ic_brightness); } } public interface Callback { void skipToPage(int page); void onMediaButton(); void autoPage(); void setNightTheme(); void skipPreChapter(); void skipNextChapter(); void openReplaceRule(); void openChapterList(); void openAdjust(); void openReadInterface(); void openMoreSetting(); void toast(int id); void dismiss(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadInterfacePop.kt ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.popupwindow import android.annotation.SuppressLint import android.content.Context import android.content.DialogInterface import android.content.Intent import android.graphics.Color import android.net.Uri import android.os.Build import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog import androidx.documentfile.provider.DocumentFile import com.kunfei.bookshelf.R import com.kunfei.bookshelf.databinding.PopReadInterfaceBinding import com.kunfei.bookshelf.help.ReadBookControl import com.kunfei.bookshelf.help.permission.Permissions import com.kunfei.bookshelf.help.permission.PermissionsCompat import com.kunfei.bookshelf.utils.* import com.kunfei.bookshelf.utils.theme.ATH import com.kunfei.bookshelf.view.activity.ReadBookActivity import com.kunfei.bookshelf.view.activity.ReadStyleActivity import com.kunfei.bookshelf.widget.font.FontSelector import com.kunfei.bookshelf.widget.font.FontSelector.OnThisListener import com.kunfei.bookshelf.widget.page.animation.PageAnimation import timber.log.Timber class ReadInterfacePop : FrameLayout { private val binding = PopReadInterfaceBinding.inflate( LayoutInflater.from( context ), this, true ) private var activity: ReadBookActivity? = null private val readBookControl = ReadBookControl.getInstance() private var callback: Callback? = null constructor(context: Context) : super(context) { init() } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { init() } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { init() } private fun init() { binding.vwBg.setOnClickListener(null) } fun setListener(readBookActivity: ReadBookActivity, callback: Callback) { activity = readBookActivity this.callback = callback initData() bindEvent() } @SuppressLint("DefaultLocale") private fun initData() { setBg() updateBg(readBookControl.textDrawableIndex) updateBoldText(readBookControl.textBold) updatePageMode(readBookControl.pageMode) binding.nbTextSize.text = String.format("%d", readBookControl.textSize) } /** * 控件事件 */ @SuppressLint("DefaultLocale") private fun bindEvent() { //字号减 binding.nbTextSizeDec.setOnClickListener { var fontSize = readBookControl.textSize - 1 if (fontSize < 10) fontSize = 10 readBookControl.textSize = fontSize binding.nbTextSize.text = String.format("%d", readBookControl.textSize) callback!!.upTextSize() } //字号加 binding.nbTextSizeAdd.setOnClickListener { var fontSize = readBookControl.textSize + 1 if (fontSize > 40) fontSize = 40 readBookControl.textSize = fontSize binding.nbTextSize.text = String.format("%d", readBookControl.textSize) callback!!.upTextSize() } //缩进 binding.flIndent.setOnClickListener { val dialog = AlertDialog.Builder( activity!!, R.style.alertDialogTheme ) .setTitle(activity!!.getString(R.string.indent)) .setSingleChoiceItems( activity!!.resources.getStringArray(R.array.indent), readBookControl.indent ) { dialogInterface: DialogInterface, i: Int -> readBookControl.indent = i callback!!.refresh() dialogInterface.dismiss() } .create() dialog.show() ATH.setAlertDialogTint(dialog) } //翻页模式 binding.tvPageMode.setOnClickListener { val dialog = AlertDialog.Builder( activity!!, R.style.alertDialogTheme ) .setTitle(activity!!.getString(R.string.page_mode)) .setSingleChoiceItems( PageAnimation.Mode.getAllPageMode(), readBookControl.pageMode ) { dialogInterface: DialogInterface, i: Int -> readBookControl.pageMode = i updatePageMode(i) callback!!.upPageMode() dialogInterface.dismiss() } .create() dialog.show() ATH.setAlertDialogTint(dialog) } //加粗切换 binding.flTextBold.setOnClickListener { readBookControl.textBold = !readBookControl.textBold updateBoldText(readBookControl.textBold) callback!!.upTextSize() } //行距单倍 binding.tvRowDef0.setOnClickListener { readBookControl.lineMultiplier = 0.6f readBookControl.paragraphSize = 1.5f callback!!.upTextSize() } //行距双倍 binding.tvRowDef1.setOnClickListener { readBookControl.lineMultiplier = 1.2f readBookControl.paragraphSize = 1.8f callback!!.upTextSize() } //行距三倍 binding.tvRowDef2.setOnClickListener { readBookControl.lineMultiplier = 1.8f readBookControl.paragraphSize = 2.0f callback!!.upTextSize() } //行距默认 binding.tvRowDef.setOnClickListener { readBookControl.lineMultiplier = 1.0f readBookControl.paragraphSize = 1.8f callback!!.upTextSize() } //自定义间距 binding.tvOther.setOnClickListener { activity!!.readAdjustMarginIn() } //背景选择 binding.civBgWhite.setOnClickListener { updateBg(0) callback!!.bgChange() } binding.civBgYellow.setOnClickListener { updateBg(1) callback!!.bgChange() } binding.civBgGreen.setOnClickListener { updateBg(2) callback!!.bgChange() } binding.civBgBlue.setOnClickListener { updateBg(3) callback!!.bgChange() } binding.civBgBlack.setOnClickListener { updateBg(4) callback!!.bgChange() } //自定义阅读样式 binding.civBgWhite.setOnLongClickListener { customReadStyle(0) } binding.civBgYellow.setOnLongClickListener { customReadStyle(1) } binding.civBgGreen.setOnLongClickListener { customReadStyle(2) } binding.civBgBlue.setOnLongClickListener { customReadStyle(3) } binding.civBgBlack.setOnLongClickListener { customReadStyle(4) } //选择字体 binding.flTextFont.setOnClickListener { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { activity!!.selectFontDir() } else { PermissionsCompat.Builder(activity!!) .addPermissions( Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE ) .rationale(R.string.get_storage_per) .onGranted { kotlin.runCatching { selectFont( DocumentUtils.listFiles(FileUtils.getSdCardPath() + "/Fonts") { it.name.matches(FontSelector.fontRegex) } ) }.onFailure { context.toastOnUi("获取文件出错\n${it.localizedMessage}") } } .request() } } //长按清除字体 binding.flTextFont.setOnLongClickListener { clearFontPath() activity!!.toast(R.string.clear_font) true } } fun showFontSelector(uri: Uri) { kotlin.runCatching { val doc = DocumentFile.fromTreeUri(context, uri) DocumentUtils.listFiles(doc!!.uri) { it.name.matches(FontSelector.fontRegex) }.let { selectFont(it) } }.onFailure { context.toastOnUi("获取文件列表出错\n${it.localizedMessage}") Timber.e(it) } } private fun selectFont(docItems: List?) { FontSelector(context, readBookControl.fontPath) .setListener(object : OnThisListener { override fun setDefault() { clearFontPath() } override fun setFontPath(fileDoc: FileDoc) { setReadFonts(fileDoc) } }) .create(docItems) .show() } //自定义阅读样式 private fun customReadStyle(index: Int): Boolean { val intent = Intent(activity, ReadStyleActivity::class.java) intent.putExtra("index", index) activity!!.startActivity(intent) return false } //设置字体 fun setReadFonts(fileDoc: FileDoc) { if (fileDoc.isContentScheme) { val file = FileUtils.createFileIfNotExist(context.externalFiles, "Fonts", fileDoc.name) file.writeBytes(fileDoc.uri.readBytes(context)) readBookControl.setReadBookFont(file.absolutePath) } else { readBookControl.setReadBookFont(fileDoc.uri.toString()) } callback!!.refresh() } //清除字体 fun clearFontPath() { readBookControl.setReadBookFont(null) callback!!.refresh() } private fun updatePageMode(pageMode: Int) { binding.tvPageMode.text = String.format("%s", PageAnimation.Mode.getPageMode(pageMode)) } private fun updateBoldText(isBold: Boolean) { binding.flTextBold.isSelected = isBold } fun setBg() { binding.tv0.setTextColor(readBookControl.getTextColor(0)) binding.tv1.setTextColor(readBookControl.getTextColor(1)) binding.tv2.setTextColor(readBookControl.getTextColor(2)) binding.tv3.setTextColor(readBookControl.getTextColor(3)) binding.tv4.setTextColor(readBookControl.getTextColor(4)) binding.civBgWhite.setImageDrawable(readBookControl.getBgDrawable(0, activity, 100, 180)) binding.civBgYellow.setImageDrawable(readBookControl.getBgDrawable(1, activity, 100, 180)) binding.civBgGreen.setImageDrawable(readBookControl.getBgDrawable(2, activity, 100, 180)) binding.civBgBlue.setImageDrawable(readBookControl.getBgDrawable(3, activity, 100, 180)) binding.civBgBlack.setImageDrawable(readBookControl.getBgDrawable(4, activity, 100, 180)) } private fun updateBg(index: Int) { binding.civBgWhite.borderColor = activity!!.getCompatColor(R.color.tv_text_default) binding.civBgYellow.borderColor = activity!!.getCompatColor(R.color.tv_text_default) binding.civBgGreen.borderColor = activity!!.getCompatColor(R.color.tv_text_default) binding.civBgBlack.borderColor = activity!!.getCompatColor(R.color.tv_text_default) binding.civBgBlue.borderColor = activity!!.getCompatColor(R.color.tv_text_default) when (index) { 0 -> binding.civBgWhite.borderColor = Color.parseColor("#F3B63F") 1 -> binding.civBgYellow.borderColor = Color.parseColor("#F3B63F") 2 -> binding.civBgGreen.borderColor = Color.parseColor("#F3B63F") 3 -> binding.civBgBlue.borderColor = Color.parseColor("#F3B63F") 4 -> binding.civBgBlack.borderColor = Color.parseColor("#F3B63F") } readBookControl.textDrawableIndex = index } interface Callback { fun upPageMode() fun upTextSize() fun upMargin() fun bgChange() fun refresh() } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadLongPressPop.java ================================================ package com.kunfei.bookshelf.view.popupwindow; import android.content.Context; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Region; import android.os.Build; import android.util.AttributeSet; import android.view.LayoutInflater; import android.widget.FrameLayout; import androidx.annotation.NonNull; import com.kunfei.bookshelf.databinding.PopReadLongPressBinding; import com.kunfei.bookshelf.help.ReadBookControl; import com.kunfei.bookshelf.utils.DensityUtil; public class ReadLongPressPop extends FrameLayout { private PopReadLongPressBinding binding = PopReadLongPressBinding.inflate(LayoutInflater.from(getContext()), this, true); //private ReadBookActivity activity; private ReadBookControl readBookControl = ReadBookControl.getInstance(); private OnBtnClickListener clickListener; public ReadLongPressPop(Context context) { super(context); init(context); } public ReadLongPressPop(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public ReadLongPressPop(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @Override protected void dispatchDraw(Canvas canvas) { Path path = new Path(); path.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()), DensityUtil.dp2px(getContext(), 4), DensityUtil.dp2px(getContext(), 4), Path.Direction.CW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { canvas.clipPath(path); } else { canvas.clipPath(path, Region.Op.REPLACE); } super.dispatchDraw(canvas); } private void init(Context context) { binding.getRoot().setOnClickListener(null); } public void setListener(@NonNull OnBtnClickListener clickListener) { //this.activity = readBookActivity; this.clickListener = clickListener; initData(); bindEvent(); } private void initData() { } private void bindEvent() { //复制 binding.flCp.setOnClickListener(v -> clickListener.copySelect()); //替换 binding.flReplace.setOnClickListener(v -> clickListener.replaceSelect()); //标记广告 binding.flReplaceAd.setOnClickListener(v -> clickListener.replaceSelectAd()); } public interface OnBtnClickListener { void copySelect(); void replaceSelect(); void replaceSelectAd(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/HttpServer.java ================================================ package com.kunfei.bookshelf.web; import com.google.gson.Gson; import com.kunfei.bookshelf.web.controller.BookshelfController; import com.kunfei.bookshelf.web.controller.SourceController; import com.kunfei.bookshelf.web.utils.AssetsWeb; import com.kunfei.bookshelf.web.utils.ReturnData; import java.util.HashMap; import java.util.List; import java.util.Map; import fi.iki.elonen.NanoHTTPD; public class HttpServer extends NanoHTTPD { private AssetsWeb assetsWeb = new AssetsWeb("web"); public HttpServer(int port) { super(port); } @Override public Response serve(IHTTPSession session) { ReturnData returnData = null; String uri = session.getUri(); try { switch (session.getMethod().name()) { case "OPTIONS": Response response = newFixedLengthResponse(""); response.addHeader("Access-Control-Allow-Methods", "POST"); response.addHeader("Access-Control-Allow-Headers", "content-type"); response.addHeader("Access-Control-Allow-Origin", session.getHeaders().get("origin")); //response.addHeader("Access-Control-Max-Age", "3600"); return response; case "POST": Map files = new HashMap<>(); session.parseBody(files); String postData = files.get("postData"); switch (uri) { case "/saveSource": returnData = new SourceController().saveSource(postData); break; case "/saveSources": returnData = new SourceController().saveSources(postData); break; case "/saveBook": returnData = new BookshelfController().saveBook(postData); break; case "/deleteSources": returnData = new SourceController().deleteSources(postData); } break; case "GET": Map> parameters = session.getParameters(); switch (uri) { case "/getSource": returnData = new SourceController().getSource(parameters); break; case "/getSources": returnData = new SourceController().getSources(); break; case "/getBookshelf": returnData = new BookshelfController().getBookshelf(); break; case "/getChapterList": returnData = new BookshelfController().getChapterList(parameters); break; case "/getBookContent": returnData = new BookshelfController().getBookContent(parameters); break; } break; } if (returnData == null) { if (uri.endsWith("/")) { uri = uri + "index.html"; } return assetsWeb.getResponse(uri); } Response response = newFixedLengthResponse(new Gson().toJson(returnData)); response.addHeader("Access-Control-Allow-Methods", "GET, POST"); response.addHeader("Access-Control-Allow-Origin", session.getHeaders().get("origin")); return response; } catch (Exception e) { return newFixedLengthResponse(e.getMessage()); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/ShareServer.java ================================================ package com.kunfei.bookshelf.web; import com.google.gson.Gson; import com.kunfei.bookshelf.bean.BookSourceBean; import java.util.List; import fi.iki.elonen.NanoHTTPD; public class ShareServer extends NanoHTTPD { private Callback callback; public ShareServer(int port, Callback callback) { super(port); this.callback = callback; } @Override public Response serve(IHTTPSession session) { return newFixedLengthResponse(new Gson().toJson(callback.getSources())); } public interface Callback { List getSources(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/WebSocketServer.java ================================================ package com.kunfei.bookshelf.web; import com.kunfei.bookshelf.web.controller.SourceDebugWebSocket; import fi.iki.elonen.NanoWSD; public class WebSocketServer extends NanoWSD { public WebSocketServer(int port) { super(port); } @Override protected WebSocket openWebSocket(IHTTPSession handshake) { if (handshake.getUri().equals("/sourceDebug")) { return new SourceDebugWebSocket(handshake); } return null; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/controller/BookshelfController.java ================================================ package com.kunfei.bookshelf.web.controller; import android.text.TextUtils; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.utils.GsonUtils; import com.kunfei.bookshelf.web.utils.ReturnData; import java.util.List; import java.util.Map; public class BookshelfController { public ReturnData getBookshelf() { List shelfBeans = BookshelfHelp.getAllBook(); ReturnData returnData = new ReturnData(); if (shelfBeans.isEmpty()) { return returnData.setErrorMsg("还没有添加小说"); } return returnData.setData(shelfBeans); } public ReturnData getChapterList(Map> parameters) { List strings = parameters.get("url"); ReturnData returnData = new ReturnData(); if (strings == null) { return returnData.setErrorMsg("参数url不能为空,请指定书籍地址"); } List chapterList = BookshelfHelp.getChapterList(strings.get(0)); return returnData.setData(chapterList); } public ReturnData getBookContent(Map> parameters) { List strings = parameters.get("url"); ReturnData returnData = new ReturnData(); if (strings == null) { return returnData.setErrorMsg("参数url不能为空,请指定内容地址"); } BookChapterBean chapter = DbHelper.getDaoSession().getBookChapterBeanDao().load(strings.get(0)); if (chapter == null) { return returnData.setErrorMsg("未找到"); } BookShelfBean bookShelfBean = BookshelfHelp.getBook(chapter.getNoteUrl()); if (bookShelfBean == null) { return returnData.setErrorMsg("未找到"); } String content = BookshelfHelp.getChapterCache(bookShelfBean, chapter); if (!TextUtils.isEmpty(content)) { return returnData.setData(content); } try { BookContentBean bookContentBean = WebBookModel.getInstance().getBookContent(bookShelfBean, chapter, null).blockingFirst(); return returnData.setData(bookContentBean.getDurChapterContent()); } catch (Exception e) { return returnData.setErrorMsg(e.getMessage()); } } public ReturnData saveBook(String postData) { BookShelfBean bookShelfBean = GsonUtils.parseJObject(postData, BookShelfBean.class); ReturnData returnData = new ReturnData(); if (bookShelfBean != null) { DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean); return returnData.setData(""); } return returnData.setErrorMsg("格式不对"); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/controller/SourceController.java ================================================ package com.kunfei.bookshelf.web.controller; import android.text.TextUtils; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.utils.GsonUtils; import com.kunfei.bookshelf.web.utils.ReturnData; import java.util.ArrayList; import java.util.List; import java.util.Map; public class SourceController { public ReturnData saveSource(String postData) { BookSourceBean bookSourceBean = GsonUtils.parseJObject(postData, BookSourceBean.class); ReturnData returnData = new ReturnData(); if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl())) { return returnData.setErrorMsg("书源名称和URL不能为空"); } BookSourceManager.addBookSource(bookSourceBean); return returnData.setData(""); } public ReturnData saveSources(String postData) { List bookSourceBeans = GsonUtils.parseJArray(postData, BookSourceBean.class); List okSources = new ArrayList<>(); for (BookSourceBean bookSourceBean : bookSourceBeans) { if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl())) { continue; } BookSourceManager.addBookSource(bookSourceBean); okSources.add(bookSourceBean); } return (new ReturnData()).setData(okSources); } public ReturnData getSource(Map> parameters) { List strings = parameters.get("url"); ReturnData returnData = new ReturnData(); if (strings == null) { return returnData.setErrorMsg("参数url不能为空,请指定书源地址"); } BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(strings.get(0)); if (bookSourceBean == null) { return returnData.setErrorMsg("未找到书源,请检查书源地址"); } return returnData.setData(bookSourceBean); } public ReturnData getSources() { List bookSourceBeans = BookSourceManager.getAllBookSource(); ReturnData returnData = new ReturnData(); if (bookSourceBeans.size() == 0) { return returnData.setErrorMsg("设备书源列表为空"); } return returnData.setData(BookSourceManager.getAllBookSource()); } public ReturnData deleteSources(String postData) { List bookSourceBeans = GsonUtils.parseJArray(postData, BookSourceBean.class); /*List okSources= new ArrayList<>();*/ for (BookSourceBean bookSourceBean : bookSourceBeans) { /*if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl())) { continue; }*/ BookSourceManager.removeBookSource(bookSourceBean); /*if(BookSourceManager.getBookSourceByUrl(bookSourceBean.getBookSourceUrl()) == null){ okSources.add(bookSourceBean); }*/ } return (new ReturnData()).setData("已执行"/*okSources*/); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/controller/SourceDebugWebSocket.java ================================================ package com.kunfei.bookshelf.web.controller; import android.text.TextUtils; import com.google.gson.Gson; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.model.content.Debug; import com.kunfei.bookshelf.utils.StringUtils; import java.io.IOException; import java.util.Map; import java.util.concurrent.TimeUnit; import fi.iki.elonen.NanoHTTPD; import fi.iki.elonen.NanoWSD; import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING; public class SourceDebugWebSocket extends NanoWSD.WebSocket { private CompositeDisposable compositeDisposable; public SourceDebugWebSocket(NanoHTTPD.IHTTPSession handshakeRequest) { super(handshakeRequest); } @Override protected void onOpen() { RxBus.get().register(this); compositeDisposable = new CompositeDisposable(); Observable.interval(10, 10, TimeUnit.SECONDS) .observeOn(Schedulers.io()) .subscribe(new MyObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(Long aLong) { try { ping(new byte[]{aLong.byteValue()}); } catch (IOException e) { e.printStackTrace(); } } }); } @Override protected void onClose(NanoWSD.WebSocketFrame.CloseCode code, String reason, boolean initiatedByRemote) { RxBus.get().unregister(this); compositeDisposable.dispose(); Debug.SOURCE_DEBUG_TAG = null; } @Override protected void onMessage(NanoWSD.WebSocketFrame message) { if (!StringUtils.isJsonType(message.getTextPayload())) return; Map debugBean = new Gson().fromJson(message.getTextPayload(), MAP_STRING); String tag = debugBean.get("tag"); String key = debugBean.get("key"); if (TextUtils.isEmpty(tag) || TextUtils.isEmpty(key)) { try { send(MApplication.getInstance().getString(R.string.cannot_empty)); close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false); } catch (IOException ignored) { } return; } Debug.newDebug(tag, key, compositeDisposable); } @Override protected void onPong(NanoWSD.WebSocketFrame pong) { } @Override protected void onException(IOException exception) { Debug.SOURCE_DEBUG_TAG = null; } @Subscribe(thread = EventThread.EXECUTOR, tags = {@Tag(RxBusTag.PRINT_DEBUG_LOG)}) public void printDebugLog(String msg) { try { send(msg); if (msg.equals("finish")) { close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false); Debug.SOURCE_DEBUG_TAG = null; } } catch (IOException e) { Debug.SOURCE_DEBUG_TAG = null; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/utils/AssetsWeb.java ================================================ package com.kunfei.bookshelf.web.utils; import android.content.res.AssetManager; import android.text.TextUtils; import com.kunfei.bookshelf.MApplication; import java.io.File; import java.io.IOException; import java.io.InputStream; import fi.iki.elonen.NanoHTTPD; public class AssetsWeb { private AssetManager assetManager; private String rootPath = "web"; public AssetsWeb(String rootPath) { if (!TextUtils.isEmpty(rootPath)) { this.rootPath = rootPath; } assetManager = MApplication.getInstance().getAssets(); } public NanoHTTPD.Response getResponse(String path) throws IOException { path = (rootPath + path).replaceAll("/+", File.separator); InputStream inputStream = assetManager.open(path); return NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.OK, getMimeType(path), inputStream); } private String getMimeType(String path) { String suffix = path.substring(path.lastIndexOf(".")); String mimeType = "text/html"; if (suffix.equalsIgnoreCase(".html") || suffix.equalsIgnoreCase(".htm")) { mimeType = "text/html"; } else if (suffix.equalsIgnoreCase(".js")) { mimeType = "text/javascript"; } else if (suffix.equalsIgnoreCase(".css")) { mimeType = "text/css"; } else if (suffix.equalsIgnoreCase(".ico")) { mimeType = "image/x-icon"; } return mimeType; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/web/utils/ReturnData.java ================================================ package com.kunfei.bookshelf.web.utils; public class ReturnData { private boolean isSuccess; private int errorCode; private String errorMsg; private Object data; public ReturnData() { this.isSuccess = false; this.errorMsg = "未知错误,请联系开发者!"; } public boolean isSuccess() { return isSuccess; } public void setSuccess(boolean success) { isSuccess = success; } public int getErrorCode() { return errorCode; } public void setErrorCode(int errorCode) { this.errorCode = errorCode; } public String getErrorMsg() { return errorMsg; } public ReturnData setErrorMsg(String errorMsg) { this.isSuccess = false; this.errorMsg = errorMsg; return this; } public Object getData() { return data; } public ReturnData setData(Object data) { this.isSuccess = true; this.errorMsg = ""; this.data = data; return this; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/BadgeView.java ================================================ package com.kunfei.bookshelf.widget; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import android.widget.TabWidget; import androidx.appcompat.widget.AppCompatTextView; import com.kunfei.bookshelf.R; /** * Created by milad heydari on 5/6/2016. */ public class BadgeView extends AppCompatTextView { private boolean mHideOnNull = true; private float radius; public BadgeView(Context context) { this(context, null); } public BadgeView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.textViewStyle); } public BadgeView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { if (!(getLayoutParams() instanceof LayoutParams)) { LayoutParams layoutParams = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER); setLayoutParams(layoutParams); } // set default font setTextColor(Color.WHITE); //setTypeface(Typeface.DEFAULT_BOLD); setTextSize(TypedValue.COMPLEX_UNIT_SP, 11); setPadding(dip2Px(5), dip2Px(1), dip2Px(5), dip2Px(1)); radius = 8; // set default background setBackground(radius, Color.parseColor("#d3321b")); setGravity(Gravity.CENTER); // default values setHideOnNull(true); setBadgeCount(0); setMinWidth(dip2Px(16)); setMinHeight(dip2Px(16)); } public void setBackground(float dipRadius, int badgeColor) { int radius = dip2Px(dipRadius); float[] radiusArray = new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; RoundRectShape roundRect = new RoundRectShape(radiusArray, null, null); ShapeDrawable bgDrawable = new ShapeDrawable(roundRect); bgDrawable.getPaint().setColor(badgeColor); setBackground(bgDrawable); } public void setBackground(int badgeColor) { setBackground(radius, badgeColor); } /** * @return Returns true if view is hidden on badge value 0 or null; */ public boolean isHideOnNull() { return mHideOnNull; } /** * @param hideOnNull the hideOnNull to set */ public void setHideOnNull(boolean hideOnNull) { mHideOnNull = hideOnNull; setText(getText()); } /** * @see android.widget.TextView#setText(java.lang.CharSequence, android.widget.TextView.BufferType) */ @Override public void setText(CharSequence text, BufferType type) { if (isHideOnNull() && TextUtils.isEmpty(text)) { setVisibility(GONE); } else { setVisibility(VISIBLE); } super.setText(text, type); } public void setBadgeCount(int count) { setText(String.valueOf(count)); if (count == 0) { setVisibility(GONE); } } public void setHighlight(boolean highlight) { setBackground(getResources().getColor(highlight ? R.color.highlight : R.color.darker_gray)); } public Integer getBadgeCount() { if (getText() == null) { return null; } String text = getText().toString(); try { return Integer.parseInt(text); } catch (NumberFormatException e) { return null; } } public void setBadgeGravity(int gravity) { LayoutParams params = (LayoutParams) getLayoutParams(); params.gravity = gravity; setLayoutParams(params); } public int getBadgeGravity() { LayoutParams params = (LayoutParams) getLayoutParams(); return params.gravity; } public void setBadgeMargin(int dipMargin) { setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin); } public void setBadgeMargin(int leftDipMargin, int topDipMargin, int rightDipMargin, int bottomDipMargin) { LayoutParams params = (LayoutParams) getLayoutParams(); params.leftMargin = dip2Px(leftDipMargin); params.topMargin = dip2Px(topDipMargin); params.rightMargin = dip2Px(rightDipMargin); params.bottomMargin = dip2Px(bottomDipMargin); setLayoutParams(params); } public int[] getBadgeMargin() { LayoutParams params = (LayoutParams) getLayoutParams(); return new int[]{params.leftMargin, params.topMargin, params.rightMargin, params.bottomMargin}; } public void incrementBadgeCount(int increment) { Integer count = getBadgeCount(); if (count == null) { setBadgeCount(increment); } else { setBadgeCount(increment + count); } } public void decrementBadgeCount(int decrement) { incrementBadgeCount(-decrement); } /** * Attach the BadgeView to the TabWidget * * @param target the TabWidget to attach the BadgeView * @param tabIndex index of the tab */ public void setTargetView(TabWidget target, int tabIndex) { View tabView = target.getChildTabViewAt(tabIndex); setTargetView(tabView); } /** * Attach the BadgeView to the target view * * @param target the view to attach the BadgeView */ public void setTargetView(View target) { if (getParent() != null) { ((ViewGroup) getParent()).removeView(this); } if (target == null) { return; } if (target.getParent() instanceof FrameLayout) { ((FrameLayout) target.getParent()).addView(this); } else if (target.getParent() instanceof ViewGroup) { // use a new Framelayout container for adding badge ViewGroup parentContainer = (ViewGroup) target.getParent(); int groupIndex = parentContainer.indexOfChild(target); parentContainer.removeView(target); FrameLayout badgeContainer = new FrameLayout(getContext()); ViewGroup.LayoutParams parentLayoutParams = target.getLayoutParams(); badgeContainer.setLayoutParams(parentLayoutParams); target.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams); badgeContainer.addView(target); badgeContainer.addView(this); } } /** * converts dip to px */ private int dip2Px(float dip) { return (int) (dip * getContext().getResources().getDisplayMetrics().density + 0.5f); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/HorizontalListView.java ================================================ package com.kunfei.bookshelf.widget; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Rect; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.view.View; import android.widget.AdapterView; import android.widget.ListAdapter; import android.widget.Scroller; import java.util.LinkedList; import java.util.Queue; public class HorizontalListView extends AdapterView { public boolean mAlwaysOverrideTouch = true; protected ListAdapter mAdapter; private int mLeftViewIndex = -1; private int mRightViewIndex = 0; protected int mCurrentX; protected int mNextX; private int mMaxX = Integer.MAX_VALUE; private int mDisplayOffset = 0; protected Scroller mScroller; private GestureDetector mGesture; private Queue mRemovedViewQueue = new LinkedList(); private OnItemSelectedListener mOnItemSelected; private OnItemClickListener mOnItemClicked; private OnItemLongClickListener mOnItemLongClicked; private boolean mDataChanged = false; public HorizontalListView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private synchronized void initView() { mLeftViewIndex = -1; mRightViewIndex = 0; mDisplayOffset = 0; mCurrentX = 0; mNextX = 0; mMaxX = Integer.MAX_VALUE; mScroller = new Scroller(getContext()); mGesture = new GestureDetector(getContext(), mOnGesture); } @Override public void setOnItemSelectedListener(OnItemSelectedListener listener) { mOnItemSelected = listener; } @Override public void setOnItemClickListener(OnItemClickListener listener){ mOnItemClicked = listener; } @Override public void setOnItemLongClickListener(OnItemLongClickListener listener) { mOnItemLongClicked = listener; } private DataSetObserver mDataObserver = new DataSetObserver() { @Override public void onChanged() { synchronized(HorizontalListView.this){ mDataChanged = true; } invalidate(); requestLayout(); } @Override public void onInvalidated() { reset(); invalidate(); requestLayout(); } }; public boolean onInterceptTouchEvent(MotionEvent ev) { getParent().requestDisallowInterceptTouchEvent(true); return mGesture.onTouchEvent(ev); }; @Override public ListAdapter getAdapter() { return mAdapter; } @Override public View getSelectedView() { return null; } @Override public void setAdapter(ListAdapter adapter) { if(mAdapter != null) { mAdapter.unregisterDataSetObserver(mDataObserver); } mAdapter = adapter; mAdapter.registerDataSetObserver(mDataObserver); reset(); } private synchronized void reset(){ initView(); removeAllViewsInLayout(); requestLayout(); } @Override public void setSelection(int position) { } private void addAndMeasureChild(final View child, int viewPos) { LayoutParams params = child.getLayoutParams(); if(params == null) { params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); } addViewInLayout(child, viewPos, params, true); child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); } @Override protected synchronized void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if(mAdapter == null){ return; } if(mDataChanged){ int oldCurrentX = mCurrentX; initView(); removeAllViewsInLayout(); mNextX = oldCurrentX; mDataChanged = false; } if(mScroller.computeScrollOffset()){ mNextX = mScroller.getCurrX(); } if(mNextX <= 0){ mNextX = 0; mScroller.forceFinished(true); } if(mNextX >= mMaxX) { mNextX = mMaxX; mScroller.forceFinished(true); } int dx = mCurrentX - mNextX; removeNonVisibleItems(dx); fillList(dx); positionItems(dx); mCurrentX = mNextX; if(!mScroller.isFinished()){ post(new Runnable(){ @Override public void run() { requestLayout(); } }); } } private void fillList(final int dx) { int edge = 0; View child = getChildAt(getChildCount()-1); if(child != null) { edge = child.getRight(); } fillListRight(edge, dx); edge = 0; child = getChildAt(0); if(child != null) { edge = child.getLeft(); } fillListLeft(edge, dx); } private void fillListRight(int rightEdge, final int dx) { while(rightEdge + dx < getWidth() && mRightViewIndex < mAdapter.getCount()) { View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this); addAndMeasureChild(child, -1); rightEdge += child.getMeasuredWidth(); if(mRightViewIndex == mAdapter.getCount()-1) { mMaxX = mCurrentX + rightEdge - getWidth(); } if (mMaxX < 0) { mMaxX = 0; } mRightViewIndex++; } } private void fillListLeft(int leftEdge, final int dx) { while(leftEdge + dx > 0 && mLeftViewIndex >= 0) { View child = mAdapter.getView(mLeftViewIndex, mRemovedViewQueue.poll(), this); addAndMeasureChild(child, 0); leftEdge -= child.getMeasuredWidth(); mLeftViewIndex--; mDisplayOffset -= child.getMeasuredWidth(); } } private void removeNonVisibleItems(final int dx) { View child = getChildAt(0); while(child != null && child.getRight() + dx <= 0) { mDisplayOffset += child.getMeasuredWidth(); mRemovedViewQueue.offer(child); removeViewInLayout(child); mLeftViewIndex++; child = getChildAt(0); } child = getChildAt(getChildCount()-1); while(child != null && child.getLeft() + dx >= getWidth()) { mRemovedViewQueue.offer(child); removeViewInLayout(child); mRightViewIndex--; child = getChildAt(getChildCount()-1); } } private void positionItems(final int dx) { if(getChildCount() > 0){ mDisplayOffset += dx; int left = mDisplayOffset; for(int i=0;i 360) { topDegree = topDegree - 360; } if (bottomDegree > 360) { bottomDegree = bottomDegree - 360; } if (changeBigger) { if (arc < 160) { arc += speedOfArc; invalidate(); } } else { if (arc > speedOfDegree) { arc -= 2 * speedOfArc; invalidate(); } } if (arc >= 160 || arc <= 10) { changeBigger = !changeBigger; invalidate(); } } public void setLoadingColor(int color) { this.color = color; } public int getLoadingColor() { return color; } public void start() { startAnimator(); isStart = true; invalidate(); } public void stop() { stopAnimator(); invalidate(); } public boolean isStart() { return isStart; } private void startAnimator() { ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 0.0f, 1); ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 0.0f, 1); scaleXAnimator.setDuration(300); scaleXAnimator.setInterpolator(new LinearInterpolator()); scaleYAnimator.setDuration(300); scaleYAnimator.setInterpolator(new LinearInterpolator()); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(scaleXAnimator, scaleYAnimator); animatorSet.start(); } private void stopAnimator() { ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 1, 0); ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 1, 0); scaleXAnimator.setDuration(300); scaleXAnimator.setInterpolator(new LinearInterpolator()); scaleYAnimator.setDuration(300); scaleYAnimator.setInterpolator(new LinearInterpolator()); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(scaleXAnimator, scaleYAnimator); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { isStart = false; } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorSet.start(); } public int dpToPx(Context context, float dpVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics()); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/ScrollTextView.java ================================================ package com.kunfei.bookshelf.widget; import android.annotation.SuppressLint; import android.content.Context; import android.text.Layout; import android.util.AttributeSet; import android.view.MotionEvent; import androidx.appcompat.widget.AppCompatTextView; public class ScrollTextView extends AppCompatTextView { //滑动距离的最大边界 private int mOffsetHeight; //是否到顶或者到底的标志 private boolean mBottomFlag = false; public ScrollTextView(Context context) { super(context); } public ScrollTextView(Context context, AttributeSet attrs) { super(context, attrs); } public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); initOffsetHeight(); } @Override protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { super.onTextChanged(text, start, lengthBefore, lengthAfter); initOffsetHeight(); } private void initOffsetHeight() { int paddingTop; int paddingBottom; int mHeight; int mLayoutHeight; //获得内容面板 Layout mLayout = getLayout(); if (mLayout == null) return; //获得内容面板的高度 mLayoutHeight = mLayout.getHeight(); //获取上内边距 paddingTop = getTotalPaddingTop(); //获取下内边距 paddingBottom = getTotalPaddingBottom(); //获得控件的实际高度 mHeight = getMeasuredHeight(); //计算滑动距离的边界 mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight; if (mOffsetHeight <= 0) { scrollTo(0, 0); } } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { //如果是新的按下事件,则对mBottomFlag重新初始化 mBottomFlag = mOffsetHeight <= 0; } //如果已经不要这次事件,则传出取消的信号,这里的作用不大 if (mBottomFlag) { event.setAction(MotionEvent.ACTION_CANCEL); } return super.dispatchTouchEvent(event); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { boolean result = super.onTouchEvent(event); //如果是需要拦截,则再拦截,这个方法会在onScrollChanged方法之后再调用一次 if (!mBottomFlag) getParent().requestDisallowInterceptTouchEvent(true); return result; } @Override protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) { super.onScrollChanged(horiz, vert, oldHoriz, oldVert); if (vert == mOffsetHeight || vert == 0) { //这里触发父布局或祖父布局的滑动事件 getParent().requestDisallowInterceptTouchEvent(false); mBottomFlag = true; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/check_box/SmoothCheckBox.java ================================================ package com.kunfei.bookshelf.widget.check_box; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.animation.LinearInterpolator; import android.widget.Checkable; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.DensityUtil; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class SmoothCheckBox extends View implements Checkable { private static final int DEF_DRAW_SIZE = 25; private static final int DEF_ANIM_DURATION = 300; private Paint mPaint, mTickPaint, mFloorPaint; private Point[] mTickPoints; private Point mCenterPoint; private Path mTickPath; private float mLeftLineDistance, mRightLineDistance, mDrewDistance; private float mScaleVal = 1.0f, mFloorScale = 1.0f; private int mWidth, mAnimDuration, mStrokeWidth; private int mCheckedColor, mUnCheckedColor, mFloorColor, mFloorUnCheckedColor; private boolean mChecked; private boolean mTickDrawing; private OnCheckedChangeListener mListener; public SmoothCheckBox(Context context) { this(context, null); } public SmoothCheckBox(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SmoothCheckBox(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public SmoothCheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } private static int getGradientColor(int startColor, int endColor, float percent) { int startA = Color.alpha(startColor); int startR = Color.red(startColor); int startG = Color.green(startColor); int startB = Color.blue(startColor); int endA = Color.alpha(endColor); int endR = Color.red(endColor); int endG = Color.green(endColor); int endB = Color.blue(endColor); int currentA = (int) (startA * (1 - percent) + endA * percent); int currentR = (int) (startR * (1 - percent) + endR * percent); int currentG = (int) (startG * (1 - percent) + endG * percent); int currentB = (int) (startB * (1 - percent) + endB * percent); return Color.argb(currentA, currentR, currentG, currentB); } private void init(Context context, AttributeSet attrs) { TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox); int tickColor = ThemeStore.accentColor(context); mCheckedColor = context.getResources().getColor(R.color.background_card); mUnCheckedColor = context.getResources().getColor(R.color.background_menu); mFloorColor = context.getResources().getColor(R.color.transparent30); tickColor = ta.getColor(R.styleable.SmoothCheckBox_color_tick, tickColor); mAnimDuration = ta.getInt(R.styleable.SmoothCheckBox_duration, DEF_ANIM_DURATION); mFloorColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked_stroke, mFloorColor); mCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_checked, mCheckedColor); mUnCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked, mUnCheckedColor); mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SmoothCheckBox_stroke_width, DensityUtil.dp2px(getContext(), 0)); ta.recycle(); mFloorUnCheckedColor = mFloorColor; mTickPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTickPaint.setStyle(Paint.Style.STROKE); mTickPaint.setStrokeCap(Paint.Cap.ROUND); mTickPaint.setColor(tickColor); mFloorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFloorPaint.setStyle(Paint.Style.FILL); mFloorPaint.setColor(mFloorColor); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mCheckedColor); mTickPath = new Path(); mCenterPoint = new Point(); mTickPoints = new Point[3]; mTickPoints[0] = new Point(); mTickPoints[1] = new Point(); mTickPoints[2] = new Point(); setOnClickListener(v -> { toggle(); mTickDrawing = false; mDrewDistance = 0; if (isChecked()) { startCheckedAnimation(); } else { startUnCheckedAnimation(); } }); } @Override public boolean isChecked() { return mChecked; } @Override public void setChecked(boolean checked) { mChecked = checked; reset(); invalidate(); if (mListener != null) { mListener.onCheckedChanged(SmoothCheckBox.this, mChecked); } } @Override public void toggle() { this.setChecked(!isChecked()); } /** * checked with animation * * @param checked checked * @param animate change with animation */ public void setChecked(boolean checked, boolean animate) { if (animate) { mTickDrawing = false; mChecked = checked; mDrewDistance = 0f; if (checked) { startCheckedAnimation(); } else { startUnCheckedAnimation(); } if (mListener != null) { mListener.onCheckedChanged(SmoothCheckBox.this, mChecked); } } else { this.setChecked(checked); } } private void reset() { mTickDrawing = true; mFloorScale = 1.0f; mScaleVal = isChecked() ? 0f : 1.0f; mFloorColor = isChecked() ? mCheckedColor : mFloorUnCheckedColor; mDrewDistance = isChecked() ? (mLeftLineDistance + mRightLineDistance) : 0; } private int measureSize(int measureSpec) { int defSize = DensityUtil.dp2px(getContext(), DEF_DRAW_SIZE); int specSize = MeasureSpec.getSize(measureSpec); int specMode = MeasureSpec.getMode(measureSpec); int result = 0; switch (specMode) { case MeasureSpec.UNSPECIFIED: case MeasureSpec.AT_MOST: result = Math.min(defSize, specSize); break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { mWidth = getMeasuredWidth(); mStrokeWidth = (mStrokeWidth == 0 ? getMeasuredWidth() / 10 : mStrokeWidth); mStrokeWidth = mStrokeWidth > getMeasuredWidth() / 5 ? getMeasuredWidth() / 5 : mStrokeWidth; mStrokeWidth = (mStrokeWidth < 3) ? 3 : mStrokeWidth; mCenterPoint.x = mWidth / 2; mCenterPoint.y = getMeasuredHeight() / 2; mTickPoints[0].x = Math.round((float) getMeasuredWidth() / 30 * 7); mTickPoints[0].y = Math.round((float) getMeasuredHeight() / 30 * 14); mTickPoints[1].x = Math.round((float) getMeasuredWidth() / 30 * 13); mTickPoints[1].y = Math.round((float) getMeasuredHeight() / 30 * 20); mTickPoints[2].x = Math.round((float) getMeasuredWidth() / 30 * 22); mTickPoints[2].y = Math.round((float) getMeasuredHeight() / 30 * 10); mLeftLineDistance = (float) Math.sqrt(Math.pow(mTickPoints[1].x - mTickPoints[0].x, 2) + Math.pow(mTickPoints[1].y - mTickPoints[0].y, 2)); mRightLineDistance = (float) Math.sqrt(Math.pow(mTickPoints[2].x - mTickPoints[1].x, 2) + Math.pow(mTickPoints[2].y - mTickPoints[1].y, 2)); mTickPaint.setStrokeWidth(mStrokeWidth); } @Override protected void onDraw(Canvas canvas) { drawBorder(canvas); drawCenter(canvas); drawTick(canvas); } private void drawCenter(Canvas canvas) { mPaint.setColor(mUnCheckedColor); float radius = (mCenterPoint.x - mStrokeWidth) * mScaleVal; canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, radius, mPaint); } private void drawBorder(Canvas canvas) { mFloorPaint.setColor(mFloorColor); int radius = mCenterPoint.x; canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, radius * mFloorScale, mFloorPaint); } private void drawTick(Canvas canvas) { if (mTickDrawing && isChecked()) { drawTickPath(canvas); } } private void drawTickPath(Canvas canvas) { mTickPath.reset(); // draw left of the tick if (mDrewDistance < mLeftLineDistance) { float step = (mWidth / 20.0f) < 3 ? 3 : (mWidth / 20.0f); mDrewDistance += step; float stopX = mTickPoints[0].x + (mTickPoints[1].x - mTickPoints[0].x) * mDrewDistance / mLeftLineDistance; float stopY = mTickPoints[0].y + (mTickPoints[1].y - mTickPoints[0].y) * mDrewDistance / mLeftLineDistance; mTickPath.moveTo(mTickPoints[0].x, mTickPoints[0].y); mTickPath.lineTo(stopX, stopY); canvas.drawPath(mTickPath, mTickPaint); if (mDrewDistance > mLeftLineDistance) { mDrewDistance = mLeftLineDistance; } } else { mTickPath.moveTo(mTickPoints[0].x, mTickPoints[0].y); mTickPath.lineTo(mTickPoints[1].x, mTickPoints[1].y); canvas.drawPath(mTickPath, mTickPaint); // draw right of the tick if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { float stopX = mTickPoints[1].x + (mTickPoints[2].x - mTickPoints[1].x) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance; float stopY = mTickPoints[1].y - (mTickPoints[1].y - mTickPoints[2].y) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance; mTickPath.reset(); mTickPath.moveTo(mTickPoints[1].x, mTickPoints[1].y); mTickPath.lineTo(stopX, stopY); canvas.drawPath(mTickPath, mTickPaint); float step = (mWidth / 20f) < 3 ? 3 : (mWidth / 20f); mDrewDistance += step; } else { mTickPath.reset(); mTickPath.moveTo(mTickPoints[1].x, mTickPoints[1].y); mTickPath.lineTo(mTickPoints[2].x, mTickPoints[2].y); canvas.drawPath(mTickPath, mTickPaint); } } // invalidate if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { postDelayed(this::postInvalidate, 10); } } private void startCheckedAnimation() { ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0f); animator.setDuration(mAnimDuration / 3 * 2); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(animation -> { mScaleVal = (float) animation.getAnimatedValue(); mFloorColor = getGradientColor(mUnCheckedColor, mCheckedColor, 1 - mScaleVal); postInvalidate(); }); animator.start(); ValueAnimator floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f); floorAnimator.setDuration(mAnimDuration); floorAnimator.setInterpolator(new LinearInterpolator()); floorAnimator.addUpdateListener(animation -> { mFloorScale = (float) animation.getAnimatedValue(); postInvalidate(); }); floorAnimator.start(); drawTickDelayed(); } private void startUnCheckedAnimation() { ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f); animator.setDuration(mAnimDuration); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(animation -> { mScaleVal = (float) animation.getAnimatedValue(); mFloorColor = getGradientColor(mCheckedColor, mFloorUnCheckedColor, mScaleVal); postInvalidate(); }); animator.start(); ValueAnimator floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f); floorAnimator.setDuration(mAnimDuration); floorAnimator.setInterpolator(new LinearInterpolator()); floorAnimator.addUpdateListener(animation -> { mFloorScale = (float) animation.getAnimatedValue(); postInvalidate(); }); floorAnimator.start(); } private void drawTickDelayed() { postDelayed(() -> { mTickDrawing = true; postInvalidate(); }, mAnimDuration); } public void setOnCheckedChangeListener(OnCheckedChangeListener l) { this.mListener = l; } public interface OnCheckedChangeListener { void onCheckedChanged(SmoothCheckBox checkBox, boolean isChecked); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/ExplosionAnimator.java ================================================ /* * Copyright (C) 2015 tyrantgit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kunfei.bookshelf.widget.explosion_field; import android.animation.ValueAnimator; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import java.util.Random; public class ExplosionAnimator extends ValueAnimator { static long DEFAULT_DURATION = 0x400; private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(0.6f); private static final float END_VALUE = 1.4f; private static final float X = Utils.dp2Px(5); private static final float Y = Utils.dp2Px(20); private static final float V = Utils.dp2Px(2); private static final float W = Utils.dp2Px(1); private Paint mPaint; private Particle[] mParticles; private Rect mBound; private View mContainer; public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) { mPaint = new Paint(); mBound = new Rect(bound); int partLen = 15; mParticles = new Particle[partLen * partLen]; Random random = new Random(System.currentTimeMillis()); int w = bitmap.getWidth() / (partLen + 2); int h = bitmap.getHeight() / (partLen + 2); for (int i = 0; i < partLen; i++) { for (int j = 0; j < partLen; j++) { mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random); } } mContainer = container; setFloatValues(0f, END_VALUE); setInterpolator(DEFAULT_INTERPOLATOR); setDuration(DEFAULT_DURATION); } private Particle generateParticle(int color, Random random) { Particle particle = new Particle(); particle.color = color; particle.radius = V; if (random.nextFloat() < 0.2f) { particle.baseRadius = V + ((X - V) * random.nextFloat()); } else { particle.baseRadius = W + ((V - W) * random.nextFloat()); } float nextFloat = random.nextFloat(); particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f); particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat()); particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f; float f = nextFloat < 0.2f ? particle.bottom : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f; particle.bottom = f; particle.mag = 4.0f * particle.top / particle.bottom; particle.neg = (-particle.mag) / particle.bottom; f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f)); particle.baseCx = f; particle.cx = f; f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f)); particle.baseCy = f; particle.cy = f; particle.life = END_VALUE / 10 * random.nextFloat(); particle.overflow = 0.4f * random.nextFloat(); particle.alpha = 1f; return particle; } public boolean draw(Canvas canvas) { if (!isStarted()) { return false; } for (Particle particle : mParticles) { particle.advance((float) getAnimatedValue()); if (particle.alpha > 0f) { mPaint.setColor(particle.color); mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha)); canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint); } } mContainer.invalidate(); return true; } @Override public void start() { super.start(); mContainer.invalidate(mBound); } private class Particle { float alpha; int color; float cx; float cy; float radius; float baseCx; float baseCy; float baseRadius; float top; float bottom; float mag; float neg; float life; float overflow; public void advance(float factor) { float f = 0f; float normalization = factor / END_VALUE; if (normalization < life || normalization > 1f - overflow) { alpha = 0f; return; } normalization = (normalization - life) / (1f - life - overflow); float f2 = normalization * END_VALUE; if (normalization >= 0.7f) { f = (normalization - 0.7f) / 0.3f; } alpha = 1f - f; f = bottom * f2; cx = baseCx + f; cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag; radius = V + (baseRadius - V) * f2; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/ExplosionField.java ================================================ /* * Copyright (C) 2015 tyrantgit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kunfei.bookshelf.widget.explosion_field; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.media.MediaPlayer; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; public class ExplosionField extends View { private long customDuration = ExplosionAnimator.DEFAULT_DURATION; private int idPlayAnimationEffect = 0; private OnAnimatorListener mZAnimatorListener; private OnClickListener mOnClickListener; private List mExplosions = new ArrayList<>(); private int[] mExpandInset = new int[2]; public ExplosionField(Context context) { super(context); init(); } public ExplosionField(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ExplosionField(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { Arrays.fill(mExpandInset, Utils.dp2Px(32)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (ExplosionAnimator explosion : mExplosions) { explosion.draw(canvas); } } public void playSoundAnimationEffect(int id) { this.idPlayAnimationEffect = id; } public void setCustomDuration(long customDuration) { this.customDuration = customDuration; } public void addActionEvent(OnAnimatorListener ievents) { this.mZAnimatorListener = ievents; } public void expandExplosionBound(int dx, int dy) { mExpandInset[0] = dx; mExpandInset[1] = dy; } public void explode(Bitmap bitmap, Rect bound, long startDelay) { explode(bitmap, bound, startDelay, null); } public void explode(Bitmap bitmap, Rect bound, long startDelay, final View view) { long currentDuration = customDuration; final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound); explosion.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mExplosions.remove(animation); if (view != null) { view.setScaleX(1); view.setScaleY(1); view.setAlpha(1); view.setOnClickListener(mOnClickListener);//set event } } }); explosion.setStartDelay(startDelay); explosion.setDuration(currentDuration); mExplosions.add(explosion); explosion.start(); } public void explode(View view) { explode(view, false); } public void explode(final View view, Boolean restartState) { Rect r = new Rect(); view.getGlobalVisibleRect(r); int[] location = new int[2]; getLocationOnScreen(location); // getLocationInWindow(location); // view.getLocationInWindow(location); r.offset(-location[0], -location[1]); r.inset(-mExpandInset[0], -mExpandInset[1]); int startDelay = 100; ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { Random random = new Random(); @Override public void onAnimationUpdate(ValueAnimator animation) { view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f); view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { if (idPlayAnimationEffect != 0) MediaPlayer.create(getContext(), idPlayAnimationEffect).start(); } @Override public void onAnimationEnd(Animator animator) { if (mZAnimatorListener != null) { mZAnimatorListener.onAnimationEnd(animator, ExplosionField.this); } } @Override public void onAnimationCancel(Animator animator) { Log.i("PRUEBA", "CANCEL"); } @Override public void onAnimationRepeat(Animator animator) { Log.i("PRUEBA", "REPEAT"); } }); animator.start(); view.animate().setDuration(150).setStartDelay(startDelay).scaleX(0f).scaleY(0f).alpha(0f).start(); if (restartState) explode(Utils.createBitmapFromView(view), r, startDelay, view); else explode(Utils.createBitmapFromView(view), r, startDelay); } public void clear() { mExplosions.clear(); invalidate(); } public static ExplosionField attach2Window(Activity activity) { ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT); ExplosionField explosionField = new ExplosionField(activity); rootView.addView(explosionField, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return explosionField; } public void setOnClickListener(OnClickListener mOnClickListener) { this.mOnClickListener = mOnClickListener; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/OnAnimatorListener.java ================================================ package com.kunfei.bookshelf.widget.explosion_field; import android.animation.Animator; import android.view.View; public interface OnAnimatorListener { void onAnimationEnd(Animator animator, View view); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/Utils.java ================================================ /* * Copyright (C) 2015 tyrantgit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kunfei.bookshelf.widget.explosion_field; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.ImageView; public class Utils { private Utils() { } private static final float DENSITY = Resources.getSystem().getDisplayMetrics().density; private static final Canvas sCanvas = new Canvas(); public static int dp2Px(int dp) { return Math.round(dp * DENSITY); } public static Bitmap createBitmapFromView(View view) { if (view instanceof ImageView) { Drawable drawable = ((ImageView) view).getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } } view.clearFocus(); Bitmap bitmap = createBitmapSafely(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888, 1); if (bitmap != null) { synchronized (sCanvas) { Canvas canvas = sCanvas; canvas.setBitmap(bitmap); view.draw(canvas); canvas.setBitmap(null); } } return bitmap; } public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) { try { return Bitmap.createBitmap(width, height, config); } catch (OutOfMemoryError e) { e.printStackTrace(); if (retryCount > 0) { System.gc(); return createBitmapSafely(width, height, config, retryCount - 1); } return null; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/adapter/FileAdapter.java ================================================ package com.kunfei.bookshelf.widget.filepicker.adapter; import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.widget.filepicker.entity.FileItem; import com.kunfei.bookshelf.widget.filepicker.icons.FilePickerIcon; import com.kunfei.bookshelf.widget.filepicker.util.ConvertUtils; import com.kunfei.bookshelf.widget.filepicker.util.FileUtils; import java.io.File; import java.util.ArrayList; public class FileAdapter extends RecyclerView.Adapter { public static final String DIR_ROOT = "."; public static final String DIR_PARENT = ".."; private ArrayList data = new ArrayList<>(); private String rootPath = null; private String currentPath = null; private String[] allowExtensions = null;//允许的扩展名 private boolean onlyListDir = false;//是否仅仅读取目录 private boolean showHomeDir = false;//是否显示返回主目录 private boolean showUpDir = true;//是否显示返回上一级 private boolean showHideDir = true;//是否显示隐藏的目录(以“.”开头) private int itemHeight = 40;// dp private Drawable homeIcon = null; private Drawable upIcon = null; private Drawable folderIcon = null; private Drawable fileIcon = null; private CallBack callBack; public void setCallBack(CallBack callBack) { this.callBack = callBack; } public FileItem getItem(int pos) { return data.get(pos); } public String getCurrentPath() { return currentPath; } public void setFileIcon(Drawable fileIcon) { this.fileIcon = fileIcon; } public void setFolderIcon(Drawable folderIcon) { this.folderIcon = folderIcon; } public void setHomeIcon(Drawable homeIcon) { this.homeIcon = homeIcon; } public void setUpIcon(Drawable upIcon) { this.upIcon = upIcon; } /** * 允许的扩展名 */ public void setAllowExtensions(String[] allowExtensions) { this.allowExtensions = allowExtensions; } /** * 是否仅仅读取目录 */ public void setOnlyListDir(boolean onlyListDir) { this.onlyListDir = onlyListDir; } public boolean isOnlyListDir() { return onlyListDir; } /** * 是否显示返回主目录 */ public void setShowHomeDir(boolean showHomeDir) { this.showHomeDir = showHomeDir; } public boolean isShowHomeDir() { return showHomeDir; } /** * 是否显示返回上一级 */ public void setShowUpDir(boolean showUpDir) { this.showUpDir = showUpDir; } public boolean isShowUpDir() { return showUpDir; } /** * 是否显示隐藏的目录(以“.”开头) */ public void setShowHideDir(boolean showHideDir) { this.showHideDir = showHideDir; } public boolean isShowHideDir() { return showHideDir; } public void setItemHeight(int itemHeight) { this.itemHeight = itemHeight; } public void loadData(String path) { if (path == null) { return; } if (homeIcon == null) { homeIcon = ConvertUtils.toDrawable(FilePickerIcon.getHOME()); } if (upIcon == null) { upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUPDIR()); } if (folderIcon == null) { folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFOLDER()); } if (fileIcon == null) { fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFILE()); } ArrayList datas = new ArrayList(); if (rootPath == null) { rootPath = path; } currentPath = path; if (showHomeDir) { //添加“返回主目录” FileItem fileRoot = new FileItem(); fileRoot.setDirectory(true); fileRoot.setIcon(homeIcon); fileRoot.setName(DIR_ROOT); fileRoot.setSize(0); fileRoot.setPath(rootPath); datas.add(fileRoot); } if (showUpDir && !path.equals("/")) { //添加“返回上一级目录” FileItem fileParent = new FileItem(); fileParent.setDirectory(true); fileParent.setIcon(upIcon); fileParent.setName(DIR_PARENT); fileParent.setSize(0); fileParent.setPath(new File(path).getParent()); datas.add(fileParent); } File[] files; if (allowExtensions == null) { if (onlyListDir) { files = FileUtils.listDirs(currentPath); } else { files = FileUtils.listDirsAndFiles(currentPath); } } else { if (onlyListDir) { files = FileUtils.listDirs(currentPath, allowExtensions); } else { files = FileUtils.listDirsAndFiles(currentPath, allowExtensions); } } if (files != null) { for (File file : files) { if (!showHideDir && file.getName().startsWith(".")) { continue; } FileItem fileItem = new FileItem(); boolean isDirectory = file.isDirectory(); fileItem.setDirectory(isDirectory); if (isDirectory) { fileItem.setIcon(folderIcon); fileItem.setSize(0); } else { fileItem.setIcon(fileIcon); fileItem.setSize(file.length()); } fileItem.setName(file.getName()); fileItem.setPath(file.getAbsolutePath()); datas.add(fileItem); } } data.clear(); data.addAll(datas); notifyDataSetChanged(); } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_file_filepicker, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) { FileItem fileItem = data.get(position); holder.imageView.setImageDrawable(fileItem.getIcon()); holder.textView.setText(fileItem.getName()); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (callBack != null) { callBack.onFileClick(position); } } }); } @Override public int getItemCount() { return data.size(); } class MyViewHolder extends RecyclerView.ViewHolder { ImageView imageView; TextView textView; MyViewHolder(@NonNull View itemView) { super(itemView); imageView = itemView.findViewById(R.id.image_view); textView = itemView.findViewById(R.id.text_view); } } public interface CallBack { void onFileClick(int position); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/adapter/PathAdapter.java ================================================ package com.kunfei.bookshelf.widget.filepicker.adapter; import android.graphics.drawable.Drawable; import android.os.Environment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.widget.filepicker.icons.FilePickerIcon; import com.kunfei.bookshelf.widget.filepicker.util.ConvertUtils; import java.util.Collections; import java.util.LinkedList; public class PathAdapter extends RecyclerView.Adapter { private static final String ROOT_HINT = "SD"; private LinkedList paths = new LinkedList<>(); private Drawable arrowIcon = null; private String sdCardDirectory = Environment.getExternalStorageDirectory().getAbsolutePath(); private CallBack callBack; public void setCallBack(CallBack callBack) { this.callBack = callBack; } public String getItem(int position) { StringBuilder tmp = new StringBuilder(sdCardDirectory + "/"); //忽略根目录 if (position == 0) { return tmp.toString(); } for (int i = 1; i <= position; i++) { tmp.append(paths.get(i)).append("/"); } return tmp.toString(); } public void setArrowIcon(Drawable arrowIcon) { this.arrowIcon = arrowIcon; } public void updatePath(String path) { path = path.replace(sdCardDirectory, ""); if (arrowIcon == null) { arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getARROW()); } paths.clear(); if (!path.equals("/") && !path.equals("")) { String[] tmps = path.substring(path.indexOf("/") + 1).split("/"); Collections.addAll(paths, tmps); } paths.addFirst(ROOT_HINT); notifyDataSetChanged(); } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_path_filepicker, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) { holder.textView.setText(paths.get(position)); holder.imageView.setImageDrawable(arrowIcon); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (callBack != null) { callBack.onPathClick(position); } } }); } @Override public int getItemCount() { return paths.size(); } class MyViewHolder extends RecyclerView.ViewHolder { ImageView imageView; TextView textView; MyViewHolder(@NonNull View itemView) { super(itemView); imageView = itemView.findViewById(R.id.image_view); textView = itemView.findViewById(R.id.text_view); } } public interface CallBack { void onPathClick(int position); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/drawable/StateBaseDrawable.java ================================================ package com.kunfei.bookshelf.widget.filepicker.drawable; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; /** * 按下状态与普通状态下显示不同的图片或颜色 *
    * Author:李玉江[QQ:1032694760] * DateTime:2017/01/01 05:30 * Builder:Android Studio */ public abstract class StateBaseDrawable extends StateListDrawable { protected void addState(Drawable pressed) { addState(new ColorDrawable(Color.TRANSPARENT), pressed); } protected void addState(Drawable normal, Drawable pressed) { addState(new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}, pressed); addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, pressed); addState(new int[]{android.R.attr.state_enabled}, normal); addState(new int[]{android.R.attr.state_focused}, pressed); addState(new int[]{android.R.attr.state_window_focused}, normal); addState(new int[]{}, normal); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/drawable/StateColorDrawable.java ================================================ package com.kunfei.bookshelf.widget.filepicker.drawable; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import androidx.annotation.ColorInt; /** * 按下状态与普通状态下显示不同的颜色 *
    * Author:李玉江[QQ:1032694760] * DateTime:2017/01/01 05:30 * Builder:Android Studio */ public class StateColorDrawable extends StateBaseDrawable { public StateColorDrawable(@ColorInt int pressedColor) { this(Color.TRANSPARENT, pressedColor); } public StateColorDrawable(@ColorInt int normalColor, @ColorInt int pressedColor) { addState(new ColorDrawable(normalColor), new ColorDrawable(pressedColor)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/entity/FileItem.java ================================================ package com.kunfei.bookshelf.widget.filepicker.entity; import android.graphics.drawable.Drawable; /** * 文件项信息 * * @author 李玉江[QQ:1032694760] * @since 2014-05-23 18:02 */ public class FileItem extends JavaBean { private Drawable icon; private String name; private String path = "/"; private long size = 0; private boolean isDirectory = false; public void setIcon(Drawable icon) { this.icon = icon; } public Drawable getIcon() { return icon; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public boolean isDirectory() { return isDirectory; } public void setDirectory(boolean isDirectory) { this.isDirectory = isDirectory; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/entity/JavaBean.java ================================================ package com.kunfei.bookshelf.widget.filepicker.entity; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; /** * JavaBean类 * * @author 李玉江[QQ:1032694760] * @since 2014-04-23 16:14 */ public class JavaBean implements Serializable { private static final long serialVersionUID = -6111323241670458039L; /** * 反射出所有字段值 */ @Override public String toString() { ArrayList list = new ArrayList<>(); Class clazz = getClass(); list.addAll(Arrays.asList(clazz.getDeclaredFields()));//得到自身的所有字段 StringBuilder sb = new StringBuilder(); while (clazz != Object.class) { clazz = clazz.getSuperclass();//得到继承自父类的字段 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { int modifier = field.getModifiers(); if (Modifier.isPublic(modifier) || Modifier.isProtected(modifier)) { list.add(field); } } } Field[] fields = list.toArray(new Field[0]); for (Field field : fields) { String fieldName = field.getName(); try { Object obj = field.get(this); sb.append(fieldName); sb.append("="); sb.append(obj); sb.append("\n"); } catch (IllegalAccessException ignored) { } } return sb.toString(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/icons/FilePickerIcon.java ================================================ package com.kunfei.bookshelf.widget.filepicker.icons; /** * Generated by https://github.com/gzu-liyujiang/Image2ByteVar * * @author 李玉江[QQ:1023694760] * @since 2017/01/04 06:03 */ public class FilePickerIcon { public static byte[] getFILE() { return FILE; } public static byte[] getFOLDER() { return FOLDER; } public static byte[] getHOME() { return HOME; } public static byte[] getUPDIR() { return UPDIR; } public static byte[] getARROW() { return ARROW; } // fixed: 17-1-7 "static final" arrays should be "private" private static final byte[] FILE = { -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, 0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71, 68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 1, -73, 73, 68, 65, 84, 88, -123, -19, -106, 63, 75, 3, 49, 24, -121, 127, -74, 69, -37, 110, -30, -94, -109, -120, -126, -125, -101, 56, 72, 63, -125, -97, -64, -63, 77, 28, 69, 20, 81, 68, 113, 83, 113, -12, 91, -120, -96, -117, -120, -85, 31, 65, 29, 28, -100, -124, -94, -101, 46, -83, -105, 92, 114, 113, -24, 31, -38, -110, 59, -34, -9, 114, 87, 69, -14, 64, 41, -92, 73, -34, -89, -55, -17, 114, 1, 60, 30, -113, -25, 79, 50, 66, -19, 120, -7, 16, 26, 0, -112, 82, -61, 24, 3, 41, 35, 0, -128, 20, 10, 74, 69, -48, 58, -126, 82, -90, -37, -90, -75, -127, 82, 81, 119, 124, 16, 40, -100, 109, 79, -109, -21, 13, 82, -94, 118, -108, 82, 99, -91, 54, -58, 25, 2, 0, 120, -85, 7, -72, -71, 127, -57, -18, -58, 12, -102, -51, 87, 115, 113, 56, -105, 74, -74, 64, -19, 104, -116, 73, 51, 63, 0, -96, 88, 104, -107, 57, -34, -102, -59, -6, -63, 75, -86, -119, -56, -94, -99, -83, -26, -96, 123, -122, 20, -37, -107, 78, -9, -25, -79, -74, -13, -52, -106, 37, -117, 114, -23, 72, 42, 109, -96, -93, 8, 66, 0, -97, 95, -83, -49, -55, -34, 2, 86, 55, 31, 89, -78, -12, -116, 10, -59, 18, -20, 48, 90, -86, -96, -47, -48, -72, -66, -5, 64, -40, -98, -94, 90, -27, -81, 15, 89, -76, -9, 9, -114, 99, 80, 18, 0, -90, 38, -127, -38, -46, 4, -76, 78, -97, 113, -128, 33, 42, -124, 78, -4, -35, 38, -87, -62, -42, -9, -14, -30, 120, 127, -69, 6, -82, 110, -21, -44, -46, 0, -72, 103, 77, 12, 73, -110, 125, 109, -55, -1, 53, 17, 86, 70, 109, 66, 113, -72, 72, -39, 32, -89, -38, 53, 99, -82, 100, -14, 48, -39, -74, 57, 107, -100, -49, -47, -84, -77, 24, 7, 121, 69, -125, -64, 126, -114, 82, -91, -66, 3, 106, 37, 59, -12, 21, 85, -46, -87, 80, -91, -20, 52, -100, 46, -38, -108, -87, 111, 104, -103, -64, 58, 71, -121, -15, -48, -60, -63, -54, -24, -80, -14, 104, 35, -73, -37, 83, -42, -112, 69, 5, -15, -10, -108, 23, -12, 3, 63, -92, -65, 63, 43, 101, -5, -10, 7, 14, -111, 112, -66, -108, -28, -111, 71, 27, -1, 47, -93, -65, 77, 38, -9, 81, 27, 46, 121, -76, -63, 18, 29, 86, 30, 109, -80, 68, -113, -50, -97, -14, -14, -16, 120, 60, -98, 1, 126, 0, -110, 81, -78, -5, 36, 19, -64, 3, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; private static final byte[] FOLDER = { -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, 0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71, 68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -102, 73, 68, 65, 84, 88, -123, -19, -105, 61, -113, 19, 49, 16, -122, 95, 111, -110, 75, -124, -124, -60, -49, 65, 20, -48, 34, 81, 80, 32, 81, 83, 80, -94, 52, 72, 8, 78, 66, -94, 64, 20, -108, 124, 20, 39, -47, 64, -51, -11, -7, 87, -96, 11, -38, -75, 61, 99, 15, -59, 110, 54, 118, -42, -5, 113, -71, 77, 117, -5, 54, -55, -50, -38, -42, -77, -81, 61, 99, 27, -104, 52, 105, -46, -92, 73, -73, 66, 106, 104, -61, -51, 57, -92, -21, -3, -29, 79, -61, -57, 58, 70, -13, 33, -115, 54, -25, -112, 71, -17, 127, 0, 0, -106, -38, -105, 65, 93, 64, -56, 0, 108, 33, 76, -72, -28, -113, -14, -20, -77, 59, 25, 108, 54, 4, -14, -63, -6, 59, -112, 123, -84, -4, -84, -114, -117, 39, 96, -74, -17, -2, -12, -59, 91, 92, -66, -103, 117, -70, 126, 19, 117, 58, -80, 57, -121, 60, 92, 127, 5, 0, -84, -106, -53, -3, 11, 93, -108, -96, 0, 96, 52, -124, 9, 96, 6, 116, -127, -33, -65, -66, -44, -51, 56, -117, -121, -73, 62, 3, -7, 56, -74, 123, 126, -11, -83, -24, 100, -23, -99, -6, -43, -67, -5, -119, -96, -119, -66, 80, 1, -128, 49, 0, -128, -25, 31, -98, -108, 65, 103, -93, 46, -62, -36, 24, -58, -27, -37, -6, -65, -39, -66, -108, -41, 63, -13, 86, -40, 65, 107, 84, -7, 43, 64, -103, 70, 92, 108, 17, -45, 82, -94, -115, -47, 77, -40, -22, 35, -108, 53, 32, 34, 56, 34, 112, -49, -14, 30, 4, 90, 66, -2, -119, 99, -34, 2, -69, -23, -33, -119, -10, -32, 18, -66, -37, -63, 114, 16, 99, -122, 2, -64, 87, 127, 97, -56, -61, -53, 24, -96, -121, -14, -107, 35, 103, 11, -120, -91, 116, 27, -25, -45, -15, 96, 9, 80, -95, 97, -56, -61, 114, 127, 14, -10, 102, 125, 27, 36, -128, 86, -56, -70, 34, -52, 50, -128, 109, -78, 77, 40, 118, -82, -73, 77, -65, -93, 98, 26, -128, -111, -62, 41, 62, -101, 67, 116, -111, 110, -41, 34, -53, 2, -26, 22, -9, 3, 29, 53, -11, -111, -109, -39, -94, -4, -83, 0, 85, -74, -120, -41, -25, 72, -22, 4, -27, -86, -58, -119, 45, -102, -119, 3, 52, -36, -124, 61, 40, 65, 81, -58, 55, -5, -117, 99, -80, 115, -89, 115, 52, 9, 93, 65, -90, -36, 76, 65, 94, 87, -67, -96, 74, -52, -2, 52, -46, 54, -91, -95, -109, -119, 108, 87, -13, -59, 126, -9, 74, 40, -109, 27, 58, 106, 124, 6, -79, 40, -117, -7, 33, 100, 0, 23, -71, -72, -37, -1, -85, 105, 31, -61, 77, 96, -24, -44, -109, 1, 40, -19, 70, 50, 113, -126, -75, -39, -22, -26, 53, -85, -61, 113, 89, 127, 8, 23, 78, 119, 80, 55, -5, -36, -12, 117, 34, -11, 23, -4, 78, 80, -14, 10, 112, 22, -30, 92, -5, -6, 28, 2, 25, -70, -103, 112, -46, -75, -19, 98, 67, 65, 57, 60, -110, -11, 13, 118, 4, -92, -9, 2, 79, 4, -94, 17, 118, 38, 97, 6, -104, 64, -108, -34, -103, -124, -35, -63, 115, -20, -68, -46, 58, 124, 2, 0, 56, 91, -18, 118, -123, -74, -48, 122, 88, -78, 117, -126, 90, 95, 102, -80, 49, 5, -88, 26, 80, -101, -46, 33, 83, -24, -42, 126, 53, 116, 42, 97, -104, -22, 2, -97, 111, 115, 108, -13, 17, 64, 1, -64, 85, 103, 71, 50, 84, 1, -106, 110, 88, 106, 95, 10, 93, -5, -67, -81, 62, -44, 22, 26, 84, 1, -33, -67, -77, -24, 5, -19, -67, -116, 93, -84, 87, 2, 32, -70, 66, -104, -32, -112, -53, 94, -63, -113, 112, 1, 125, 119, -15, -17, -92, -73, -40, 73, -109, 38, 77, -70, -19, -6, 15, -2, -54, -98, -96, -19, -118, -95, -10, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; private static final byte[] HOME = { -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, 0, 0, 0, 40, 8, 4, 0, 0, 0, 34, 2, -96, -37, 0, 0, 0, 1, 115, 82, 71, 66, 0, -82, -50, 28, -23, 0, 0, 0, 2, 98, 75, 71, 68, 0, -1, -121, -113, -52, -65, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1, 120, -91, 63, 118, 0, 0, 0, 7, 116, 73, 77, 69, 7, -37, 8, 4, 10, 36, 16, -22, -9, -18, -50, 0, 0, 2, -84, 73, 68, 65, 84, 72, -57, -19, -106, 91, 72, 20, 97, 20, -57, 127, -77, -18, -106, -41, 54, -14, -106, -23, -102, 4, 21, 97, 5, 5, 61, 70, 74, -76, -122, -94, -11, 18, -108, -92, 121, 41, 16, -124, 96, -95, 48, -118, -108, -94, 18, -70, 96, 33, 68, -81, -127, 15, 25, 68, 15, 21, 89, 80, 42, 25, 42, -91, 97, -106, -103, -122, 107, 41, -19, -86, -37, -22, 122, -39, 77, -73, -103, -81, 7, -105, 84, -36, -85, -26, -125, -44, -127, -31, 59, -52, -103, -7, 113, -50, -103, -17, -4, -65, -127, -1, -74, 44, 76, -102, 113, -29, 22, -119, 50, -3, -15, 84, 75, -111, -87, -38, -81, -89, 84, 24, -120, 1, -96, -106, -102, -65, 83, -66, -118, 59, -119, 39, 54, 97, 69, -31, -109, 115, -14, 8, 15, 124, -107, -17, 27, 42, 113, 123, 125, 81, 46, 102, 28, 8, -84, -68, 116, 78, -26, 80, 77, 5, -79, 104, -48, -48, 70, -39, 124, -88, -38, 103, 37, -107, -70, -94, 28, -6, 105, 7, 34, 89, 67, -86, -90, -74, 106, 82, -51, 65, 125, -110, 13, 27, -99, 43, 2, -17, -87, -60, -51, -124, -30, 99, -104, -88, -63, -12, 20, -93, -90, 96, 111, -80, -106, 20, 117, -3, -35, -97, -65, 54, -13, 29, 21, 8, -9, -3, -14, -122, -68, 17, 127, 50, 15, 51, -49, 49, 85, -109, 73, -79, 51, -81, 94, -103, 96, 21, 123, -126, 66, 86, 10, 64, 4, 12, -107, -72, -70, -50, -112, -57, 0, 47, -24, 123, 66, 46, 50, 80, -19, 40, 121, -59, 20, -31, -20, 102, 0, 121, 1, -48, -14, -72, 83, -7, 12, 81, -121, -79, -106, 67, 76, -71, -18, 94, -73, 85, 54, 34, 19, -122, 115, -102, 23, 16, -12, 114, 92, 73, 1, 22, 26, -24, 110, -26, 0, -114, 89, 17, -61, -32, -61, 22, 4, 2, -127, -30, -31, 125, -9, -48, -117, -79, 103, -13, -79, -48, 68, -57, 123, -46, 25, -101, 19, -109, 57, -38, -41, -40, -127, 64, 16, -127, 70, -113, -34, 63, 104, 105, -52, -7, 2, -84, -76, -48, -42, 69, 26, -42, 121, 113, 59, 89, 93, -35, -67, 8, -126, -39, -87, 81, -35, 103, -69, 111, -24, -71, -24, 11, -123, 12, -45, -58, -37, -81, -20, -61, -20, -74, 18, 11, -23, -19, -125, 102, 20, -76, 36, 107, 121, 76, -68, 119, -24, -103, -88, 75, -123, -116, -16, -111, 38, 51, 122, -66, 121, -4, -116, 95, 68, -42, 59, -5, 8, -126, 24, 54, -24, 120, 68, -124, 103, -24, -23, -88, -14, -29, -40, -24, -28, -75, 85, -92, -47, -27, 117, 48, -102, -27, -20, 86, 121, 28, 5, 29, 107, 119, 112, 111, -10, 24, 5, -51, -72, 17, -122, -32, 107, -39, -110, -99, 30, -22, -58, -108, -3, -76, -6, 20, -93, -49, -78, -59, -102, 17, -50, 40, -126, -47, -115, 66, 59, 94, -29, 78, 80, -86, -40, 74, -108, 20, -119, 34, 50, -88, -13, 83, 58, -81, -112, 73, 47, 70, -116, 52, -104, -34, 120, 86, 41, -119, 4, 10, 1, 24, -26, -106, 71, 88, 10, 41, 0, 60, -93, -47, 31, -107, 18, -124, -123, -106, -19, -30, 7, 31, 122, -68, 64, 83, 19, 75, -75, -12, 51, 108, -101, -127, -6, -40, -4, 97, -92, -110, -20, 67, 18, -109, -40, -58, 106, -113, -86, -66, 96, 83, -36, 15, -2, 34, -96, -110, 88, 8, 84, -8, 60, -37, 60, 67, -43, -18, -127, 2, 1, 26, -74, 120, -124, 70, 11, 20, -108, 64, -113, 104, -119, 88, -99, -24, 112, -105, -71, 112, 117, -44, 22, 88, -90, 32, 8, -27, 48, 33, 76, 119, 78, -52, 89, 101, 20, -100, -12, 49, 21, 24, 84, 97, 12, 59, 19, 46, -44, -36, 75, 65, 70, 70, 118, -83, -2, 67, 101, -21, 80, -123, -65, -69, -64, -79, -68, 127, -48, -106, 4, -6, -113, -37, 111, 38, -57, 11, 112, 71, 102, 113, -50, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; private static final byte[] UPDIR = { -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, 0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71, 68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -111, 73, 68, 65, 84, 88, -123, -19, -106, -51, 74, -21, 64, 20, -57, -1, -87, -47, 7, -15, 41, 92, 118, -47, 82, 10, -59, 110, 20, -95, 32, 77, -101, 44, -92, -37, -66, 80, -23, 3, 8, -126, -76, -48, -115, -120, 43, 117, -19, -109, -44, -113, -50, -57, -103, 115, 87, 103, 76, 106, -117, 77, 19, -17, -107, 75, -2, 48, -52, -112, 100, 38, -65, 57, -25, -52, 57, 3, 84, -86, 84, -87, -46, -1, -83, 36, 73, 56, 73, 18, -2, -87, -11, -61, 50, 22, -119, -29, -104, 47, 47, 47, 97, -116, -127, -75, -106, 39, -109, 73, 80, -58, -70, 105, 21, 94, 48, -114, 99, 78, -110, 4, 68, 4, -91, 20, -116, 49, 120, 121, 121, -63, 120, 60, 46, 21, -74, 86, 100, -14, 112, 56, -28, -85, -85, 43, 0, -128, -75, 22, -50, 57, 48, 51, -114, -113, -113, -47, -21, -11, 74, 13, -125, -67, 65, -121, -61, 33, -113, 70, 35, 4, 65, 0, 99, -116, 111, -42, 90, 16, 17, 58, -99, 14, 46, 46, 46, 74, -125, -35, 11, 84, 44, -23, -100, -61, -57, -57, 7, 86, -85, 21, -116, 49, 32, 34, 111, 89, 107, 45, 90, -83, 22, -50, -50, -50, 74, -127, -51, 125, -104, 6, -125, 1, -57, 113, -20, -83, -89, -108, -126, -75, 22, 0, -32, -100, -13, 45, 8, 2, 48, 51, -102, -51, 38, -100, 115, 124, 125, 125, 93, 40, 102, 115, -127, 14, 6, 3, -114, -94, -56, -69, 87, 107, 13, 34, 2, 0, -1, 44, 8, -78, 60, 68, -124, 70, -93, 1, 34, -30, -101, -101, -101, -67, 97, 119, 6, -115, -94, -120, 123, -67, -98, -121, 19, 43, 10, -88, -12, -58, 24, 56, -25, -4, 55, -78, -127, 122, -67, 14, 34, -30, -37, -37, -37, -67, 96, 119, -102, 20, 69, 17, 119, -69, 93, 15, 98, -83, -59, -63, -63, 1, 0, 32, 12, -61, 12, -88, -124, -61, -37, -37, 27, -34, -33, -33, -3, -122, 68, 15, 15, 15, -104, -49, -25, -71, 97, -65, -99, -48, -17, -9, -71, -35, 110, -5, 83, 45, 58, 58, 58, -14, -112, 34, -79, -30, -21, -21, 43, -106, -53, 37, -76, -42, 0, 0, -26, -49, -13, 20, -122, 33, -18, -17, -17, -79, 88, 44, 114, -63, 126, -21, 122, 102, -58, 108, 54, -13, 22, 19, 32, 0, 56, 61, 61, -3, -14, 76, 107, -115, -43, 106, 5, 34, -62, -45, -45, 83, -26, -35, -6, -72, 84, -48, -23, 116, -70, 113, -25, -25, -25, -25, -20, -100, -53, -4, 60, 29, -109, 114, -6, 103, -77, 89, 41, 21, 106, -17, 90, 47, -112, 2, 8, -64, 3, -118, -85, -45, -33, 20, -43, -34, -96, -52, -100, 57, -35, -64, 103, -116, -82, 3, -1, 83, 80, -87, 62, 2, -72, -98, 79, 5, -74, 44, -107, 2, 42, 112, 0, 50, -15, -7, 43, 64, -103, 121, 35, -24, -81, 118, -67, -28, -41, -76, -53, -91, -43, -21, 117, -106, 119, -23, 94, -58, -113, -113, -113, 59, 101, -123, -62, -96, -101, -54, -87, 88, -14, -28, -28, 4, -121, -121, -121, 25, 48, -71, 35, 40, -91, -16, -4, -4, -68, -13, -1, 10, -71, 62, 125, -75, 91, 7, -83, -43, 106, 30, 74, 42, -104, -28, 89, -83, 53, -76, -42, -71, 98, -72, -112, 69, -91, 68, 110, -69, -96, -92, 123, -7, 70, 41, 5, -91, 84, -18, 10, 85, 10, 104, 58, 70, -45, 125, -6, -80, 73, 47, 22, -51, -85, 66, -82, 87, 74, 1, -128, -17, 55, -127, 109, 2, -1, -85, -96, 98, -47, -76, 91, -73, 1, -82, -113, 1, -8, 107, -30, -113, -125, -34, -35, -35, 5, 68, -28, 19, -27, -74, -12, 83, 102, -46, -81, 84, -87, 82, -91, 95, -88, 63, 49, -122, -88, 68, 127, -55, -90, 73, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; private static final byte[] ARROW = {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 48, 0, 0, 0, 117, 8, 3, 0, 0, 0, 63, 73, -110, 106, 0, 0, 2, -9, 80, 76, 84, 69, -46, -46, -46, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -65, -65, -65, 0, 0, 0, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -54, -54, -54, -48, -48, -48, -48, -48, -48, -47, -47, -47, -47, -47, -47, -49, -49, -49, -86, -86, -86, -48, -48, -48, -48, -48, -48, -47, -47, -47, -52, -52, -52, -49, -49, -49, -48, -48, -48, -48, -48, -48, -1, -1, -1, -49, -49, -49, -48, -48, -48, -48, -48, -48, -51, -51, -51, -50, -50, -50, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52, -52, -52, -48, -48, -48, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47, -52, -52, -52, -48, -48, -48, -48, -48, -48, -1, -1, -1, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -41, -41, -41, -48, -48, -48, -49, -49, -49, -43, -43, -43, -47, -47, -47, -49, -49, -49, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49, -47, -47, -47, -49, -49, -49, -47, -47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49, -47, -47, -47, -44, -44, -44, -48, -48, -48, -48, -48, -48, -1, -1, -1, -46, -46, -46, -48, -48, -48, -49, -49, -49, -47, -47, -47, -49, -49, -49, -35, -35, -35, -49, -49, -49, -49, -49, -49, -49, -49, -49, -47, -47, -47, -40, -40, -40, -49, -49, -49, -48, -48, -48, -48, -48, -48, -47, -47, -47, -47, -47, -47, -47, -47, -47, -48, -48, -48, -60, -60, -60, -53, -53, -53, -48, -48, -48, -46, -46, -46, -65, -65, -65, -48, -48, -48, -54, -54, -54, -45, -45, -45, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49, -51, -51, -51, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47, -47, -47, -47, -37, -37, -37, -47, -47, -47, -49, -49, -49, -50, -50, -50, -48, -48, -48, -128, -128, -128, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47, -47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47, -47, -47, -48, -48, -48, -50, -50, -50, -49, -49, -49, -48, -48, -48, -48, -48, -48, -33, -33, -33, -48, -48, -48, -52, -52, -52, -51, -51, -51, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52, -52, -52, -48, -48, -48, -46, -46, -46, -48, -48, -48, -37, -37, -37, -29, -29, -29, -48, -48, -48, -48, -48, -48, -47, -47, -47, -48, -48, -48, -49, -49, -49, -46, -46, -46, -48, -48, -48, -49, -49, -49, -47, -47, -47, -58, -58, -58, -49, -49, -49, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47, -47, -47, -50, -50, -50, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -48, -48, -48, -55, -55, -55, -45, -45, -45, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47, -48, -48, -48, -47, -47, -47, -51, -51, -51, -47, -47, -47, -51, -51, -51, -48, -48, -48, -47, -47, -47, -48, -48, -48, -65, -65, -65, -48, -48, -48, -50, -50, -50, -48, -48, -48, -47, -47, -47, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -48, -48, -48, -50, -50, -50, -47, -47, -47, -49, -49, -49, -49, -49, -49, -47, -47, -47, -48, -48, -48, -47, -47, -47, -49, -49, -49, -39, -39, -39, -50, -50, -50, -47, -47, -47, -48, -48, -48, -42, -42, -42, -47, -47, -47, -46, -46, -46, -47, -47, -47, -47, -47, -47, -48, -48, -48, -49, -49, -49, -49, -49, -49, -47, -47, -47, -48, -48, -48, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47, -47, -47, -48, -48, -48, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49, -50, -50, -50, -47, -47, -47, -56, -56, -56, -48, -48, -48, -48, -48, -48, -40, -40, -40, -45, -45, -45, -47, -47, -47, -47, -47, -47, -49, -49, -49, -48, -48, -48, -43, -43, -43, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47, -47, -47, -50, -50, -50, -49, -49, -49, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49, -115, 52, -27, 40, 0, 0, 0, -3, 116, 82, 78, 83, 34, -37, -1, -7, 120, 4, 0, 5, 118, -9, -41, 38, 24, -46, -2, -100, 78, 69, 3, -98, -54, 28, 40, -37, -8, 115, 3, 122, -6, -43, 36, 31, -49, -109, 78, 70, -93, -50, 20, 43, -38, -10, 113, 123, 30, -45, -115, 2, 86, -5, -9, 65, -94, -55, 19, -29, 111, 6, 127, 32, -39, -3, -114, 92, -4, 58, -86, -63, 22, -32, 109, -126, -5, -51, 36, -44, -121, 1, 90, -11, 59, -79, -59, 15, 48, -11, 106, -123, 26, 37, -40, 98, -3, -14, -80, -61, 13, 44, -25, 103, 8, -120, 29, 35, -35, -4, -127, 101, 46, -76, -71, 17, -24, 99, 7, -118, -58, 42, 124, 2, 103, -13, 47, -67, -65, 12, 55, -27, -116, -57, 24, 44, -35, 119, 112, -21, 49, 8, -70, 10, 51, -19, -14, 93, -111, -62, 25, -30, 119, -78, 14, 9, -108, 113, 117, -18, -53, 63, -23, -15, 89, 9, -106, -2, -66, 21, 50, -125, -33, -28, 57, 97, -97, -73, 19, 69, -12, -17, 81, -87, 57, 94, -99, -73, 72, -18, 82, -88, -20, 27, 12, -100, 67, -16, 83, 16, -91, -122, 54, -26, 104, -101, 64, 85, 11, -82, 100, -101, 20, 84, -47, 125, 31, -102, 62, -57, -88, -125, 107, -102, -69, -15, -53, -77, 52, -31, -105, 66, -21, 87, 121, 110, -67, 23, -60, -84, 13, 46, 111, 88, -75, 119, 42, 50, -32, 106, -107, 63, 91, -78, 117, 114, -108, 41, -51, -28, -65, 0, 0, 3, -107, 73, 68, 65, 84, 88, -61, -107, -40, 105, 84, -108, 85, 24, 7, 112, -4, -113, -13, -112, -37, -101, -111, -96, 78, 67, -94, 38, -18, -66, 14, 46, 52, -38, -28, -96, 34, 90, -67, -31, -106, 75, -114, 70, 74, 42, 90, 86, -109, 4, 38, -88, -111, -90, 50, -126, -72, -46, -54, -102, 38, -102, -53, 104, -101, 123, -88, -71, 78, -18, -91, -90, 88, 82, 46, 89, 90, -71, -107, -27, 7, 95, 62, 120, -114, -100, -93, -121, -5, 127, 63, -65, -65, 15, -9, -36, 123, -97, -5, 127, -98, -96, -96, 106, -80, 84, -73, -118, -14, 23, 20, -4, 64, 13, -44, -84, 69, 0, -87, 93, 7, -38, -125, 12, -112, -70, 22, -32, 33, 6, -124, 60, 12, -44, 11, 37, -128, -124, -43, 71, -125, -122, 33, 4, -80, 61, 98, 71, -8, -93, 4, -112, 70, 17, -48, 26, 51, 64, -102, 0, 53, -102, 50, 64, 30, -45, -48, 44, -110, 1, -51, 91, -96, 101, 43, 43, 1, -84, -83, -19, -88, -39, -122, 0, 18, -38, 22, 104, -89, 19, 64, -38, 59, 16, -43, -127, 1, -42, -114, 64, -89, -50, 4, -112, -80, 104, 68, 61, -82, 19, -64, -39, -59, -126, -120, -82, 4, -112, -48, 39, -32, 122, -110, 1, -46, -51, 1, 119, 12, 3, -84, -35, -127, 30, 61, 9, 32, -51, 99, 17, -43, 75, 39, 64, 112, 92, 111, -12, 105, 68, 0, 121, -22, 105, 104, -49, 48, -64, 120, 22, -120, -17, 75, 0, -111, 126, 64, -1, 1, 12, 24, 24, -117, -25, 6, -39, 8, -32, 28, -20, 64, -77, 33, 4, -112, -95, -49, 3, 113, 6, 1, 100, -104, 7, -61, 99, 24, -32, 28, 1, -68, -112, 64, 0, 121, 113, 36, -30, 71, -23, 4, 72, 124, -55, -114, 78, -93, 9, 32, 99, -58, 34, 105, 28, 3, 100, -68, 27, 13, 94, 102, -128, -13, 21, 96, -62, -85, 4, -112, -127, -81, 33, -2, 117, -125, 0, -34, 55, -110, 48, 49, -108, 0, -110, -36, 2, -82, 55, 25, -112, -110, 10, 76, 122, -117, 0, 34, -109, -127, -76, 116, 6, 76, -103, -118, 73, -61, 116, 2, 120, -89, -71, -15, 118, 6, 1, -28, -99, -23, -64, 12, -125, 0, -58, -69, -64, -52, 89, 4, -112, -39, -26, -70, 51, 125, 4, -112, 57, 89, -56, -98, -101, 66, 0, 95, -114, 3, -13, -26, 19, 64, -110, 23, -64, -79, -112, 1, 82, -35, -123, -8, -95, 12, -16, -103, -21, 94, -76, -104, 0, 50, 101, 38, -122, 119, 51, 8, -112, -5, -98, 11, 105, -75, 9, 80, 81, 58, -19, -17, 51, 64, -1, 0, -8, 112, 12, 1, -60, -6, -111, -71, -18, -39, 4, -112, -113, -13, -112, 95, -112, 66, -128, -62, 34, -83, -8, -109, 37, 4, -112, -91, -79, -16, -92, 50, -64, -8, 20, 88, -58, 0, -55, 1, 74, 24, -80, 124, 36, 44, 43, 8, -112, -16, -103, -85, 120, -27, 42, 2, -84, 94, 3, -1, 90, 67, 29, -92, -41, 1, -42, 89, -43, 55, -50, 54, 24, -120, 14, 34, -114, -58, -25, 37, 112, 127, 65, 28, 62, -33, -105, -59, -38, 87, -99, 9, -16, -11, 122, -8, 55, 16, 23, -56, 23, 14, 108, -12, -86, 95, 81, 91, 28, -80, 105, 51, 81, 4, -52, 10, 110, 73, 37, -54, -52, -106, -83, 46, -84, -116, 36, -64, 55, 107, 80, -70, -115, -88, -83, -37, -21, 1, 59, -104, 98, -4, -83, -122, -84, -47, 68, -71, -33, -71, 11, -10, -35, -60, 11, -108, -66, -57, -115, -52, 72, 2, -20, -51, -122, 127, -97, -95, 14, 2, 17, -64, 119, 78, -11, 103, 87, -17, 98, -34, -29, 90, -60, -61, -66, 60, 26, -98, -3, 68, 116, -16, 29, -48, -76, -52, -125, 4, 88, -67, 9, -2, 67, 68, -4, -15, 30, 6, -70, 39, -86, 7, 44, -37, 17, 32, -21, 40, 17, -31, 118, -106, -64, 50, -120, 8, -119, -127, -17, 93, -104, -80, -124, 0, 63, 100, 35, -65, 61, -111, -116, -73, 31, 3, -114, 7, 8, 112, -62, 60, -43, 109, -120, -80, -66, 116, 42, -20, 63, 18, -3, 67, -32, -92, 27, -89, 14, 18, -96, 111, -39, -67, 79, -11, -3, -128, -17, 52, -16, -109, 83, -67, 105, -46, -51, 61, -34, 21, 70, -76, 101, 63, -97, -127, 103, 60, -47, -8, 21, -106, 87, 122, -110, -85, 6, -65, -4, -118, -46, -77, -122, 58, -16, -98, -82, 28, 43, -86, 2, 33, -25, -52, -32, 114, -108, 104, -64, -25, -100, -127, -3, 60, -47, -30, -5, -54, 93, -72, -16, 27, 1, 98, -4, -56, 46, 32, -26, 26, 9, 23, -127, -33, -1, 32, -64, 17, 13, 101, 42, 73, -27, -50, 63, -105, 74, -48, -5, 50, 49, -3, 9, 20, 37, -95, -86, 61, -82, 4, 98, 74, -111, -1, -89, -95, 14, 114, -51, 123, -4, 87, -94, -6, -56, -53, -8, 27, -56, -69, 66, 12, -43, -82, 94, -125, -25, 58, 49, -74, 43, -68, -95, 21, 87, 57, -109, -71, 27, -4, -109, -121, -78, 127, 83, -44, -127, -43, 124, -113, 111, 22, -118, 50, -48, -1, 3, -4, -86, -109, -54, 10, 80, 17, -8, -1, 23, 117, -112, 123, -53, 108, 41, 50, -44, -63, 109, -80, -19, 79, -78, 17, 39, 44, -102, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/picker/FilePicker.java ================================================ package com.kunfei.bookshelf.widget.filepicker.picker; import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.widget.filepicker.adapter.FileAdapter; import com.kunfei.bookshelf.widget.filepicker.adapter.PathAdapter; import com.kunfei.bookshelf.widget.filepicker.entity.FileItem; import com.kunfei.bookshelf.widget.filepicker.popup.ConfirmPopup; import com.kunfei.bookshelf.widget.filepicker.util.StorageUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 文件目录选择器 * * @author 李玉江[QQ:1032694760] * @since 2015/9/29, 2017/01/01, 2017/01/08 */ public class FilePicker extends ConfirmPopup implements FileAdapter.CallBack, PathAdapter.CallBack { public static final int DIRECTORY = 0; public static final int FILE = 1; private String initPath; private FileAdapter adapter = new FileAdapter(); private PathAdapter pathAdapter = new PathAdapter(); private TextView emptyView; private OnFilePickListener onFilePickListener; private int mode; private CharSequence emptyHint = java.util.Locale.getDefault().getDisplayLanguage().contains("中文") ? "<空>" : ""; @IntDef(value = {DIRECTORY, FILE}) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } /** * @see #FILE * @see #DIRECTORY */ public FilePicker(Activity activity, @Mode int mode) { super(activity); setHalfScreen(true); try { this.initPath = StorageUtils.getDownloadPath(); } catch (RuntimeException e) { this.initPath = StorageUtils.getInternalRootPath(activity); } this.mode = mode; adapter.setOnlyListDir(mode == DIRECTORY); adapter.setShowHideDir(false); adapter.setShowHomeDir(false); adapter.setShowUpDir(false); adapter.setCallBack(this); pathAdapter.setCallBack(this); } @Override @NonNull protected LinearLayout makeCenterView() { @SuppressLint("InflateParams") LinearLayout rootLayout = (LinearLayout) LayoutInflater.from(activity).inflate(R.layout.view_file_picker, null); RecyclerView recyclerView = rootLayout.findViewById(R.id.rv_file); recyclerView.addItemDecoration(new DividerItemDecoration(activity, LinearLayout.VERTICAL)); recyclerView.setLayoutManager(new LinearLayoutManager(activity)); recyclerView.setAdapter(adapter); emptyView = rootLayout.findViewById(R.id.tv_empty); RecyclerView pathView = rootLayout.findViewById(R.id.rv_path); pathView.setLayoutManager(new LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false)); pathView.setAdapter(pathAdapter); return rootLayout; } public void setRootPath(String initPath) { this.initPath = initPath; } public void setAllowExtensions(String[] allowExtensions) { adapter.setAllowExtensions(allowExtensions); } public void setShowUpDir(boolean showUpDir) { adapter.setShowUpDir(showUpDir); } public void setShowHomeDir(boolean showHomeDir) { adapter.setShowHomeDir(showHomeDir); } public void setShowHideDir(boolean showHideDir) { adapter.setShowHideDir(showHideDir); } public void setFileIcon(Drawable fileIcon) { adapter.setFileIcon(fileIcon); } public void setFolderIcon(Drawable folderIcon) { adapter.setFolderIcon(folderIcon); } public void setHomeIcon(Drawable homeIcon) { adapter.setHomeIcon(homeIcon); } public void setUpIcon(Drawable upIcon) { adapter.setUpIcon(upIcon); } public void setArrowIcon(Drawable arrowIcon) { pathAdapter.setArrowIcon(arrowIcon); } public void setItemHeight(int itemHeight) { adapter.setItemHeight(itemHeight); } public void setEmptyHint(CharSequence emptyHint) { this.emptyHint = emptyHint; } @Override protected void setContentViewBefore() { boolean isPickFile = mode == FILE; setCancelVisible(!isPickFile); if (isPickFile) { setSubmitText(activity.getString(android.R.string.cancel)); } else { setSubmitText(activity.getString(android.R.string.ok)); } } @Override protected void setContentViewAfter(View contentView) { refreshCurrentDirPath(initPath); } @Override protected void onSubmit() { if (mode != FILE) { String currentPath = adapter.getCurrentPath(); if (onFilePickListener != null) { onFilePickListener.onFilePicked(currentPath); } } } @Override public void dismiss() { super.dismiss(); } public FileAdapter getAdapter() { return adapter; } public PathAdapter getPathAdapter() { return pathAdapter; } public String getCurrentPath() { return adapter.getCurrentPath(); } /** * 响应选择器的列表项点击事件 */ @Override public void onFileClick(int position) { FileItem fileItem = adapter.getItem(position); if (fileItem.isDirectory()) { refreshCurrentDirPath(fileItem.getPath()); } else { String clickPath = fileItem.getPath(); if (mode != DIRECTORY) { dismiss(); if (onFilePickListener != null) { onFilePickListener.onFilePicked(clickPath); } } } } @Override public void onPathClick(int position) { refreshCurrentDirPath(pathAdapter.getItem(position)); } private void refreshCurrentDirPath(String currentPath) { if (currentPath.equals("/")) { pathAdapter.updatePath("/"); } else { pathAdapter.updatePath(currentPath); } adapter.loadData(currentPath); int adapterCount = adapter.getItemCount(); if (adapter.isShowHomeDir()) { adapterCount--; } if (adapter.isShowUpDir()) { adapterCount--; } if (adapterCount < 1) { emptyView.setVisibility(View.VISIBLE); emptyView.setText(emptyHint); } else { emptyView.setVisibility(View.GONE); } } public void setOnFilePickListener(OnFilePickListener listener) { this.onFilePickListener = listener; } public interface OnFilePickListener { void onFilePicked(String currentPath); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/popup/BasicPopup.java ================================================ package com.kunfei.bookshelf.widget.filepicker.popup; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.FrameLayout; import androidx.annotation.StyleRes; import com.kunfei.bookshelf.widget.filepicker.util.ScreenUtils; /** * 弹窗基类 * * @param 弹窗的内容视图类型 * @author 李玉江[QQ:1023694760] * @since 2015/7/19 */ public abstract class BasicPopup implements DialogInterface.OnKeyListener, DialogInterface.OnDismissListener { public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; protected Activity activity; protected int screenWidthPixels; protected int screenHeightPixels; private Dialog dialog; private FrameLayout contentLayout; private boolean isPrepared = false; public BasicPopup(Activity activity) { this.activity = activity; DisplayMetrics metrics = ScreenUtils.displayMetrics(activity); screenWidthPixels = metrics.widthPixels; screenHeightPixels = metrics.heightPixels; initDialog(); } private void initDialog() { contentLayout = new FrameLayout(activity); contentLayout.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); contentLayout.setFocusable(true); contentLayout.setFocusableInTouchMode(true); dialog = new Dialog(activity); dialog.setCanceledOnTouchOutside(true);//触摸屏幕取消窗体 dialog.setCancelable(true);//按返回键取消窗体 dialog.setOnKeyListener(this); dialog.setOnDismissListener(this); Window window = dialog.getWindow(); if (window != null) { window.setGravity(Gravity.BOTTOM); window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); //AndroidRuntimeException: requestFeature() must be called before adding content window.requestFeature(Window.FEATURE_NO_TITLE); window.setContentView(contentLayout); } setSize(screenWidthPixels, WRAP_CONTENT); } public int getScreenWidthPixels() { return screenWidthPixels; } public int getScreenHeightPixels() { return screenHeightPixels; } /** * 创建弹窗的内容视图 * * @return the view */ protected abstract V makeContentView(); /** * 固定高度为屏幕的高 * * @param fillScreen true为全屏 */ public void setFillScreen(boolean fillScreen) { if (fillScreen) { setSize(screenWidthPixels, (int) (screenHeightPixels * 0.85f)); } } /** * 固定高度为屏幕的一半 * * @param halfScreen true为半屏 */ public void setHalfScreen(boolean halfScreen) { if (halfScreen) { setSize(screenWidthPixels, screenHeightPixels / 2); } } /** * 位于屏幕何处 * * @see Gravity */ public void setGravity(int gravity) { Window window = dialog.getWindow(); if (window != null) { window.setGravity(gravity); } if (gravity == Gravity.CENTER) { //居于屏幕正中间时,宽度不允许填充屏幕 setWidth((int) (screenWidthPixels * 0.7f)); } } /** * 设置弹窗的内容视图之前执行 */ protected void setContentViewBefore() { } /** * 设置弹窗的内容视图之后执行 * * @param contentView 弹窗的内容视图 */ protected void setContentViewAfter(V contentView) { } public void setContentView(View view) { contentLayout.removeAllViews(); contentLayout.addView(view); } public void setFitsSystemWindows(boolean flag) { contentLayout.setFitsSystemWindows(flag); } public void setAnimationStyle(@StyleRes int animRes) { Window window = dialog.getWindow(); if (window != null) { window.setWindowAnimations(animRes); } } public void setCanceledOnTouchOutside(boolean flag) { dialog.setCanceledOnTouchOutside(flag); } public void setCancelable(boolean flag) { dialog.setCancelable(flag); } public void setOnDismissListener(final DialogInterface.OnDismissListener onDismissListener) { dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { BasicPopup.this.onDismiss(dialog); onDismissListener.onDismiss(dialog); } }); } public void setOnKeyListener(final DialogInterface.OnKeyListener onKeyListener) { dialog.setOnKeyListener(new DialogInterface.OnKeyListener() { @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { BasicPopup.this.onKey(dialog, keyCode, event); return onKeyListener.onKey(dialog, keyCode, event); } }); } /** * 设置弹窗的宽和高 * * @param width 宽 * @param height 高 */ public void setSize(int width, int height) { if (width == MATCH_PARENT) { //360奇酷等手机对话框MATCH_PARENT时两边还会有边距,故强制填充屏幕宽 width = screenWidthPixels; } if (width == 0 && height == 0) { width = screenWidthPixels; height = WRAP_CONTENT; } else if (width == 0) { width = screenWidthPixels; } else if (height == 0) { height = WRAP_CONTENT; } ViewGroup.LayoutParams params = contentLayout.getLayoutParams(); if (params == null) { params = new ViewGroup.LayoutParams(width, height); } else { params.width = width; params.height = height; } contentLayout.setLayoutParams(params); } /** * 设置弹窗的宽 * * @param width 宽 * @see #setSize(int, int) */ public void setWidth(int width) { setSize(width, 0); } /** * 设置弹窗的高 * * @param height 高 * @see #setSize(int, int) */ public void setHeight(int height) { setSize(0, height); } /** * 设置是否需要重新初始化视图,可用于数据刷新 */ public void setPrepared(boolean prepared) { isPrepared = prepared; } public boolean isShowing() { return dialog.isShowing(); } public final void show() { if (isPrepared) { dialog.show(); showAfter(); return; } setContentViewBefore(); V view = makeContentView(); setContentView(view);// 设置弹出窗体的布局 setContentViewAfter(view); isPrepared = true; dialog.show(); showAfter(); } protected void showAfter() { } public void dismiss() { dismissImmediately(); } protected final void dismissImmediately() { dialog.dismiss(); } public boolean onBackPress() { dismiss(); return false; } @Override public final boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { onBackPress(); } return false; } @Override public void onDismiss(DialogInterface dialog) { dismiss(); } public Context getContext() { return dialog.getContext(); } public Window getWindow() { return dialog.getWindow(); } /** * 弹框的内容视图 */ public View getContentView() { // IllegalStateException: The specified child already has a parent. // You must call removeView() on the child's parent first. return contentLayout.getChildAt(0); } /** * 弹框的根视图 */ public ViewGroup getRootView() { return contentLayout; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/popup/ConfirmPopup.java ================================================ package com.kunfei.bookshelf.widget.filepicker.popup; import android.app.Activity; import android.graphics.Color; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import com.kunfei.bookshelf.widget.filepicker.util.ConvertUtils; /** * 带确定及取消按钮的弹窗 * * @author 李玉江[QQ:1032694760] * @since 2015/10/21 */ @SuppressWarnings("WeakerAccess") public abstract class ConfirmPopup extends BasicPopup { protected boolean topLineVisible = true; protected int topLineColor = 0xFF33B5E5; protected int topLineHeightPixels = 1;//px protected int topBackgroundColor = Color.WHITE; protected int topHeight = 40;//dp protected int topPadding = 15;//dp protected int contentLeftAndRightPadding = 0;//dp protected int contentTopAndBottomPadding = 0;//dp protected boolean cancelVisible = true; protected CharSequence cancelText = ""; protected CharSequence submitText = ""; protected CharSequence titleText = ""; protected int cancelTextColor = 0xFF33B5E5; protected int submitTextColor = 0xFF33B5E5; protected int titleTextColor = Color.BLACK; protected int pressedTextColor = 0XFF0288CE; protected int cancelTextSize = 0; protected int submitTextSize = 0; protected int titleTextSize = 0; protected int backgroundColor = Color.WHITE; protected TextView cancelButton, submitButton; protected View titleView; protected View headerView, centerView, footerView; public ConfirmPopup(Activity activity) { super(activity); cancelText = activity.getString(android.R.string.cancel); submitText = activity.getString(android.R.string.ok); } /** * 设置顶部标题栏下划线颜色 */ public void setTopLineColor(@ColorInt int topLineColor) { this.topLineColor = topLineColor; } /** * 设置顶部标题栏下划线高度,单位为px */ public void setTopLineHeight(int topLineHeightPixels) { this.topLineHeightPixels = topLineHeightPixels; } /** * 设置顶部标题栏背景颜色 */ public void setTopBackgroundColor(@ColorInt int topBackgroundColor) { this.topBackgroundColor = topBackgroundColor; } /** * 设置顶部标题栏高度(单位为dp) */ public void setTopHeight(@IntRange(from = 10, to = 80) int topHeight) { this.topHeight = topHeight; } /** * 设置顶部按钮左边及右边边距(单位为dp) */ public void setTopPadding(int topPadding) { this.topPadding = topPadding; } /** * 设置顶部标题栏下划线是否显示 */ public void setTopLineVisible(boolean topLineVisible) { this.topLineVisible = topLineVisible; } /** * 设置内容上下左右边距(单位为dp) */ public void setContentPadding(int leftAndRight, int topAndBottom) { this.contentLeftAndRightPadding = leftAndRight; this.contentTopAndBottomPadding = topAndBottom; } /** * 设置顶部标题栏取消按钮是否显示 */ public void setCancelVisible(boolean cancelVisible) { if (null != cancelButton) { cancelButton.setVisibility(cancelVisible ? View.VISIBLE : View.GONE); } else { this.cancelVisible = cancelVisible; } } /** * 设置顶部标题栏取消按钮文字 */ public void setCancelText(CharSequence cancelText) { if (null != cancelButton) { cancelButton.setText(cancelText); } else { this.cancelText = cancelText; } } /** * 设置顶部标题栏取消按钮文字 */ public void setCancelText(@StringRes int textRes) { setCancelText(activity.getString(textRes)); } /** * 设置顶部标题栏确定按钮文字 */ public void setSubmitText(CharSequence submitText) { if (null != submitButton) { submitButton.setText(submitText); } else { this.submitText = submitText; } } /** * 设置顶部标题栏确定按钮文字 */ public void setSubmitText(@StringRes int textRes) { setSubmitText(activity.getString(textRes)); } /** * 设置顶部标题栏标题文字 */ public void setTitleText(CharSequence titleText) { if (titleView != null && titleView instanceof TextView) { ((TextView) titleView).setText(titleText); } else { this.titleText = titleText; } } /** * 设置顶部标题栏标题文字 */ public void setTitleText(@StringRes int textRes) { setTitleText(activity.getString(textRes)); } /** * 设置顶部标题栏取消按钮文字颜色 */ public void setCancelTextColor(@ColorInt int cancelTextColor) { if (null != cancelButton) { cancelButton.setTextColor(cancelTextColor); } else { this.cancelTextColor = cancelTextColor; } } /** * 设置顶部标题栏确定按钮文字颜色 */ public void setSubmitTextColor(@ColorInt int submitTextColor) { if (null != submitButton) { submitButton.setTextColor(submitTextColor); } else { this.submitTextColor = submitTextColor; } } /** * 设置顶部标题栏标题文字颜色 */ public void setTitleTextColor(@ColorInt int titleTextColor) { if (null != titleView && titleView instanceof TextView) { ((TextView) titleView).setTextColor(titleTextColor); } else { this.titleTextColor = titleTextColor; } } /** * 设置按下时的文字颜色 */ public void setPressedTextColor(int pressedTextColor) { this.pressedTextColor = pressedTextColor; } /** * 设置顶部标题栏取消按钮文字大小(单位为sp) */ public void setCancelTextSize(@IntRange(from = 10, to = 40) int cancelTextSize) { this.cancelTextSize = cancelTextSize; } /** * 设置顶部标题栏确定按钮文字大小(单位为sp) */ public void setSubmitTextSize(@IntRange(from = 10, to = 40) int submitTextSize) { this.submitTextSize = submitTextSize; } /** * 设置顶部标题栏标题文字大小(单位为sp) */ public void setTitleTextSize(@IntRange(from = 10, to = 40) int titleTextSize) { this.titleTextSize = titleTextSize; } /** * 设置选择器主体背景颜色 */ public void setBackgroundColor(@ColorInt int backgroundColor) { this.backgroundColor = backgroundColor; } public TextView getCancelButton() { if (null == cancelButton) { throw new NullPointerException("please call show at first"); } return cancelButton; } public TextView getSubmitButton() { if (null == submitButton) { throw new NullPointerException("please call show at first"); } return submitButton; } /** * @see #makeHeaderView() * @see #makeCenterView() * @see #makeFooterView() */ @Override protected final View makeContentView() { LinearLayout rootLayout = new LinearLayout(activity); rootLayout.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); rootLayout.setBackgroundColor(backgroundColor); rootLayout.setOrientation(LinearLayout.VERTICAL); rootLayout.setGravity(Gravity.CENTER); rootLayout.setPadding(0, 0, 0, 0); rootLayout.setClipToPadding(false); View headerView = makeHeaderView(); if (headerView != null) { rootLayout.addView(headerView); } if (topLineVisible) { View lineView = new View(activity); lineView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, topLineHeightPixels)); lineView.setBackgroundColor(topLineColor); rootLayout.addView(lineView); } if (centerView == null) { centerView = makeCenterView(); } int lr = 0; int tb = 0; if (contentLeftAndRightPadding > 0) { lr = ConvertUtils.toPx(activity, contentLeftAndRightPadding); } if (contentTopAndBottomPadding > 0) { tb = ConvertUtils.toPx(activity, contentTopAndBottomPadding); } centerView.setPadding(lr, tb, lr, tb); ViewGroup vg = (ViewGroup) centerView.getParent(); if (vg != null) { //IllegalStateException: The specified child already has a parent vg.removeView(centerView); } rootLayout.addView(centerView, new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1.0f)); View footerView = makeFooterView(); if (footerView != null) { rootLayout.addView(footerView); } return rootLayout; } @Nullable protected View makeHeaderView() { if (null != headerView) { return headerView; } RelativeLayout topButtonLayout = new RelativeLayout(activity); int height = ConvertUtils.toPx(activity, topHeight); topButtonLayout.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, height)); topButtonLayout.setBackgroundColor(topBackgroundColor); topButtonLayout.setGravity(Gravity.CENTER_VERTICAL); cancelButton = new TextView(activity); cancelButton.setVisibility(cancelVisible ? View.VISIBLE : View.GONE); RelativeLayout.LayoutParams cancelParams = new RelativeLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT); cancelParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); cancelParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE); cancelButton.setLayoutParams(cancelParams); cancelButton.setBackgroundColor(Color.TRANSPARENT); cancelButton.setGravity(Gravity.CENTER); int padding = ConvertUtils.toPx(activity, topPadding); cancelButton.setPadding(padding, 0, padding, 0); if (!TextUtils.isEmpty(cancelText)) { cancelButton.setText(cancelText); } cancelButton.setTextColor(ConvertUtils.toColorStateList(cancelTextColor, pressedTextColor)); if (cancelTextSize != 0) { cancelButton.setTextSize(cancelTextSize); } cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismiss(); onCancel(); } }); topButtonLayout.addView(cancelButton); if (null == titleView) { TextView textView = new TextView(activity); RelativeLayout.LayoutParams titleParams = new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT); int margin = ConvertUtils.toPx(activity, topPadding); titleParams.leftMargin = margin; titleParams.rightMargin = margin; titleParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE); titleParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE); textView.setLayoutParams(titleParams); textView.setGravity(Gravity.CENTER); if (!TextUtils.isEmpty(titleText)) { textView.setText(titleText); } textView.setTextColor(titleTextColor); if (titleTextSize != 0) { textView.setTextSize(titleTextSize); } titleView = textView; } topButtonLayout.addView(titleView); submitButton = new TextView(activity); RelativeLayout.LayoutParams submitParams = new RelativeLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT); submitParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE); submitParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE); submitButton.setLayoutParams(submitParams); submitButton.setBackgroundColor(Color.TRANSPARENT); submitButton.setGravity(Gravity.CENTER); submitButton.setPadding(padding, 0, padding, 0); if (!TextUtils.isEmpty(submitText)) { submitButton.setText(submitText); } submitButton.setTextColor(ConvertUtils.toColorStateList(submitTextColor, pressedTextColor)); if (submitTextSize != 0) { submitButton.setTextSize(submitTextSize); } submitButton.setOnClickListener(v -> { dismiss(); onSubmit(); }); topButtonLayout.addView(submitButton); return topButtonLayout; } @NonNull protected abstract V makeCenterView(); @Nullable protected View makeFooterView() { if (null != footerView) { return footerView; } return null; } protected void onSubmit() { } protected void onCancel() { } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/ConvertUtils.java ================================================ package com.kunfei.bookshelf.widget.filepicker.util; import android.annotation.TargetApi; import android.content.ContentUris; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.TextUtils; import android.view.View; import android.widget.ListView; import android.widget.ScrollView; import androidx.annotation.ColorInt; import androidx.annotation.FloatRange; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.text.DecimalFormat; import java.util.Arrays; import java.util.List; /** * 数据类型转换、单位转换 * * @author 李玉江[QQ:1023694760] * @since 2014-4-18 */ public class ConvertUtils { public static final long GB = 1073741824; public static final long MB = 1048576; public static final long KB = 1024; public static int toInt(Object obj) { try { return Integer.parseInt(obj.toString()); } catch (NumberFormatException e) { return -1; } } public static int toInt(byte[] bytes) { int result = 0; byte abyte; for (int i = 0; i < bytes.length; i++) { abyte = bytes[i]; result += (abyte & 0xFF) << (8 * i); } return result; } public static int toShort(byte first, byte second) { return (first << 8) + (second & 0xFF); } public static long toLong(Object obj) { try { return Long.parseLong(obj.toString()); } catch (NumberFormatException e) { return -1L; } } public static float toFloat(Object obj) { try { return Float.parseFloat(obj.toString()); } catch (NumberFormatException e) { return -1f; } } /** * int占4字节 * * @param i the * @return byte [ ] */ public static byte[] toByteArray(int i) { // byte[] bytes = new byte[4]; // bytes[0] = (byte) (0xff & i); // bytes[1] = (byte) ((0xff00 & i) >> 8); // bytes[2] = (byte) ((0xff0000 & i) >> 16); // bytes[3] = (byte) ((0xff000000 & i) >> 24); // return bytes; return ByteBuffer.allocate(4).putInt(i).array(); } public static byte[] toByteArray(String hexData, boolean isHex) { if (hexData == null || hexData.equals("")) { return null; } if (!isHex) { return hexData.getBytes(); } hexData = hexData.replaceAll("\\s+", ""); String hexDigits = "0123456789ABCDEF"; ByteArrayOutputStream baos = new ByteArrayOutputStream( hexData.length() / 2); // 将每2位16进制整数组装成一个字节 for (int i = 0; i < hexData.length(); i += 2) { baos.write((hexDigits.indexOf(hexData.charAt(i)) << 4 | hexDigits .indexOf(hexData.charAt(i + 1)))); } byte[] bytes = baos.toByteArray(); try { baos.close(); } catch (IOException e) { } return bytes; } public static String toHexString(String str) { if (TextUtils.isEmpty(str)) return ""; StringBuilder builder = new StringBuilder(); byte[] bytes = str.getBytes(); for (byte aByte : bytes) { builder.append(Integer.toHexString(0xFF & aByte)); builder.append(" "); } return builder.toString(); } /** * To hex string string. * * @param bytes the bytes * @return the string */ public static String toHexString(byte... bytes) { char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; // 参见:http://www.oschina.net/code/snippet_116768_9019 char[] buffer = new char[bytes.length * 2]; for (int i = 0, j = 0; i < bytes.length; ++i) { int u = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];//转无符号整型 buffer[j++] = DIGITS[u >>> 4]; buffer[j++] = DIGITS[u & 0xf]; } return new String(buffer); } /** * To hex string string. * * @param num the num * @return the string */ public static String toHexString(int num) { String hexString = Integer.toHexString(num); return hexString; } /** * To binary string string. * * @param bytes the bytes * @return the string */ public static String toBinaryString(byte... bytes) { char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; // 参见:http://www.oschina.net/code/snippet_116768_9019 char[] buffer = new char[bytes.length * 8]; for (int i = 0, j = 0; i < bytes.length; ++i) { int u = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];//转无符号整型 buffer[j++] = DIGITS[(u >>> 7) & 0x1]; buffer[j++] = DIGITS[(u >>> 6) & 0x1]; buffer[j++] = DIGITS[(u >>> 5) & 0x1]; buffer[j++] = DIGITS[(u >>> 4) & 0x1]; buffer[j++] = DIGITS[(u >>> 3) & 0x1]; buffer[j++] = DIGITS[(u >>> 2) & 0x1]; buffer[j++] = DIGITS[(u >>> 1) & 0x1]; buffer[j++] = DIGITS[u & 0x1]; } return new String(buffer); } /** * To binary string string. * * @param num the num * @return the string */ public static String toBinaryString(int num) { String binaryString = Integer.toBinaryString(num); return binaryString; } public static String toSlashString(String str) { String result = ""; char[] chars = str.toCharArray(); for (char chr : chars) { if (chr == '"' || chr == '\'' || chr == '\\') { result += "\\";//符合“"”“'”“\”这三个符号的前面加一个“\” } result += chr; } return result; } public static List toList(T[] array) { return Arrays.asList(array); } public static String toString(Object[] objects) { return Arrays.deepToString(objects); } public static String toString(Object[] objects, String tag) { StringBuilder sb = new StringBuilder(); for (Object object : objects) { sb.append(object); sb.append(tag); } return sb.toString(); } public static byte[] toByteArray(InputStream is) { if (is == null) { return null; } try { ByteArrayOutputStream os = new ByteArrayOutputStream(); byte[] buff = new byte[100]; while (true) { int len = is.read(buff, 0, 100); if (len == -1) { break; } else { os.write(buff, 0, len); } } byte[] bytes = os.toByteArray(); os.close(); is.close(); return bytes; } catch (IOException e) { } return null; } public static byte[] toByteArray(Bitmap bitmap) { if (bitmap == null) { return null; } ByteArrayOutputStream os = new ByteArrayOutputStream(); // 将Bitmap压缩成PNG编码,质量为100%存储,除了PNG还有很多常见格式,如jpeg等。 bitmap.compress(Bitmap.CompressFormat.PNG, 100, os); byte[] bytes = os.toByteArray(); try { os.close(); } catch (IOException e) { } return bytes; } public static Bitmap toBitmap(byte[] bytes, int width, int height) { Bitmap bitmap = null; if (bytes.length != 0) { try { BitmapFactory.Options options = new BitmapFactory.Options(); // 不进行图片抖动处理 options.inDither = false; // 设置让解码器以最佳方式解码 options.inPreferredConfig = null; if (width > 0 && height > 0) { options.outWidth = width; options.outHeight = height; } bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); bitmap.setDensity(96);// 96 dpi } catch (Exception e) { } } return bitmap; } public static Bitmap toBitmap(byte[] bytes) { return toBitmap(bytes, -1, -1); } /** * 将Drawable转换为Bitmap * 参考:http://kylines.iteye.com/blog/1660184 */ public static Bitmap toBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } else if (drawable instanceof ColorDrawable) { //color Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawColor(((ColorDrawable) drawable).getColor()); return bitmap; } else if (drawable instanceof NinePatchDrawable) { //.9.png Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); drawable.draw(canvas); return bitmap; } return null; } /** * 从第三方文件选择器获取路径。 * 参见:http://blog.csdn.net/zbjdsbj/article/details/42387551 */ @TargetApi(Build.VERSION_CODES.KITKAT) public static String toPath(Context context, Uri uri) { if (uri == null) { return ""; } String path = uri.getPath(); String scheme = uri.getScheme(); String authority = uri.getAuthority(); //是否是4.4及以上版本 boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { String docId = DocumentsContract.getDocumentId(uri); String[] split = docId.split(":"); String type = split[0]; Uri contentUri = null; switch (authority) { // ExternalStorageProvider case "com.android.externalstorage.documents": if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } break; // DownloadsProvider case "com.android.providers.downloads.documents": contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId)); return _queryPathFromMediaStore(context, contentUri, null, null); // MediaProvider case "com.android.providers.media.documents": if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } String selection = "_id=?"; String[] selectionArgs = new String[]{split[1]}; return _queryPathFromMediaStore(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else { if ("content".equalsIgnoreCase(scheme)) { // Return the remote address if (authority.equals("com.google.android.apps.photos.content")) { return uri.getLastPathSegment(); } return _queryPathFromMediaStore(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(scheme)) { return uri.getPath(); } } return path; } private static String _queryPathFromMediaStore(Context context, Uri uri, String selection, String[] selectionArgs) { String filePath = null; try { String[] projection = {MediaStore.Images.Media.DATA}; Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null) { int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); filePath = cursor.getString(column_index); cursor.close(); } } catch (IllegalArgumentException e) { } return filePath; } /** * 把view转化为bitmap(截图) * 参见:http://www.cnblogs.com/lee0oo0/p/3355468.html */ public static Bitmap toBitmap(View view) { int width = view.getWidth(); int height = view.getHeight(); if (view instanceof ListView) { height = 0; // 获取listView实际高度 ListView listView = (ListView) view; for (int i = 0; i < listView.getChildCount(); i++) { height += listView.getChildAt(i).getHeight(); } } else if (view instanceof ScrollView) { height = 0; // 获取scrollView实际高度 ScrollView scrollView = (ScrollView) view; for (int i = 0; i < scrollView.getChildCount(); i++) { height += scrollView.getChildAt(i).getHeight(); } } view.setDrawingCacheEnabled(true); view.clearFocus(); view.setPressed(false); boolean willNotCache = view.willNotCacheDrawing(); view.setWillNotCacheDrawing(false); // Reset the drawing cache background color to fully transparent for the duration of this operation int color = view.getDrawingCacheBackgroundColor(); view.setDrawingCacheBackgroundColor(Color.WHITE);//截图去黑色背景(透明像素) if (color != Color.WHITE) { view.destroyDrawingCache(); } view.buildDrawingCache(); Bitmap cacheBitmap = view.getDrawingCache(); if (cacheBitmap == null) { return null; } Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(cacheBitmap, 0, 0, null); canvas.save(); canvas.restore(); if (!bitmap.isRecycled()) { bitmap.recycle(); } // Restore the view view.destroyDrawingCache(); view.setWillNotCacheDrawing(willNotCache); view.setDrawingCacheBackgroundColor(color); return bitmap; } public static Drawable toDrawable(Bitmap bitmap) { return bitmap == null ? null : new BitmapDrawable(Resources.getSystem(), bitmap); } public static byte[] toByteArray(Drawable drawable) { return toByteArray(toBitmap(drawable)); } public static Drawable toDrawable(byte[] bytes) { return toDrawable(toBitmap(bytes)); } /** * dp转换为px */ public static int toPx(Context context, float dpValue) { final float scale = context.getResources().getDisplayMetrics().density; int pxValue = (int) (dpValue * scale + 0.5f); return pxValue; } /** * px转换为dp */ public static int toDp(Context context, float pxValue) { final float scale = context.getResources().getDisplayMetrics().density; int dpValue = (int) (pxValue / scale + 0.5f); return dpValue; } /** * px转换为sp */ public static int toSp(Context context, float pxValue) { final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; int spValue = (int) (pxValue / fontScale + 0.5f); return spValue; } public static String toGbk(String str) { try { return new String(str.getBytes("utf-8"), "gbk"); } catch (UnsupportedEncodingException e) { return str; } } public static String toFileSizeString(long fileSize) { DecimalFormat df = new DecimalFormat("0.00"); String fileSizeString; if (fileSize < KB) { fileSizeString = fileSize + "B"; } else if (fileSize < MB) { fileSizeString = df.format((double) fileSize / KB) + "K"; } else if (fileSize < GB) { fileSizeString = df.format((double) fileSize / MB) + "M"; } else { fileSizeString = df.format((double) fileSize / GB) + "G"; } return fileSizeString; } public static String toString(InputStream is, String charset) { StringBuilder sb = new StringBuilder(); try { BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset)); while (true) { String line = reader.readLine(); if (line == null) { break; } else { sb.append(line).append("\n"); } } reader.close(); is.close(); } catch (IOException e) { } return sb.toString(); } public static String toString(InputStream is) { return toString(is, "utf-8"); } public static int toDarkenColor(@ColorInt int color) { return toDarkenColor(color, 0.8f); } public static int toDarkenColor(@ColorInt int color, @FloatRange(from = 0f, to = 1f) float value) { float[] hsv = new float[3]; Color.colorToHSV(color, hsv); hsv[2] *= value;//HSV指Hue、Saturation、Value,即色调、饱和度和亮度,此处表示修改亮度 return Color.HSVToColor(hsv); } /** * 转换为6位十六进制颜色代码,不含“#” */ public static String toColorString(@ColorInt int color) { return toColorString(color, false); } /** * 转换为6位十六进制颜色代码,不含“#” */ public static String toColorString(@ColorInt int color, boolean includeAlpha) { String alpha = Integer.toHexString(Color.alpha(color)); String red = Integer.toHexString(Color.red(color)); String green = Integer.toHexString(Color.green(color)); String blue = Integer.toHexString(Color.blue(color)); if (alpha.length() == 1) { alpha = "0" + alpha; } if (red.length() == 1) { red = "0" + red; } if (green.length() == 1) { green = "0" + green; } if (blue.length() == 1) { blue = "0" + blue; } String colorString; if (includeAlpha) { colorString = alpha + red + green + blue; } else { colorString = red + green + blue; } return colorString; } /** * 对TextView、Button等设置不同状态时其文字颜色。 * 参见:http://blog.csdn.net/sodino/article/details/6797821 * Modified by liyujiang at 2015.08.13 */ public static ColorStateList toColorStateList(@ColorInt int normalColor, @ColorInt int pressedColor, @ColorInt int focusedColor, @ColorInt int unableColor) { int[] colors = new int[]{pressedColor, focusedColor, normalColor, focusedColor, unableColor, normalColor}; int[][] states = new int[6][]; states[0] = new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}; states[1] = new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}; states[2] = new int[]{android.R.attr.state_enabled}; states[3] = new int[]{android.R.attr.state_focused}; states[4] = new int[]{android.R.attr.state_window_focused}; states[5] = new int[]{}; return new ColorStateList(states, colors); } public static ColorStateList toColorStateList(@ColorInt int normalColor, @ColorInt int pressedColor) { return toColorStateList(normalColor, pressedColor, pressedColor, normalColor); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/DateUtils.java ================================================ package com.kunfei.bookshelf.widget.filepicker.util; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; /** * 日期时间工具类 * * @author 李玉江[QQ:1023694760] * @since 2015/8/5 */ public class DateUtils extends android.text.format.DateUtils { public static final int Second = 0; public static final int Minute = 1; public static final int Hour = 2; public static final int Day = 3; @IntDef(value = {Second, Minute, Hour, Day}) @Retention(RetentionPolicy.SOURCE) public @interface DifferenceMode { } public static long calculateDifferentSecond(Date startDate, Date endDate) { return calculateDifference(startDate, endDate, Second); } public static long calculateDifferentMinute(Date startDate, Date endDate) { return calculateDifference(startDate, endDate, Minute); } public static long calculateDifferentHour(Date startDate, Date endDate) { return calculateDifference(startDate, endDate, Hour); } public static long calculateDifferentDay(Date startDate, Date endDate) { return calculateDifference(startDate, endDate, Day); } public static long calculateDifferentSecond(long startTimeMillis, long endTimeMillis) { return calculateDifference(startTimeMillis, endTimeMillis, Second); } public static long calculateDifferentMinute(long startTimeMillis, long endTimeMillis) { return calculateDifference(startTimeMillis, endTimeMillis, Minute); } public static long calculateDifferentHour(long startTimeMillis, long endTimeMillis) { return calculateDifference(startTimeMillis, endTimeMillis, Hour); } public static long calculateDifferentDay(long startTimeMillis, long endTimeMillis) { return calculateDifference(startTimeMillis, endTimeMillis, Day); } /** * 计算两个时间戳之间相差的时间戳数 */ public static long calculateDifference(long startTimeMillis, long endTimeMillis, @DifferenceMode int mode) { return calculateDifference(new Date(startTimeMillis), new Date(endTimeMillis), mode); } /** * 计算两个日期之间相差的时间戳数 */ public static long calculateDifference(Date startDate, Date endDate, @DifferenceMode int mode) { long[] different = calculateDifference(startDate, endDate); if (mode == Minute) { return different[2]; } else if (mode == Hour) { return different[1]; } else if (mode == Day) { return different[0]; } else { return different[3]; } } private static long[] calculateDifference(Date startDate, Date endDate) { return calculateDifference(endDate.getTime() - startDate.getTime()); } private static long[] calculateDifference(long differentMilliSeconds) { long secondsInMilli = 1000;//1s==1000ms long minutesInMilli = secondsInMilli * 60; long hoursInMilli = minutesInMilli * 60; long daysInMilli = hoursInMilli * 24; long elapsedDays = differentMilliSeconds / daysInMilli; differentMilliSeconds = differentMilliSeconds % daysInMilli; long elapsedHours = differentMilliSeconds / hoursInMilli; differentMilliSeconds = differentMilliSeconds % hoursInMilli; long elapsedMinutes = differentMilliSeconds / minutesInMilli; differentMilliSeconds = differentMilliSeconds % minutesInMilli; long elapsedSeconds = differentMilliSeconds / secondsInMilli; return new long[]{elapsedDays, elapsedHours, elapsedMinutes, elapsedSeconds}; } /** * 计算每月的天数 */ public static int calculateDaysInMonth(int month) { return calculateDaysInMonth(0, month); } /** * 根据年份及月份计算每月的天数 */ public static int calculateDaysInMonth(int year, int month) { // 添加大小月月份并将其转换为list,方便之后的判断 String[] bigMonths = {"1", "3", "5", "7", "8", "10", "12"}; String[] littleMonths = {"4", "6", "9", "11"}; List bigList = Arrays.asList(bigMonths); List littleList = Arrays.asList(littleMonths); // 判断大小月及是否闰年,用来确定"日"的数据 if (bigList.contains(String.valueOf(month))) { return 31; } else if (littleList.contains(String.valueOf(month))) { return 30; } else { if (year <= 0) { return 29; } // 是否闰年 if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) { return 29; } else { return 28; } } } /** * 月日时分秒,0-9前补0 */ @NonNull public static String fillZero(int number) { return number < 10 ? "0" + number : "" + number; } /** * 截取掉前缀0以便转换为整数 * * @see #fillZero(int) */ public static int trimZero(@NonNull String text) { try { if (text.startsWith("0")) { text = text.substring(1); } return Integer.parseInt(text); } catch (NumberFormatException e) { return 0; } } /** * 功能:判断日期是否和当前date对象在同一天。 * 参见:http://www.cnblogs.com/myzhijie/p/3330970.html * * @param date 比较的日期 * @return boolean 如果在返回true,否则返回false。 */ public static boolean isSameDay(Date date) { if (date == null) { throw new IllegalArgumentException("date is null"); } Calendar nowCalendar = Calendar.getInstance(); Calendar newCalendar = Calendar.getInstance(); newCalendar.setTime(date); return (nowCalendar.get(Calendar.ERA) == newCalendar.get(Calendar.ERA) && nowCalendar.get(Calendar.YEAR) == newCalendar.get(Calendar.YEAR) && nowCalendar.get(Calendar.DAY_OF_YEAR) == newCalendar.get(Calendar.DAY_OF_YEAR)); } /** * 将yyyy-MM-dd HH:mm:ss字符串转换成日期
    * * @param dateStr 时间字符串 * @param dataFormat 当前时间字符串的格式。 * @return Date 日期 ,转换异常时返回null。 */ public static Date parseDate(String dateStr, String dataFormat) { try { SimpleDateFormat dateFormat = new SimpleDateFormat(dataFormat, Locale.PRC); Date date = dateFormat.parse(dateStr); return new Date(date.getTime()); } catch (ParseException e) { return null; } } /** * 将yyyy-MM-dd HH:mm:ss字符串转换成日期
    * * @param dateStr yyyy-MM-dd HH:mm:ss字符串 * @return Date 日期 ,转换异常时返回null。 */ public static Date parseDate(String dateStr) { return parseDate(dateStr, "yyyy-MM-dd HH:mm:ss"); } /** * 将指定的日期转换为一定格式的字符串 */ public static String formatDate(Date date, String format) { SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.PRC); return sdf.format(date); } /** * 将当前日期转换为一定格式的字符串 */ public static String formatDate(String format) { return formatDate(Calendar.getInstance(Locale.CHINA).getTime(), format); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/FileUtils.java ================================================ package com.kunfei.bookshelf.widget.filepicker.util; import android.webkit.MimeTypeMap; import androidx.annotation.IntDef; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.regex.Pattern; /** * 文件处理 * * * * @author 李玉江[QQ:1023694760] * @since 2014-4-18 */ public final class FileUtils { public static final int BY_NAME_ASC = 0; public static final int BY_NAME_DESC = 1; public static final int BY_TIME_ASC = 2; public static final int BY_TIME_DESC = 3; public static final int BY_SIZE_ASC = 4; public static final int BY_SIZE_DESC = 5; public static final int BY_EXTENSION_ASC = 6; public static final int BY_EXTENSION_DESC = 7; @IntDef(value = { BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC }) @Retention(RetentionPolicy.SOURCE) public @interface SortType { } /** * 将目录分隔符统一为平台默认的分隔符,并为目录结尾添加分隔符 */ public static String separator(String path) { String separator = File.separator; path = path.replace("\\", separator); if (!path.endsWith(separator)) { path += separator; } return path; } public static void closeSilently(Closeable c) { if (c == null) { return; } try { c.close(); } catch (IOException ignored) { } } /** * 列出指定目录下的所有子目录 */ public static File[] listDirs(String startDirPath, String[] excludeDirs, @SortType int sortType) { ArrayList dirList = new ArrayList(); File startDir = new File(startDirPath); if (!startDir.isDirectory()) { return new File[0]; } File[] dirs = startDir.listFiles(new FileFilter() { public boolean accept(File f) { if (f == null) { return false; } //noinspection RedundantIfStatement if (f.isDirectory()) { return true; } return false; } }); if (dirs == null) { return new File[0]; } if (excludeDirs == null) { excludeDirs = new String[0]; } for (File dir : dirs) { File file = dir.getAbsoluteFile(); if (!ConvertUtils.toString(excludeDirs).contains(file.getName())) { dirList.add(file); } } if (sortType == BY_NAME_ASC) { Collections.sort(dirList, new SortByName()); } else if (sortType == BY_NAME_DESC) { Collections.sort(dirList, new SortByName()); Collections.reverse(dirList); } else if (sortType == BY_TIME_ASC) { Collections.sort(dirList, new SortByTime()); } else if (sortType == BY_TIME_DESC) { Collections.sort(dirList, new SortByTime()); Collections.reverse(dirList); } else if (sortType == BY_SIZE_ASC) { Collections.sort(dirList, new SortBySize()); } else if (sortType == BY_SIZE_DESC) { Collections.sort(dirList, new SortBySize()); Collections.reverse(dirList); } else if (sortType == BY_EXTENSION_ASC) { Collections.sort(dirList, new SortByExtension()); } else if (sortType == BY_EXTENSION_DESC) { Collections.sort(dirList, new SortByExtension()); Collections.reverse(dirList); } return dirList.toArray(new File[dirList.size()]); } /** * 列出指定目录下的所有子目录 */ public static File[] listDirs(String startDirPath, String[] excludeDirs) { return listDirs(startDirPath, excludeDirs, BY_NAME_ASC); } /** * 列出指定目录下的所有子目录 */ public static File[] listDirs(String startDirPath) { return listDirs(startDirPath, null, BY_NAME_ASC); } /** * 列出指定目录下的所有子目录及所有文件 */ public static File[] listDirsAndFiles(String startDirPath, String[] allowExtensions) { File[] dirs, files, dirsAndFiles; dirs = listDirs(startDirPath); if (allowExtensions == null) { files = listFiles(startDirPath); } else { files = listFiles(startDirPath, allowExtensions); } if (dirs == null || files == null) { return null; } dirsAndFiles = new File[dirs.length + files.length]; System.arraycopy(dirs, 0, dirsAndFiles, 0, dirs.length); System.arraycopy(files, 0, dirsAndFiles, dirs.length, files.length); return dirsAndFiles; } /** * 列出指定目录下的所有子目录及所有文件 */ public static File[] listDirsAndFiles(String startDirPath) { return listDirsAndFiles(startDirPath, null); } /** * 列出指定目录下的所有文件 */ public static File[] listFiles(String startDirPath, final Pattern filterPattern, @SortType int sortType) { ArrayList fileList = new ArrayList(); File f = new File(startDirPath); if (!f.isDirectory()) { return new File[0]; } File[] files = f.listFiles(new FileFilter() { public boolean accept(File f) { if (f == null) { return false; } if (f.isDirectory()) { return false; } //noinspection SimplifiableIfStatement if (filterPattern == null) { return true; } return filterPattern.matcher(f.getName()).find(); } }); if (files == null) { return new File[0]; } for (File file : files) { fileList.add(file.getAbsoluteFile()); } if (sortType == BY_NAME_ASC) { Collections.sort(fileList, new SortByName()); } else if (sortType == BY_NAME_DESC) { Collections.sort(fileList, new SortByName()); Collections.reverse(fileList); } else if (sortType == BY_TIME_ASC) { Collections.sort(fileList, new SortByTime()); } else if (sortType == BY_TIME_DESC) { Collections.sort(fileList, new SortByTime()); Collections.reverse(fileList); } else if (sortType == BY_SIZE_ASC) { Collections.sort(fileList, new SortBySize()); } else if (sortType == BY_SIZE_DESC) { Collections.sort(fileList, new SortBySize()); Collections.reverse(fileList); } else if (sortType == BY_EXTENSION_ASC) { Collections.sort(fileList, new SortByExtension()); } else if (sortType == BY_EXTENSION_DESC) { Collections.sort(fileList, new SortByExtension()); Collections.reverse(fileList); } return fileList.toArray(new File[fileList.size()]); } /** * 列出指定目录下的所有文件 */ public static File[] listFiles(String startDirPath, Pattern filterPattern) { return listFiles(startDirPath, filterPattern, BY_NAME_ASC); } /** * 列出指定目录下的所有文件 */ public static File[] listFiles(String startDirPath) { return listFiles(startDirPath, null, BY_NAME_ASC); } /** * 列出指定目录下的所有文件 */ public static File[] listFiles(String startDirPath, final String[] allowExtensions) { File file = new File(startDirPath); return file.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { //返回当前目录所有以某些扩展名结尾的文件 String extension = FileUtils.getExtension(name); return ConvertUtils.toString(allowExtensions).contains(extension); } }); } /** * 列出指定目录下的所有文件 */ public static File[] listFiles(String startDirPath, String allowExtension) { return listFiles(startDirPath, new String[]{allowExtension}); } /** * 判断文件或目录是否存在 */ public static boolean exist(String path) { File file = new File(path); return file.exists(); } /** * 删除文件或目录 */ public static boolean delete(File file, boolean deleteRootDir) { boolean result = false; if (file.isFile()) { //是文件 result = deleteResolveEBUSY(file); } else { //是目录 File[] files = file.listFiles(); if (files == null) { return false; } if (files.length == 0) { result = deleteRootDir && deleteResolveEBUSY(file); } else { for (File f : files) { delete(f, deleteRootDir); result = deleteResolveEBUSY(f); } } if (deleteRootDir) { result = deleteResolveEBUSY(file); } } return result; } /** * bug: open failed: EBUSY (Device or resource busy) * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy */ private static boolean deleteResolveEBUSY(File file) { // Before you delete a Directory or File: rename it! final File to = new File(file.getAbsolutePath() + System.currentTimeMillis()); //noinspection ResultOfMethodCallIgnored file.renameTo(to); return to.delete(); } /** * 删除文件或目录 */ public static boolean delete(String path, boolean deleteRootDir) { File file = new File(path); //noinspection SimplifiableIfStatement if (file.exists()) { return delete(file, deleteRootDir); } return false; } /** * 删除文件或目录, 不删除最顶层目录 */ public static boolean delete(String path) { return delete(path, false); } /** * 删除文件或目录, 不删除最顶层目录 */ public static boolean delete(File file) { return delete(file, false); } /** * 复制文件为另一个文件,或复制某目录下的所有文件及目录到另一个目录下 */ public static boolean copy(String src, String tar) { File srcFile = new File(src); return srcFile.exists() && copy(srcFile, new File(tar)); } /** * 复制文件或目录 */ public static boolean copy(File src, File tar) { try { if (src.isFile()) { InputStream is = new FileInputStream(src); OutputStream op = new FileOutputStream(tar); BufferedInputStream bis = new BufferedInputStream(is); BufferedOutputStream bos = new BufferedOutputStream(op); byte[] bt = new byte[1024 * 8]; while (true) { int len = bis.read(bt); if (len == -1) { break; } else { bos.write(bt, 0, len); } } bis.close(); bos.close(); } else if (src.isDirectory()) { File[] files = src.listFiles(); //noinspection ResultOfMethodCallIgnored tar.mkdirs(); for (File file : files) { copy(file.getAbsoluteFile(), new File(tar.getAbsoluteFile(), file.getName())); } } return true; } catch (Exception e) { return false; } } /** * 移动文件或目录 */ public static boolean move(String src, String tar) { return move(new File(src), new File(tar)); } /** * 移动文件或目录 */ public static boolean move(File src, File tar) { return rename(src, tar); } /** * 文件重命名 */ public static boolean rename(String oldPath, String newPath) { return rename(new File(oldPath), new File(newPath)); } /** * 文件重命名 */ public static boolean rename(File src, File tar) { return src.renameTo(tar); } /** * 读取文本文件, 失败将返回空串 */ public static String readText(String filepath, String charset) { try { byte[] data = readBytes(filepath); if (data != null) { return new String(data, charset).trim(); } } catch (UnsupportedEncodingException ignored) { } return ""; } /** * 读取文本文件, 失败将返回空串 */ public static String readText(String filepath) { return readText(filepath, "utf-8"); } /** * 读取文件内容, 失败将返回空串 */ public static byte[] readBytes(String filepath) { FileInputStream fis = null; try { fis = new FileInputStream(filepath); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; while (true) { int len = fis.read(buffer, 0, buffer.length); if (len == -1) { break; } else { baos.write(buffer, 0, len); } } byte[] data = baos.toByteArray(); baos.close(); return data; } catch (IOException e) { return null; } finally { closeSilently(fis); } } /** * 保存文本内容 */ public static boolean writeText(String filepath, String content, String charset) { try { writeBytes(filepath, content.getBytes(charset)); return true; } catch (UnsupportedEncodingException e) { return false; } } /** * 保存文本内容 */ public static boolean writeText(String filepath, String content) { return writeText(filepath, content, "utf-8"); } /** * 保存文件内容 */ public static boolean writeBytes(String filepath, byte[] data) { File file = new File(filepath); FileOutputStream fos = null; try { if (!file.exists()) { //noinspection ResultOfMethodCallIgnored file.getParentFile().mkdirs(); //noinspection ResultOfMethodCallIgnored file.createNewFile(); } fos = new FileOutputStream(filepath); fos.write(data); return true; } catch (IOException e) { return false; } finally { closeSilently(fos); } } /** * 追加文本内容 */ public static boolean appendText(String path, String content) { File file = new File(path); FileWriter writer = null; try { if (!file.exists()) { //noinspection ResultOfMethodCallIgnored file.createNewFile(); } writer = new FileWriter(file, true); writer.write(content); return true; } catch (IOException e) { return false; } finally { closeSilently(writer); } } /** * 获取文件大小 */ public static long getLength(String path) { File file = new File(path); if (!file.isFile() || !file.exists()) { return 0; } return file.length(); } /** * 获取文件或网址的名称(包括后缀) */ public static String getName(String pathOrUrl) { if (pathOrUrl == null) { return ""; } int pos = pathOrUrl.lastIndexOf('/'); if (0 <= pos) { return pathOrUrl.substring(pos + 1); } else { return String.valueOf(System.currentTimeMillis()) + "." + getExtension(pathOrUrl); } } /** * 获取文件名(不包括扩展名) */ public static String getNameExcludeExtension(String path) { try { String fileName = (new File(path)).getName(); int lastIndexOf = fileName.lastIndexOf("."); if (lastIndexOf != -1) { fileName = fileName.substring(0, lastIndexOf); } return fileName; } catch (Exception e) { return ""; } } /** * 获取格式化后的文件大小 */ public static String getSize(String path) { long fileSize = getLength(path); return ConvertUtils.toFileSizeString(fileSize); } /** * 获取文件后缀,不包括“.” */ public static String getExtension(String pathOrUrl) { int dotPos = pathOrUrl.lastIndexOf('.'); if (0 <= dotPos) { return pathOrUrl.substring(dotPos + 1); } else { return "ext"; } } /** * 获取文件的MIME类型 */ public static String getMimeType(String pathOrUrl) { String ext = getExtension(pathOrUrl); MimeTypeMap map = MimeTypeMap.getSingleton(); String mimeType; if (map.hasExtension(ext)) { mimeType = map.getMimeTypeFromExtension(ext); } else { mimeType = "*/*"; } return mimeType; } /** * 获取格式化后的文件/目录创建或最后修改时间 */ public static String getDateTime(String path) { return getDateTime(path, "yyyy年MM月dd日HH:mm"); } /** * 获取格式化后的文件/目录创建或最后修改时间 */ public static String getDateTime(String path, String format) { File file = new File(path); return getDateTime(file, format); } /** * 获取格式化后的文件/目录创建或最后修改时间 */ public static String getDateTime(File file, String format) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(file.lastModified()); return DateUtils.formatDate(cal.getTime(), format); } /** * 比较两个文件的最后修改时间 */ public static int compareLastModified(String path1, String path2) { long stamp1 = (new File(path1)).lastModified(); long stamp2 = (new File(path2)).lastModified(); if (stamp1 > stamp2) { return 1; } else if (stamp1 < stamp2) { return -1; } else { return 0; } } /** * 创建多级别的目录 */ public static boolean makeDirs(String path) { return makeDirs(new File(path)); } /** * 创建多级别的目录 */ public static boolean makeDirs(File file) { return file.mkdirs(); } public static class SortByExtension implements Comparator { public SortByExtension() { super(); } public int compare(File f1, File f2) { if (f1 == null || f2 == null) { if (f1 == null) { return -1; } else { return 1; } } else { if (f1.isDirectory() && f2.isFile()) { return -1; } else if (f1.isFile() && f2.isDirectory()) { return 1; } else { return f1.getName().compareToIgnoreCase(f2.getName()); } } } } public static class SortByName implements Comparator { private boolean caseSensitive; public SortByName(boolean caseSensitive) { this.caseSensitive = caseSensitive; } public SortByName() { this.caseSensitive = false; } public int compare(File f1, File f2) { if (f1 == null || f2 == null) { if (f1 == null) { return -1; } else { return 1; } } else { if (f1.isDirectory() && f2.isFile()) { return -1; } else if (f1.isFile() && f2.isDirectory()) { return 1; } else { String s1 = f1.getName(); String s2 = f2.getName(); if (caseSensitive) { return s1.compareTo(s2); } else { return s1.compareToIgnoreCase(s2); } } } } } public static class SortBySize implements Comparator { public SortBySize() { super(); } public int compare(File f1, File f2) { if (f1 == null || f2 == null) { if (f1 == null) { return -1; } else { return 1; } } else { if (f1.isDirectory() && f2.isFile()) { return -1; } else if (f1.isFile() && f2.isDirectory()) { return 1; } else { if (f1.length() < f2.length()) { return -1; } else { return 1; } } } } } public static class SortByTime implements Comparator { public SortByTime() { super(); } public int compare(File f1, File f2) { if (f1 == null || f2 == null) { if (f1 == null) { return -1; } else { return 1; } } else { if (f1.isDirectory() && f2.isFile()) { return -1; } else if (f1.isFile() && f2.isDirectory()) { return 1; } else { if (f1.lastModified() > f2.lastModified()) { return -1; } else { return 1; } } } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/ScreenUtils.java ================================================ package com.kunfei.bookshelf.widget.filepicker.util; import android.app.Activity; import android.content.Context; import android.util.DisplayMetrics; import android.view.Window; import android.view.WindowManager; /** * 获取屏幕宽高等信息、全屏切换、保持屏幕常亮、截屏等 * * @author liyujiang[QQ:1032694760] * @since 2015/11/26 */ public final class ScreenUtils { private static boolean isFullScreen = false; private static DisplayMetrics dm = null; public static DisplayMetrics displayMetrics(Context context) { if (null != dm) { return dm; } DisplayMetrics dm = new DisplayMetrics(); WindowManager windowManager = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); windowManager.getDefaultDisplay().getMetrics(dm); return dm; } public static int widthPixels(Context context) { return displayMetrics(context).widthPixels; } public static int heightPixels(Context context) { return displayMetrics(context).heightPixels; } public static float density(Context context) { return displayMetrics(context).density; } public static int densityDpi(Context context) { return displayMetrics(context).densityDpi; } public static boolean isFullScreen() { return isFullScreen; } public static void toggleFullScreen(Activity activity) { Window window = activity.getWindow(); int flagFullscreen = WindowManager.LayoutParams.FLAG_FULLSCREEN; if (isFullScreen) { window.clearFlags(flagFullscreen); isFullScreen = false; } else { window.setFlags(flagFullscreen, flagFullscreen); isFullScreen = true; } } /** * 保持屏幕常亮 */ public static void keepBright(Activity activity) { //需在setContentView前调用 int keepScreenOn = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; activity.getWindow().setFlags(keepScreenOn, keepScreenOn); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/StorageUtils.java ================================================ package com.kunfei.bookshelf.widget.filepicker.util; import android.content.Context; import android.os.Environment; import android.text.TextUtils; import java.io.File; import java.io.IOException; /** * 存储设备工具类 * * * * @author 李玉江[QQ:1023694760] * @since 2013-11-2 */ public final class StorageUtils { /** * 判断外置存储是否可用 * * @return the boolean */ public static boolean externalMounted() { String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { return true; } return false; } /** * 返回以“/”结尾的内部存储根目录 */ public static String getInternalRootPath(Context context, String type) { File file; if (TextUtils.isEmpty(type)) { file = context.getFilesDir(); } else { file = new File(FileUtils.separator(context.getFilesDir().getAbsolutePath()) + type); //noinspection ResultOfMethodCallIgnored file.mkdirs(); } String path = ""; if (file != null) { path = FileUtils.separator(file.getAbsolutePath()); } return path; } public static String getInternalRootPath(Context context) { return getInternalRootPath(context, null); } /** * 返回以“/”结尾的外部存储根目录,外置卡不可用则返回空字符串 */ public static String getExternalRootPath(String type) { File file = null; if (externalMounted()) { file = Environment.getExternalStorageDirectory(); } if (file != null && !TextUtils.isEmpty(type)) { file = new File(file, type); //noinspection ResultOfMethodCallIgnored file.mkdirs(); } String path = ""; if (file != null) { path = FileUtils.separator(file.getAbsolutePath()); } return path; } public static String getExternalRootPath() { return getExternalRootPath(null); } /** * 各种类型的文件的专用的保存路径,以“/”结尾 * * @return 诸如:/mnt/sdcard/Android/data/[package]/files/[type]/ */ public static String getExternalPrivatePath(Context context, String type) { File file = null; if (externalMounted()) { file = context.getExternalFilesDir(type); } //高频触发java.lang.NullPointerException,是SD卡不可用或暂时繁忙么? String path = ""; if (file != null) { path = FileUtils.separator(file.getAbsolutePath()); } return path; } public static String getExternalPrivatePath(Context context) { return getExternalPrivatePath(context, null); } /** * 下载的文件的保存路径,必须为外部存储,以“/”结尾 * * @return 诸如 :/mnt/sdcard/Download/ */ public static String getDownloadPath() throws RuntimeException { File file; if (externalMounted()) { file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); } else { throw new RuntimeException("外置存储不可用!"); } return FileUtils.separator(file.getAbsolutePath()); } /** * 各种类型的文件的专用的缓存存储保存路径,优先使用外置存储,以“/”结尾 */ public static String getCachePath(Context context, String type) { File file; if (externalMounted()) { file = context.getExternalCacheDir(); } else { file = context.getCacheDir(); } if (!TextUtils.isEmpty(type)) { file = new File(file, type); //noinspection ResultOfMethodCallIgnored file.mkdirs(); } String path = ""; if (file != null) { path = FileUtils.separator(file.getAbsolutePath()); } return path; } public static String getCachePath(Context context) { return getCachePath(context, null); } /** * 返回以“/”结尾的临时存储目录 */ public static String getTempDirPath(Context context) { return getExternalPrivatePath(context, "temporary"); } /** * 返回临时存储文件路径 */ public static String getTempFilePath(Context context) { try { return File.createTempFile("lyj_", ".tmp", context.getCacheDir()).getAbsolutePath(); } catch (IOException e) { return getTempDirPath(context) + "lyj.tmp"; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/font/FontAdapter.java ================================================ package com.kunfei.bookshelf.widget.font; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Typeface; import android.os.Build; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.FileDoc; import com.kunfei.bookshelf.utils.RealPathUtil; import java.io.File; import java.io.FileDescriptor; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; public class FontAdapter extends RecyclerView.Adapter { private final List docList = new ArrayList<>(); private final FontSelector.OnThisListener thisListener; private final Context context; private String selectName; FontAdapter(Context context, String selectPath, FontSelector.OnThisListener thisListener) { this.context = context; try { String[] x = URLDecoder.decode(selectPath, "utf-8") .split(File.separator); this.selectName = x[x.length - 1]; } catch (Exception e) { this.selectName = ""; } this.thisListener = thisListener; } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_font, parent, false)); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { if (docList.size() > 0) { FileDoc docItem = docList.get(position); try { Typeface typeface; if (docItem.isContentScheme()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { FileDescriptor fd = context.getContentResolver().openFileDescriptor(docItem.getUri(), "r") .getFileDescriptor(); typeface = new Typeface.Builder(fd).build(); } else { typeface = Typeface.createFromFile(RealPathUtil.getPath(context, docItem.getUri())); } } else { typeface = Typeface.createFromFile(docItem.getUri().toString()); } holder.tvFont.setTypeface(typeface); } catch (Exception ignored) { } holder.tvFont.setText(docItem.getName()); if (docItem.getName().equals(selectName)) { holder.ivChecked.setVisibility(View.VISIBLE); } else { holder.ivChecked.setVisibility(View.INVISIBLE); } holder.tvFont.setOnClickListener(view -> { if (thisListener != null) { thisListener.setFontPath(docItem); } }); } else { holder.tvFont.setText(R.string.fonts_folder); } } @Override public int getItemCount() { return docList.size() == 0 ? 1 : docList.size(); } @SuppressLint("NotifyDataSetChanged") void upData(List docItems) { if (docItems != null) { docList.clear(); docList.addAll(docItems); } notifyDataSetChanged(); } static class MyViewHolder extends RecyclerView.ViewHolder { TextView tvFont; ImageView ivChecked; MyViewHolder(View itemView) { super(itemView); tvFont = itemView.findViewById(R.id.tv_font); ivChecked = itemView.findViewById(R.id.iv_checked); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/font/FontSelector.java ================================================ package com.kunfei.bookshelf.widget.font; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.FileDoc; import com.kunfei.bookshelf.utils.theme.ATH; import java.util.List; import kotlin.text.Regex; public class FontSelector { private final AlertDialog.Builder builder; private final FontAdapter adapter; private OnThisListener thisListener; private AlertDialog alertDialog; public static Regex fontRegex = new Regex("(?i).*\\.[ot]tf"); public FontSelector(Context context, String selectPath) { builder = new AlertDialog.Builder(context, R.style.alertDialogTheme); @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.view_recycler_font, null); RecyclerView recyclerView = view.findViewById(R.id.recycler_view); builder.setView(view); builder.setTitle(R.string.select_font); builder.setNegativeButton(R.string.cancel, null); adapter = new FontAdapter(context, selectPath, new OnThisListener() { @Override public void setDefault() { if (thisListener != null) { thisListener.setDefault(); } alertDialog.dismiss(); } @Override public void setFontPath(FileDoc fileDoc) { if (thisListener != null) { thisListener.setFontPath(fileDoc); } alertDialog.dismiss(); } }); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(new LinearLayoutManager(context)); } public FontSelector setListener(OnThisListener thisListener) { this.thisListener = thisListener; builder.setPositiveButton(R.string.default_font, ((dialogInterface, i) -> thisListener.setDefault())); return this; } public FontSelector create(List docItems) { adapter.upData(docItems); builder.create(); return this; } public void show() { alertDialog = builder.show(); ATH.setAlertDialogTint(alertDialog); } public interface OnThisListener { void setDefault(); void setFontPath(FileDoc fileDoc); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/image/CoverImageView.kt ================================================ package com.kunfei.bookshelf.widget.image 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 com.kunfei.bookshelf.R import com.kunfei.bookshelf.help.glide.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.drawable.image_cover_default) .error(R.drawable.image_cover_default) .listener(object : RequestListener { override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean ): Boolean { e?.printStackTrace() 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) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/image/FilletImageView.java ================================================ package com.kunfei.bookshelf.widget.image; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Path; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatImageView; import com.kunfei.bookshelf.R; public class FilletImageView extends AppCompatImageView { float width, height; private int leftTopRadius; private int rightTopRadius; private int rightBottomRadius; private int leftBottomRadius; public FilletImageView(Context context) { super(context); } public FilletImageView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public FilletImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { // 读取配置 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView); int defaultRadius = 5; int radius = array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius); leftTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_top_radius, defaultRadius); rightTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_top_radius, defaultRadius); rightBottomRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_bottom_radius, defaultRadius); leftBottomRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_bottom_radius, defaultRadius); //如果四个角的值没有设置,那么就使用通用的radius的值。 if (defaultRadius == leftTopRadius) { leftTopRadius = radius; } if (defaultRadius == rightTopRadius) { rightTopRadius = radius; } if (defaultRadius == rightBottomRadius) { rightBottomRadius = radius; } if (defaultRadius == leftBottomRadius) { leftBottomRadius = radius; } array.recycle(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); width = getWidth(); height = getHeight(); } @Override protected void onDraw(Canvas canvas) { //这里做下判断,只有图片的宽高大于设置的圆角距离的时候才进行裁剪 int maxLeft = Math.max(leftTopRadius, leftBottomRadius); int maxRight = Math.max(rightTopRadius, rightBottomRadius); int minWidth = maxLeft + maxRight; int maxTop = Math.max(leftTopRadius, rightTopRadius); int maxBottom = Math.max(leftBottomRadius, rightBottomRadius); int minHeight = maxTop + maxBottom; if (width >= minWidth && height > minHeight) { @SuppressLint("DrawAllocation") Path path = new Path(); //四个角:右上,右下,左下,左上 path.moveTo(leftTopRadius, 0); path.lineTo(width - rightTopRadius, 0); path.quadTo(width, 0, width, rightTopRadius); path.lineTo(width, height - rightBottomRadius); path.quadTo(width, height, width - rightBottomRadius, height); path.lineTo(leftBottomRadius, height); path.quadTo(0, height, 0, height - leftBottomRadius); path.lineTo(0, leftTopRadius); path.quadTo(0, 0, leftTopRadius, 0); canvas.clipPath(path); } super.onDraw(canvas); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/itemdecoration/DividerGridItemDecoration.java ================================================ package com.kunfei.bookshelf.widget.itemdecoration; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.View; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; /** * Created by newbiechen on 2017/10/8. */ public class DividerGridItemDecoration extends RecyclerView.ItemDecoration { private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; private Drawable mWidthDivider; private Drawable mHeightDivider; public DividerGridItemDecoration(Context context) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mWidthDivider = a.getDrawable(0); mHeightDivider = mWidthDivider; a.recycle(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public DividerGridItemDecoration(Context context, @DrawableRes int widthDividerRes, @DrawableRes int heightDividerRes) { mWidthDivider = context.getDrawable(widthDividerRes); mHeightDivider = context.getDrawable(heightDividerRes); } @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { drawHorizontal(c, parent); drawVertical(c, parent); } private int getSpanCount(RecyclerView parent) { // 列数 int spanCount = -1; RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { spanCount = ((GridLayoutManager) layoutManager).getSpanCount(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { spanCount = ((StaggeredGridLayoutManager) layoutManager) .getSpanCount(); } return spanCount; } public void drawHorizontal(Canvas c, RecyclerView parent) { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int left = child.getLeft() - params.leftMargin; final int right = child.getRight() + params.rightMargin + mWidthDivider.getIntrinsicWidth(); final int top = child.getBottom() + params.bottomMargin; final int bottom = top + mWidthDivider.getIntrinsicHeight(); mWidthDivider.setBounds(left, top, right, bottom); mWidthDivider.draw(c); } } public void drawVertical(Canvas c, RecyclerView parent) { final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getTop() - params.topMargin; final int bottom = child.getBottom() + params.bottomMargin; final int left = child.getRight() + params.rightMargin; final int right = left + mHeightDivider.getIntrinsicWidth(); mHeightDivider.setBounds(left, top, right, bottom); mHeightDivider.draw(c); } } private boolean isLastColumn(RecyclerView parent, int pos, int spanCount, int childCount) { RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { if ((pos + 1) % spanCount == 0) { return true; } } else if (layoutManager instanceof StaggeredGridLayoutManager) { int orientation = ((StaggeredGridLayoutManager) layoutManager) .getOrientation(); if (orientation == StaggeredGridLayoutManager.VERTICAL) { // 如果是最后一列,则不需要绘制右边 if ((pos + 1) % spanCount == 0) { return true; } } else { childCount = childCount - childCount % spanCount; // 如果是最后一列,则不需要绘制右边 if (pos >= childCount) return true; } } return false; } private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) { RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { childCount = childCount - childCount % spanCount; if (pos >= childCount)// 如果是最后一行,则不需要绘制底部 return true; } else if (layoutManager instanceof StaggeredGridLayoutManager) { int orientation = ((StaggeredGridLayoutManager) layoutManager) .getOrientation(); // StaggeredGridLayoutManager 且纵向滚动 if (orientation == StaggeredGridLayoutManager.VERTICAL) { childCount = childCount - childCount % spanCount; // 如果是最后一行,则不需要绘制底部 if (pos >= childCount) return true; } else { // 如果是最后一行,则不需要绘制底部 if ((pos + 1) % spanCount == 0) { return true; } } } return false; } @Override public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { int spanCount = getSpanCount(parent); int childCount = parent.getAdapter().getItemCount(); // 如果是最后一行,则不需要绘制底部 if (isLastRaw(parent, itemPosition, spanCount, childCount)) { outRect.set(0, 0, mHeightDivider.getIntrinsicWidth(), 0); } // 如果是最后一列,则不需要绘制右边 else if (isLastColumn(parent, itemPosition, spanCount, childCount)) { outRect.set(0, 0, 0, mWidthDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mHeightDivider.getIntrinsicWidth(), mWidthDivider.getIntrinsicHeight()); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/itemdecoration/DividerItemDecoration.java ================================================ package com.kunfei.bookshelf.widget.itemdecoration; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.View; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; /** * Created by newbiechen on 2017/10/8. */ public class DividerItemDecoration extends RecyclerView.ItemDecoration { public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; private static final String TAG = "DividerItemDecoration"; private static final int[] ATTRS = new int[]{ android.R.attr.listDivider }; private Drawable mDrawable; public DividerItemDecoration(Context context) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mDrawable = a.getDrawable(0); a.recycle(); } @Override public void onDraw(Canvas c, RecyclerView parent) { if (getLayoutManagerType(parent) == VERTICAL_LIST) { drawVertical(c, parent); } else { drawHorizontal(c, parent); } } private int getLayoutManagerType(RecyclerView rv) { RecyclerView.LayoutManager manager = rv.getLayoutManager(); if (!(manager instanceof LinearLayoutManager)) { throw new IllegalArgumentException("only supply linearLayoutManager"); } return ((LinearLayoutManager) manager).getOrientation(); } public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin; final int bottom = top + mDrawable.getIntrinsicHeight(); mDrawable.setBounds(left, top, right, bottom); mDrawable.draw(c); } } public void drawHorizontal(Canvas c, RecyclerView parent) { final int top = parent.getPaddingTop(); final int bottom = parent.getHeight() - parent.getPaddingBottom(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int left = child.getRight() + params.rightMargin; final int right = left + mDrawable.getIntrinsicHeight(); mDrawable.setBounds(left, top, right, bottom); mDrawable.draw(c); } } @Override public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { if (getLayoutManagerType(parent) == VERTICAL_LIST) { outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight()); } else { outRect.set(0, 0, mDrawable.getIntrinsicWidth(), 0); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/BaseDialog.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.app.Dialog; import android.content.Context; import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.kunfei.bookshelf.utils.SoftInputUtil; public class BaseDialog extends Dialog { public BaseDialog(@NonNull Context context) { super(context); } public BaseDialog(@NonNull Context context, int themeResId) { super(context, themeResId); } protected BaseDialog(@NonNull Context context, boolean cancelable, @Nullable OnCancelListener cancelListener) { super(context, cancelable, cancelListener); } @Override public void dismiss() { View view = getCurrentFocus(); if (view instanceof TextView) { SoftInputUtil.hideIMM(view); } super.dismiss(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/BookmarkDialog.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import android.widget.TextView; import androidx.annotation.NonNull; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookmarkBean; public class BookmarkDialog extends BaseDialog { private Context context; private TextView tvChapterName; private EditText tvContent; private View llEdit; private View tvOk; private BookmarkBean bookmarkBean; private View tvSave; private View tvDel; public static BookmarkDialog builder(Context context, @NonNull BookmarkBean bookmarkBean, boolean isAdd) { return new BookmarkDialog(context, bookmarkBean, isAdd); } private BookmarkDialog(Context context, @NonNull BookmarkBean bookmarkBean, boolean isAdd) { super(context, R.style.alertDialogTheme); this.context = context; this.bookmarkBean = bookmarkBean; @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_bookmark, null); bindView(view); setContentView(view); tvChapterName.setText(bookmarkBean.getChapterName()); tvContent.setText(bookmarkBean.getContent()); if (isAdd) { llEdit.setVisibility(View.GONE); tvOk.setVisibility(View.VISIBLE); } else { llEdit.setVisibility(View.VISIBLE); tvOk.setVisibility(View.GONE); } } private void bindView(View view) { tvChapterName = view.findViewById(R.id.tvChapterName); tvContent = view.findViewById(R.id.tie_content); tvOk = view.findViewById(R.id.tv_ok); tvSave = view.findViewById(R.id.tv_save); tvDel = view.findViewById(R.id.tv_del); llEdit = view.findViewById(R.id.llEdit); } public BookmarkDialog setPositiveButton(Callback callback) { tvChapterName.setOnClickListener(v -> { callback.openChapter(bookmarkBean.getChapterIndex(), bookmarkBean.getPageIndex()); dismiss(); }); tvOk.setOnClickListener(v -> { bookmarkBean.setContent(tvContent.getText().toString()); callback.saveBookmark(bookmarkBean); dismiss(); }); tvSave.setOnClickListener(v -> { bookmarkBean.setContent(tvContent.getText().toString()); callback.saveBookmark(bookmarkBean); dismiss(); }); tvDel.setOnClickListener(v -> { callback.delBookmark(bookmarkBean); dismiss(); }); return this; } public interface Callback { void saveBookmark(BookmarkBean bookmarkBean); void delBookmark(BookmarkBean bookmarkBean); void openChapter(int chapterIndex, int pageIndex); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/ChangeSourceDialog.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.annotation.SuppressLint; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.widget.SearchView; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import com.hwangjr.rxbus.RxBus; import com.hwangjr.rxbus.annotation.Subscribe; import com.hwangjr.rxbus.annotation.Tag; import com.hwangjr.rxbus.thread.EventThread; import com.kunfei.bookshelf.DbHelper; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.BookSourceBean; import com.kunfei.bookshelf.bean.SearchBookBean; import com.kunfei.bookshelf.constant.RxBusTag; import com.kunfei.bookshelf.dao.SearchBookBeanDao; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.BookSourceManager; import com.kunfei.bookshelf.model.SearchBookModel; import com.kunfei.bookshelf.model.UpLastChapterModel; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.view.activity.SourceEditActivity; import com.kunfei.bookshelf.view.adapter.ChangeSourceAdapter; import com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerView; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.SingleOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class ChangeSourceDialog extends BaseDialog implements ChangeSourceAdapter.CallBack { private Context context; private TextView atvTitle; private ImageButton ibtStop; private SearchView searchView; private RefreshRecyclerView rvSource; private Handler handler = new Handler(Looper.getMainLooper()); private ChangeSourceAdapter adapter; private SearchBookModel searchBookModel; private BookShelfBean book; private String bookTag; private String bookName; private String bookAuthor; private int shelfLastChapter; private CompositeDisposable compositeDisposable; private Callback callback; public static ChangeSourceDialog builder(Context context, BookShelfBean bookShelfBean) { return new ChangeSourceDialog(context, bookShelfBean); } private ChangeSourceDialog(@NonNull Context context, BookShelfBean bookShelfBean) { super(context, R.style.alertDialogTheme); this.context = context; init(bookShelfBean); } private void init(BookShelfBean bookShelf) { this.book = bookShelf; compositeDisposable = new CompositeDisposable(); @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_change_source, null); bindView(view); setContentView(view); initData(); } private void bindView(View view) { View llContent = view.findViewById(R.id.ll_content); llContent.setOnClickListener(null); searchView = view.findViewById(R.id.searchView); atvTitle = view.findViewById(R.id.atv_title); ibtStop = view.findViewById(R.id.ibt_stop); rvSource = view.findViewById(R.id.rf_rv_change_source); ibtStop.setVisibility(View.INVISIBLE); rvSource.addItemDecoration(new DividerItemDecoration(context, LinearLayout.VERTICAL)); rvSource.setBaseRefreshListener(this::reSearchBook); ibtStop.setOnClickListener(v -> stopChangeSource()); searchView.onActionViewExpanded(); searchView.clearFocus(); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { if (StringUtils.isTrimEmpty(newText)) { List searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder() .where(SearchBookBeanDao.Properties.Name.eq(bookName), SearchBookBeanDao.Properties.Author.eq(bookAuthor)) .build().list(); adapter.reSetSourceAdapter(); adapter.addAllSourceAdapter(searchBookBeans); } else { List searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder() .where(SearchBookBeanDao.Properties.Name.eq(bookName), SearchBookBeanDao.Properties.Author.eq(bookAuthor), SearchBookBeanDao.Properties.Origin.like("%" + searchView.getQuery().toString() + "%")) .build().list(); adapter.reSetSourceAdapter(); adapter.addAllSourceAdapter(searchBookBeans); } return false; } }); } @Override public void changeTo(SearchBookBean searchBookBean) { if (!Objects.equals(book.getNoteUrl(), searchBookBean.getNoteUrl())) { callback.changeSource(searchBookBean); } dismiss(); } @Override public void showMenu(View view, SearchBookBean searchBookBean) { final String url = searchBookBean.getTag(); final BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(url); PopupMenu popupMenu = new PopupMenu(context, view); popupMenu.getMenu().add(0, R.id.menu_disable, 1, "禁用书源"); popupMenu.getMenu().add(0, R.id.menu_del, 2, "删除书源"); popupMenu.getMenu().add(0, R.id.menu_edit, 3, "编辑书源"); popupMenu.setOnMenuItemClickListener(menuItem -> { if (sourceBean != null) { switch (menuItem.getItemId()) { case R.id.menu_disable: sourceBean.setEnable(false); BookSourceManager.addBookSource(sourceBean); adapter.removeData(searchBookBean); Toast.makeText(context, String.format("%s已禁用", sourceBean.getBookSourceName()), Toast.LENGTH_SHORT).show(); break; case R.id.menu_del: BookSourceManager.removeBookSource(sourceBean); adapter.removeData(searchBookBean); Toast.makeText(context, String.format("%s已删除", sourceBean.getBookSourceName()), Toast.LENGTH_SHORT).show(); break; case R.id.menu_edit: SourceEditActivity.startThis(context, sourceBean); break; } } return true; }); popupMenu.show(); } @SuppressLint("InflateParams") private void initData() { adapter = new ChangeSourceAdapter(false); rvSource.setRefreshRecyclerViewAdapter(adapter, new LinearLayoutManager(context)); adapter.setCallBack(this); View viewRefreshError = LayoutInflater.from(context).inflate(R.layout.view_refresh_error, null); viewRefreshError.setBackgroundResource(R.color.background_card); viewRefreshError.findViewById(R.id.tv_refresh_again).setOnClickListener(v -> { //刷新失败 ,重试 reSearchBook(); }); rvSource.setNoDataAndRefreshErrorView(LayoutInflater.from(context).inflate(R.layout.view_refresh_no_data, null), viewRefreshError); SearchBookModel.OnSearchListener searchListener = new SearchBookModel.OnSearchListener() { @Override public void refreshSearchBook() { ibtStop.setVisibility(View.VISIBLE); adapter.reSetSourceAdapter(); } @Override public void refreshFinish(Boolean value) { ibtStop.setVisibility(View.INVISIBLE); rvSource.finishRefresh(true, true); } @Override public void loadMoreFinish(Boolean value) { ibtStop.setVisibility(View.INVISIBLE); rvSource.finishRefresh(true); } @Override public void loadMoreSearchBook(List value) { addSearchBook(value); } @Override public void searchBookError(Throwable throwable) { ibtStop.setVisibility(View.INVISIBLE); if (adapter.getICount() == 0) { rvSource.refreshError(); } } @Override public int getItemCount() { return 0; } }; searchBookModel = new SearchBookModel(searchListener); bookTag = book.getTag(); bookName = book.getBookInfoBean().getName(); bookAuthor = book.getBookInfoBean().getAuthor(); shelfLastChapter = BookshelfHelp.guessChapterNum(book.getLastChapterName()); atvTitle.setText(String.format("%s (%s)", bookName, bookAuthor)); rvSource.startRefresh(); getSearchBookInDb(book); RxBus.get().register(this); setOnDismissListener(dialog -> { RxBus.get().unregister(ChangeSourceDialog.this); compositeDisposable.dispose(); if (searchBookModel != null) { searchBookModel.onDestroy(); } }); } public ChangeSourceDialog setCallback(Callback callback) { this.callback = callback; return this; } public void show() { super.show(); WindowManager.LayoutParams params = Objects.requireNonNull(getWindow()).getAttributes(); params.height = ScreenUtils.getAppSize()[1] - 60; params.width = ScreenUtils.getAppSize()[0] - 60; getWindow().setAttributes(params); } private void getSearchBookInDb(BookShelfBean bookShelf) { Single.create((SingleOnSubscribe>) e -> { List searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder() .where(SearchBookBeanDao.Properties.Name.eq(bookName), SearchBookBeanDao.Properties.Author.eq(bookAuthor)).build().list(); if (searchBookBeans == null) searchBookBeans = new ArrayList<>(); List searchBookList = new ArrayList<>(); List bookSourceList = new ArrayList<>(BookSourceManager.getSelectedBookSource()); if (bookSourceList.size() > 0) { for (BookSourceBean bookSourceBean : BookSourceManager.getSelectedBookSource()) { boolean hasSource = false; for (SearchBookBean searchBookBean : new ArrayList<>(searchBookBeans)) { if (Objects.equals(searchBookBean.getTag(), bookSourceBean.getBookSourceUrl())) { bookSourceList.remove(bookSourceBean); searchBookList.add(searchBookBean); hasSource = true; break; } } if (hasSource) { bookSourceList.remove(bookSourceBean); } } searchBookModel.searchReNew(); searchBookModel.initSearchEngineS(bookSourceList); long startThisSearchTime = System.currentTimeMillis(); searchBookModel.setSearchTime(startThisSearchTime); List bookList = new ArrayList<>(); bookList.add(book); searchBookModel.search(bookName, startThisSearchTime, bookList, false); UpLastChapterModel.getInstance().startUpdate(searchBookList); } if (searchBookList.size() > 0) { for (SearchBookBean searchBookBean : searchBookList) { if (searchBookBean.getTag().equals(bookShelf.getTag())) { searchBookBean.setIsCurrentSource(true); } else { searchBookBean.setIsCurrentSource(false); } } Collections.sort(searchBookList, this::compareSearchBooks); } e.onSuccess(searchBookList); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new SingleObserver>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onSuccess(List searchBookBeans) { if (searchBookBeans.size() > 0) { adapter.addAllSourceAdapter(searchBookBeans); ibtStop.setVisibility(View.INVISIBLE); rvSource.finishRefresh(true, true); } else { reSearchBook(); } } @Override public void onError(Throwable e) { reSearchBook(); } }); } private void reSearchBook() { rvSource.startRefresh(); searchBookModel.initSearchEngineS(BookSourceManager.getSelectedBookSource()); searchBookModel.searchReNew(); long startThisSearchTime = System.currentTimeMillis(); searchBookModel.setSearchTime(startThisSearchTime); List bookList = new ArrayList<>(); bookList.add(book); searchBookModel.search(bookName, startThisSearchTime, bookList, false); } private synchronized void addSearchBook(List value) { if (value.size() > 0) { Collections.sort(value, this::compareSearchBooks); for (SearchBookBean searchBookBean : value) { if (searchBookBean.getName().equals(bookName) && (searchBookBean.getAuthor().equals(bookAuthor) || TextUtils.isEmpty(searchBookBean.getAuthor()) || TextUtils.isEmpty(bookAuthor))) { if (searchBookBean.getTag().equals(bookTag)) { searchBookBean.setIsCurrentSource(true); } else { searchBookBean.setIsCurrentSource(false); } boolean saveBookSource = false; BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(searchBookBean.getTag()); if (searchBookBean.getSearchTime() < 60 && bookSourceBean != null) { bookSourceBean.increaseWeight(100 / (10 + searchBookBean.getSearchTime())); saveBookSource = true; } if (shelfLastChapter > 0 && bookSourceBean != null) { int lastChapter = BookshelfHelp.guessChapterNum(searchBookBean.getLastChapter()); if (lastChapter > shelfLastChapter) { bookSourceBean.increaseWeight(100); saveBookSource = true; } } if (saveBookSource) { DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean); } DbHelper.getDaoSession().getSearchBookBeanDao().insertOrReplace(searchBookBean); if (StringUtils.isTrimEmpty(searchView.getQuery().toString()) || searchBookBean.getOrigin().equals(searchView.getQuery().toString())) { handler.post(() -> adapter.addSourceAdapter(searchBookBean)); } break; } } } } private int compareSearchBooks(SearchBookBean s1, SearchBookBean s2) { boolean s1tag = s1.getTag().equals(bookTag); boolean s2tag = s2.getTag().equals(bookTag); if (s2tag && !s1tag) return 1; else if (s1tag && !s2tag) return -1; int result = Long.compare(s2.getAddTime(), s1.getAddTime()); if (result != 0) return result; result = Integer.compare(s2.getLastChapterNum(), s1.getLastChapterNum()); if (result != 0) return result; return Integer.compare(s2.getWeight(), s1.getWeight()); } private void stopChangeSource() { compositeDisposable.dispose(); if (searchBookModel != null) { searchBookModel.stopSearch(); } } public interface Callback { void changeSource(SearchBookBean searchBookBean); } @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.UP_SEARCH_BOOK)}) public void upSearchBook(SearchBookBean searchBookBean) { if (!Objects.equals(book.getBookInfoBean().getName(), searchBookBean.getName()) || !Objects.equals(book.getBookInfoBean().getAuthor(), searchBookBean.getAuthor())) { return; } for (int i = 0; i < adapter.getSearchBookBeans().size(); i++) { if (adapter.getSearchBookBeans().get(i).getTag().equals(searchBookBean.getTag()) && !adapter.getSearchBookBeans().get(i).getLastChapter().equals(searchBookBean.getLastChapter())) { adapter.getSearchBookBeans().get(i).setLastChapter(searchBookBean.getLastChapter()); adapter.notifyItemChanged(i); } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/DownLoadDialog.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.annotation.SuppressLint; import android.content.Context; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.kunfei.bookshelf.R; public class DownLoadDialog extends BaseDialog { private Context context; private EditText edtStart; private EditText edtEnd; private TextView tvCancel; private TextView tvDownload; public static DownLoadDialog builder(Context context, int startIndex, int endIndex, final int all) { return new DownLoadDialog(context, startIndex, endIndex, all); } private DownLoadDialog(Context context, int startIndex, int endIndex, final int all) { super(context, R.style.alertDialogTheme); this.context = context; @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_download_choice, null); bindView(view, startIndex, endIndex, all); setContentView(view); } private void bindView(View view, int startIndex, int endIndex, final int all) { View llContent = view.findViewById(R.id.ll_content); llContent.setOnClickListener(null); edtStart = view.findViewById(R.id.edt_start); edtEnd = view.findViewById(R.id.edt_end); tvCancel = view.findViewById(R.id.tv_cancel); tvDownload = view.findViewById(R.id.tv_download); edtStart.setText(String.valueOf(startIndex + 1)); edtEnd.setText(String.valueOf(endIndex + 1)); edtStart.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) { if (edtStart.getText().length() > 0) { try { int temp = Integer.parseInt(edtStart.getText().toString().trim()); if (temp > all) { edtStart.setText(String.valueOf(all)); edtStart.setSelection(edtStart.getText().length()); Toast.makeText(context, "超过总章节", Toast.LENGTH_SHORT).show(); } else if (temp <= 0) { edtStart.setText(String.valueOf(1)); edtStart.setSelection(edtStart.getText().length()); } } catch (Exception e) { e.printStackTrace(); } } } }); edtEnd.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) { if (edtEnd.getText().length() > 0) { try { int temp = Integer.parseInt(edtEnd.getText().toString().trim()); if (temp > all) { edtEnd.setText(String.valueOf(all)); edtEnd.setSelection(edtEnd.getText().length()); Toast.makeText(context, "超过总章节", Toast.LENGTH_SHORT).show(); } else if (temp <= 0) { edtEnd.setText(String.valueOf(1)); edtEnd.setSelection(edtEnd.getText().length()); } } catch (Exception e) { e.printStackTrace(); } } } }); tvCancel.setOnClickListener(v -> dismiss()); } public DownLoadDialog setPositiveButton(Callback callback) { tvDownload.setOnClickListener(v -> { if (edtStart.getText().length() > 0 && edtEnd.getText().length() > 0) { if (Integer.parseInt(edtStart.getText().toString()) > Integer.parseInt(edtEnd.getText().toString())) { Toast.makeText(context, "输入错误", Toast.LENGTH_SHORT).show(); } else { callback.download(Integer.parseInt(edtStart.getText().toString()) - 1, Integer.parseInt(edtEnd.getText().toString()) - 1); } dismiss(); } else { Toast.makeText(context, "请输入要离线的章节", Toast.LENGTH_SHORT).show(); } }); return this; } public interface Callback { void download(int start, int end); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/InputDialog.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.widget.views.ATEAutoCompleteTextView; import java.util.List; /** * 输入框 */ public class InputDialog extends BaseDialog { private boolean showDel = false; private TextView tvTitle; private ATEAutoCompleteTextView etInput; private TextView tvOk; private Callback callback = null; private final Context context; public static InputDialog builder(Context context) { return new InputDialog(context); } private InputDialog(Context context) { super(context, R.style.alertDialogTheme); this.context = context; @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_input, null); setContentView(view); bindView(view); } public InputDialog setShowDel(boolean showDel) { this.showDel = showDel; return this; } public InputDialog setDefaultValue(String defaultValue) { if (defaultValue != null) { etInput.setTextSize(2, 16); // 2 --> sp etInput.setText(defaultValue); etInput.setSelectAllOnFocus(true); } return this; } public InputDialog setTitle(String title) { tvTitle.setText(title); return this; } public InputDialog setAdapterValues(List adapterValues) { if (adapterValues != null) { MyAdapter mAdapter = new MyAdapter(context, adapterValues); etInput.setAdapter(mAdapter); } return this; } private void bindView(View view) { View llContent = view.findViewById(R.id.ll_content); llContent.setOnClickListener(null); tvTitle = view.findViewById(R.id.tv_title); etInput = view.findViewById(R.id.et_input); tvOk = view.findViewById(R.id.tv_ok); } public InputDialog setCallback(Callback callback) { this.callback = callback; tvOk.setOnClickListener(view -> { callback.setInputText(etInput.getText().toString()); dismiss(); }); return this; } class MyAdapter extends ArrayAdapter { MyAdapter(@NonNull Context context, @NonNull List objects) { super(context, R.layout.item_1line_text_and_del, objects); } @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view; if (convertView == null) { view = LayoutInflater.from(context).inflate(R.layout.item_1line_text_and_del, parent, false); } else { view = convertView; } TextView tv = view.findViewById(R.id.text); ImageView iv = view.findViewById(R.id.iv_del); if (showDel) { iv.setVisibility(View.VISIBLE); } else { iv.setVisibility(View.GONE); } String value = String.valueOf(getItem(position)); tv.setText(value); iv.setOnClickListener(v -> { remove(value); if (callback != null) { callback.delete(value); } etInput.showDropDown(); }); return view; } } /** * 输入book地址确定 */ public interface Callback { void setInputText(String inputText); void delete(String value); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/MoDialogHUD.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.SoftInputUtil; /** * 对话框 */ public class MoDialogHUD { private Boolean isFinishing = false; private Context context; private ViewGroup decorView;//activity的根View private ViewGroup rootView;// mSharedView 的 根View private MoDialogView mSharedView; private OnDismissListener dismissListener; private Animation inAnim; private Animation outAnim; private Boolean canBack = false; private Animation.AnimationListener outAnimListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { isFinishing = true; } @Override public void onAnimationEnd(Animation animation) { dismissImmediately(); } @Override public void onAnimationRepeat(Animation animation) { } }; public MoDialogHUD(Context context) { this.context = context; initViews(); initCenter(); initAnimation(); } private void initAnimation() { inAnim = getInAnimation(); outAnim = getOutAnimation(); } private void initFromTopRight() { inAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_in_top_right); outAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_out_top_right); } private void initFromBottomRight() { inAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_in_bottom_right); outAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_out_bottom_right); } private void initFromBottomAnimation() { inAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_bottom_in); outAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_bottom_out); } private void initCenter() { mSharedView.setGravity(Gravity.CENTER); if (mSharedView != null) { FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mSharedView.getLayoutParams(); if (layoutParams != null) { layoutParams.setMargins(0, 0, 0, 0); mSharedView.setLayoutParams(layoutParams); } mSharedView.setPadding(0, 0, 0, 0); } } private void initBottom() { mSharedView.setGravity(Gravity.BOTTOM); if (mSharedView != null) { FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mSharedView.getLayoutParams(); if (layoutParams != null) { layoutParams.setMargins(0, 0, 0, 0); mSharedView.setLayoutParams(layoutParams); } mSharedView.setPadding(0, 0, 0, 0); } } private void initMarRightTop() { mSharedView.setGravity(Gravity.RIGHT | Gravity.TOP); if (mSharedView != null) { FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mSharedView.getLayoutParams(); if (layoutParams != null) { layoutParams.setMargins(0, 0, 0, 0); mSharedView.setLayoutParams(layoutParams); } mSharedView.setPadding(0, 0, 0, 0); } } private void initViews() { decorView = ((Activity) context).getWindow().getDecorView().findViewById(android.R.id.content); rootView = new FrameLayout(context); FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ); rootView.setLayoutParams(layoutParams); rootView.setClickable(true); rootView.setBackgroundColor(context.getResources().getColor(R.color.btn_bg_press_tp)); mSharedView = new MoDialogView(context); } private Animation getInAnimation() { return AnimationUtils.loadAnimation(context, R.anim.moprogress_in); } private Animation getOutAnimation() { return AnimationUtils.loadAnimation(context, R.anim.moprogress_out); } private boolean isShowing() { return rootView.getParent() != null; } private void onAttached() { decorView.addView(rootView); if (mSharedView.getParent() != null) ((ViewGroup) mSharedView.getParent()).removeView(mSharedView); rootView.addView(mSharedView); isFinishing = false; } public void dismiss() { //消失动画 if (mSharedView != null && rootView != null && mSharedView.getParent() != null) { SoftInputUtil.hideIMM(rootView); if (!isFinishing) { new Handler().post(() -> { outAnim.setAnimationListener(outAnimListener); mSharedView.getChildAt(0).startAnimation(outAnim); }); } } } public Boolean isShow() { return (mSharedView != null && mSharedView.getParent() != null); } private void dismissImmediately() { if (dismissListener != null) { dismissListener.onDismiss(); dismissListener = null; } if (mSharedView != null && rootView != null && mSharedView.getParent() != null) { new Handler().post(() -> { rootView.removeView(mSharedView); decorView.removeView(rootView); }); } isFinishing = false; } /** * 返回键事件 */ public Boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (isShowing()) { if (canBack) { dismiss(); } return true; } } return false; } /** * 加载动画 */ public void showLoading(String msg) { initCenter(); initAnimation(); canBack = false; rootView.setOnClickListener(null); if (!isShowing()) { onAttached(); } mSharedView.showLoading(msg); mSharedView.getChildAt(0).startAnimation(inAnim); } /** * 单个按钮的提示信息 */ public void showInfo(String msg) { initCenter(); initAnimation(); canBack = true; rootView.setOnClickListener(null); mSharedView.showInfo(msg, v -> dismiss()); if (!isShowing()) { onAttached(); } mSharedView.getChildAt(0).startAnimation(inAnim); } /** * 单个按钮的提示信息 */ public void showInfo(String msg, String btnText, View.OnClickListener listener) { initCenter(); initAnimation(); canBack = true; rootView.setOnClickListener(null); mSharedView.showInfo(msg, btnText, listener); if (!isShowing()) { onAttached(); } mSharedView.getChildAt(0).startAnimation(inAnim); } /** * 两个不同等级的按钮 */ public void showTwoButton(String msg, String b_f, View.OnClickListener c_f, String b_s, View.OnClickListener c_s, boolean canBack) { initCenter(); initAnimation(); this.canBack = canBack; rootView.setOnClickListener(v -> dismiss()); mSharedView.showTwoButton(msg, b_f, c_f, b_s, c_s); if (!isShowing()) { onAttached(); } mSharedView.getChildAt(0).startAnimation(inAnim); } /** * 显示一段文本 */ public void showText(String text) { initCenter(); initAnimation(); canBack = true; rootView.setOnClickListener(v -> dismiss()); mSharedView.showText(text); if (!isShowing()) { onAttached(); } mSharedView.getChildAt(0).startAnimation(inAnim); } /** * 显示asset Markdown */ public void showAssetMarkdown(String assetFileName) { initCenter(); initAnimation(); canBack = true; rootView.setOnClickListener(v -> dismiss()); mSharedView.showAssetMarkdown(assetFileName); if (!isShowing()) { onAttached(); } mSharedView.getChildAt(0).startAnimation(inAnim); } public void showImageText(Bitmap bitmap, String text) { initCenter(); initAnimation(); canBack = true; rootView.setOnClickListener(v -> dismiss()); mSharedView.showImageText(bitmap, text); if (!isShowing()) { onAttached(); } mSharedView.getChildAt(0).startAnimation(inAnim); } private interface OnDismissListener { void onDismiss(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/MoDialogView.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.content.Context; import android.graphics.Bitmap; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.cardview.widget.CardView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.ReadAssets; import com.kunfei.bookshelf.widget.RotateLoading; import ru.noties.markwon.Markwon; /** * 对话框 */ public class MoDialogView extends LinearLayout { private Context context; public MoDialogView(Context context) { this(context, null); } public MoDialogView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MoDialogView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; setOrientation(VERTICAL); } //转圈的载入 public void showLoading(String text) { removeAllViews(); LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_loading, this, true); TextView msgTv = findViewById(R.id.msg_tv); if (text != null && text.length() > 0) { msgTv.setText(text); } RotateLoading rlLoading = findViewById(R.id.rl_loading); rlLoading.start(); } //单个按钮的信息提示框 public void showInfo(String msg, final OnClickListener listener) { removeAllViews(); LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_infor, this, true); View llContent = findViewById(R.id.ll_content); llContent.setOnClickListener(null); TextView msgTv = findViewById(R.id.msg_tv); msgTv.setText(msg); TextView tvClose = findViewById(R.id.tv_close); tvClose.setOnClickListener(listener); } //单个按钮的信息提示框 public void showInfo(String msg, String btnText, final OnClickListener listener) { removeAllViews(); LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_infor, this, true); View llContent = findViewById(R.id.ll_content); llContent.setOnClickListener(null); TextView msgTv = findViewById(R.id.msg_tv); msgTv.setText(msg); TextView tvClose = findViewById(R.id.tv_close); tvClose.setText(btnText); tvClose.setOnClickListener(listener); } //////////////////////两个不同等级的按钮////////////////////// public void showTwoButton(String msg, String b_f, OnClickListener c_f, String b_s, OnClickListener c_s) { removeAllViews(); LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_two, this, true); TextView tvMsg = findViewById(R.id.tv_msg); TextView tvCancel = findViewById(R.id.tv_cancel); TextView tvDone = findViewById(R.id.tv_done); tvMsg.setText(msg); tvCancel.setText(b_f); tvCancel.setOnClickListener(c_f); tvDone.setText(b_s); tvDone.setOnClickListener(c_s); } /** * 显示一段文本 */ public void showText(String text) { removeAllViews(); LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_text_large, this, true); TextView textView = findViewById(R.id.tv_can_copy); textView.setText(text); } /** * 显示asset Markdown */ public void showAssetMarkdown(String assetFileName) { removeAllViews(); LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_markdown, this, true); TextView tvMarkdown = findViewById(R.id.tv_markdown); Markwon.create(tvMarkdown.getContext()).setMarkdown(tvMarkdown, ReadAssets.getText(context, assetFileName)); } /** * 显示图像和文本 */ public void showImageText(Bitmap bitmap, String text) { removeAllViews(); LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_image_text, this, true); CardView cardView = findViewById(R.id.cv_content); cardView.setOnClickListener(null); ImageView imageView = findViewById(R.id.image_view); TextView tvCanCopy = findViewById(R.id.tv_can_copy); int imageWidth = Math.min(cardView.getWidth(), cardView.getHeight()); imageView.setMaxWidth(imageWidth - 20); imageView.setMaxHeight(imageWidth - 20); imageView.setImageBitmap(bitmap); tvCanCopy.setText(text); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/PageKeyDialog.kt ================================================ package com.kunfei.bookshelf.widget.modialog import android.content.Context import android.view.KeyEvent import android.view.LayoutInflater import com.kunfei.bookshelf.MApplication import com.kunfei.bookshelf.databinding.DialogPageKeyBinding import com.kunfei.bookshelf.utils.SoftInputUtil import org.jetbrains.anko.sdk27.listeners.onClick class PageKeyDialog(context: Context) : BaseDialog(context) { val binding = DialogPageKeyBinding.inflate(LayoutInflater.from(context)) init { setContentView(binding.root) binding.etPrev.setText(MApplication.getConfigPreferences().getInt("prevKeyCode", 0).toString()) binding.etNext.setText(MApplication.getConfigPreferences().getInt("nextKeyCode", 0).toString()) binding.tvOk.onClick { val edit = MApplication.getConfigPreferences().edit() binding.etPrev.text?.let { edit.putInt("prevKeyCode", it.toString().toInt()) } binding.etNext.text?.let { edit.putInt("nextKeyCode", it.toString().toInt()) } edit.apply() dismiss() } } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { if (keyCode != KeyEvent.KEYCODE_BACK) { if (binding.etPrev.hasFocus()) { binding.etPrev.setText(keyCode.toString()) } else if (binding.etNext.hasFocus()) { binding.etNext.setText(keyCode.toString()) } return true } return super.onKeyDown(keyCode, event) } override fun dismiss() { super.dismiss() SoftInputUtil.hideIMM(currentFocus) } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/ReplaceRuleDialog.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.annotation.SuppressLint; import android.content.Context; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.CheckBox; import android.widget.TextView; import androidx.appcompat.widget.AppCompatEditText; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.bean.ReplaceRuleBean; public class ReplaceRuleDialog extends BaseDialog { private Context context; private AppCompatEditText tieReplaceSummary; private AppCompatEditText tieReplaceRule; private AppCompatEditText tieReplaceTo; private AppCompatEditText tieUseTo; private CheckBox cbUseRegex; private TextView tvOk; private ReplaceRuleBean replaceRuleBean; private BookShelfBean bookShelfBean; private TextView replace_ad_intro, tvtitle; private View til_replace_to; // 替换规则编辑UI的模式 1 默认 2 广告话术 3 添加广告话术 private int ReplaceUIMode = 1; public static int DefaultUI = 1, AdUI = 2, AddAdUI = 3; private String str_summary = ""; public static ReplaceRuleDialog builder(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean, int replaceUIMode) { return new ReplaceRuleDialog(context, replaceRuleBean, bookShelfBean, replaceUIMode); } public static ReplaceRuleDialog builder(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean) { return new ReplaceRuleDialog(context, replaceRuleBean, bookShelfBean); } private ReplaceRuleDialog(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean) { super(context, R.style.alertDialogTheme); this.context = context; this.replaceRuleBean = replaceRuleBean; this.bookShelfBean = bookShelfBean; @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_replace_rule, null); bindView(view); setContentView(view); } private ReplaceRuleDialog(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean, int replaceUIMod) { super(context, R.style.alertDialogTheme); this.context = context; this.replaceRuleBean = replaceRuleBean; this.bookShelfBean = bookShelfBean; this.ReplaceUIMode = replaceUIMod; @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_replace_rule, null); bindView(view); setContentView(view); } private void bindView(View view) { View llContent = view.findViewById(R.id.ll_content); llContent.setOnClickListener(null); tieReplaceRule = view.findViewById(R.id.tie_replace_rule); tieReplaceSummary = view.findViewById(R.id.tie_replace_summary); tieReplaceTo = view.findViewById(R.id.tie_replace_to); tieUseTo = view.findViewById(R.id.tie_use_to); cbUseRegex = view.findViewById(R.id.cb_use_regex); tvOk = view.findViewById(R.id.tv_ok); replace_ad_intro = view.findViewById(R.id.replace_ad_intro); tvtitle = view.findViewById(R.id.title); til_replace_to=view.findViewById(R.id.til_replace_to); if (replaceRuleBean != null) { tieReplaceSummary.setText(replaceRuleBean.getReplaceSummary()); tieReplaceTo.setText(replaceRuleBean.getReplacement()); tieReplaceRule.setText(replaceRuleBean.getRegex()); tieUseTo.setText(replaceRuleBean.getUseTo()); cbUseRegex.setChecked(replaceRuleBean.getIsRegex()); // 初始化广告话术规则的UI if (ReplaceUIMode == DefaultUI) { if (replaceRuleBean.getReplaceSummary().matches("^" + view.getContext().getString(R.string.replace_ad) + ".*")) ReplaceUIMode = AdUI; } if (ReplaceUIMode > DefaultUI) { // tieReplaceTo.setVisibility(View.GONE); til_replace_to.setVisibility(View.GONE); cbUseRegex.setVisibility(View.GONE); replace_ad_intro.setVisibility(View.VISIBLE); tieReplaceSummary.setInputType(EditorInfo.TYPE_NULL); tieReplaceRule.setMaxLines(8); if (ReplaceUIMode == AdUI) { tvtitle.setText(view.getContext().getString(R.string.replace_ad_title)); } else { tvtitle.setText(view.getContext().getString(R.string.replace_add_ad_title)); } str_summary = view.getContext().getString(R.string.replace_ad); TextWatcher mTextWatcher = new TextWatcher() { private CharSequence temp; @Override public void onTextChanged(CharSequence s, int start, int before, int count) { temp = s; String str=s.toString().trim(); if (str.replaceAll("[\\s,]", "").length() > 0) tieReplaceSummary.setText(str_summary + "-" + str); else tieReplaceSummary.setText(str_summary); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { /* if (s.toString().replaceAll("[\\s,]", "").length() > 0) tieReplaceSummary.setText(str_summary + "-" + temp); else tieReplaceSummary.setText(str_summary);*/ } }; tieUseTo.addTextChangedListener(mTextWatcher); } } else { replaceRuleBean = new ReplaceRuleBean(); replaceRuleBean.setEnable(true); cbUseRegex.setChecked(MApplication.getConfigPreferences().getBoolean("useRegexInNewRule", false)); if (bookShelfBean != null) { tieUseTo.setText(String.format("%s,%s", bookShelfBean.getBookInfoBean().getName(), bookShelfBean.getTag())); } } } public ReplaceRuleDialog setPositiveButton(Callback callback) { tvOk.setOnClickListener(v -> { replaceRuleBean.setReplaceSummary(getEditableText(tieReplaceSummary.getText())); replaceRuleBean.setRegex(getEditableText(tieReplaceRule.getText())); replaceRuleBean.setIsRegex(cbUseRegex.isChecked()); replaceRuleBean.setReplacement(getEditableText(tieReplaceTo.getText())); replaceRuleBean.setUseTo(getEditableText(tieUseTo.getText())); callback.onPositiveButton(replaceRuleBean); dismiss(); }); return this; } private String getEditableText(Editable editable) { if (editable == null) { return ""; } return editable.toString(); } public interface Callback { void onPositiveButton(ReplaceRuleBean replaceRuleBean); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/modialog/TxtChapterRuleDialog.java ================================================ package com.kunfei.bookshelf.widget.modialog; import android.annotation.SuppressLint; import android.content.Context; import android.text.Editable; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.TxtChapterRuleBean; import com.kunfei.bookshelf.widget.views.ATEEditText; public class TxtChapterRuleDialog { private ATEEditText tieRuleName; private ATEEditText tieRuleRegex; private TextView tvOk; private BaseDialog dialog; private TxtChapterRuleBean txtChapterRuleBean; public static TxtChapterRuleDialog builder(Context context, TxtChapterRuleBean txtChapterRuleBean) { return new TxtChapterRuleDialog(context, txtChapterRuleBean); } private TxtChapterRuleDialog(Context context, TxtChapterRuleBean txtChapterRuleBean) { if (txtChapterRuleBean != null) { this.txtChapterRuleBean = txtChapterRuleBean.copy(); } dialog = new BaseDialog(context, R.style.alertDialogTheme); @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_txt_chpater_rule, null); bindView(view); dialog.setContentView(view); } private void bindView(View view) { tieRuleName = view.findViewById(R.id.tie_rule_name); tieRuleRegex = view.findViewById(R.id.tie_rule_regex); tvOk = view.findViewById(R.id.tv_ok); if (txtChapterRuleBean != null) { tieRuleName.setText(txtChapterRuleBean.getName()); tieRuleRegex.setText(txtChapterRuleBean.getRule()); } } public TxtChapterRuleDialog setPositiveButton(Callback callback) { tvOk.setOnClickListener(v -> { if (txtChapterRuleBean == null) { txtChapterRuleBean = new TxtChapterRuleBean(); } txtChapterRuleBean.setName(getEditableText(tieRuleName.getText())); txtChapterRuleBean.setRule(getEditableText(tieRuleRegex.getText())); callback.onPositiveButton(txtChapterRuleBean); dialog.dismiss(); }); return this; } private String getEditableText(Editable editable) { if (editable == null) { return ""; } return editable.toString(); } public TxtChapterRuleDialog show() { dialog.show(); return this; } public interface Callback { void onPositiveButton(TxtChapterRuleBean txtChapterRuleBean); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/number/NumberButton.java ================================================ package com.kunfei.bookshelf.widget.number; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.kunfei.bookshelf.R; import java.text.DecimalFormat; public class NumberButton extends FrameLayout implements View.OnClickListener { public static final int INT = 0; public static final int FLOAT = 1; private OnChangedListener onChangedListener; private TextView tvNumber; private DecimalFormat decimalFormat = new DecimalFormat("#"); private int numberType = INT; private float minNumber = 0; private float maxNumber = 10; private float stepNumber = 1; private String tile = "请选择"; public NumberButton(Context context) { this(context, null); } public NumberButton(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } private void init(Context context) { LayoutInflater.from(context).inflate(R.layout.view_number_buttom, this); TextView addButton = findViewById(R.id.button_add); addButton.setOnClickListener(this); TextView subButton = findViewById(R.id.button_sub); subButton.setOnClickListener(this); tvNumber = findViewById(R.id.tv_number); tvNumber.setOnClickListener(this); } public NumberButton setTitle(@NonNull String title) { this.tile = title; return this; } public void setOnChangedListener(OnChangedListener onChangedListener) { this.onChangedListener = onChangedListener; } public float getNumber() { try { return Float.parseFloat(tvNumber.getText().toString()); } catch (NumberFormatException e) { tvNumber.setText(decimalFormat.format(minNumber)); return minNumber; } } public NumberButton setNumber(float number) { tvNumber.setText(decimalFormat.format(number)); return this; } public NumberButton setFormat(String pattern) { decimalFormat = new DecimalFormat(pattern); return this; } public NumberButton setMinNumber(float minNumber) { this.minNumber = minNumber; return this; } public NumberButton setMaxNumber(float maxNumber) { this.maxNumber = maxNumber; return this; } public NumberButton setStepNumber(float stepNumber) { this.stepNumber = stepNumber; return this; } public NumberButton setNumberType(int numberType) { this.numberType = numberType; return this; } @Override public void onClick(View view) { float count = getNumber(); switch (view.getId()) { case R.id.button_add: if (count < maxNumber) { changeNumber(count + stepNumber); } break; case R.id.button_sub: if (count > minNumber) { changeNumber(count - stepNumber); } break; case R.id.tv_number: if (numberType == INT) { NumberPickerDialog npd = new NumberPickerDialog(getContext()); npd.setTitle(tile) .setMaxValue((int) maxNumber) .setMinValue((int) minNumber) .setValue((int) getNumber()) .setListener(this::changeNumber) .create() .show(); } break; } } private void changeNumber(float f) { tvNumber.setText(decimalFormat.format(f)); if (onChangedListener != null) { onChangedListener.numberChange(f); } } public interface OnChangedListener { void numberChange(float number); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/number/NumberPickerDialog.java ================================================ package com.kunfei.bookshelf.widget.number; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.widget.NumberPicker; import androidx.appcompat.app.AlertDialog; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.SoftInputUtil; import com.kunfei.bookshelf.utils.theme.ATH; public class NumberPickerDialog { private AlertDialog.Builder builder; private NumberPicker numberPicker; NumberPickerDialog(Context context) { builder = new AlertDialog.Builder(context); @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_number_picker, null); numberPicker = view.findViewById(R.id.number_picker); builder.setView(view); } public NumberPickerDialog setTitle(String title) { builder.setTitle(title); return this; } public NumberPickerDialog setMaxValue(int value) { numberPicker.setMaxValue(value); return this; } public NumberPickerDialog setMinValue(int value) { numberPicker.setMinValue(value); return this; } public NumberPickerDialog setValue(int value) { numberPicker.setValue(value); return this; } public NumberPickerDialog create() { builder.create(); return this; } public NumberPickerDialog setListener(OnClickListener onClickListener) { builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { numberPicker.clearFocus(); SoftInputUtil.hideIMM(numberPicker); if (onClickListener != null) { onClickListener.setNumber(numberPicker.getValue()); } }); builder.setNegativeButton(R.string.cancel, null); return this; } public void show() { AlertDialog dialog = builder.show(); ATH.setAlertDialogTint(dialog); } public interface OnClickListener { void setNumber(int i); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/number/NumberPickerPreference.java ================================================ package com.kunfei.bookshelf.widget.number; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; import android.preference.DialogPreference; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.NumberPicker; import androidx.annotation.NonNull; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.SoftInputUtil; /** * Displaying a NumberPicker in a DialogPreference */ public class NumberPickerPreference extends DialogPreference { private static final String TAG = NumberPickerPreference.class.getSimpleName(); /** * this variables will be initialised in 'init()' */ //by default min value is 0 private int minValue = 0; //by default max value is 10 private int maxValue = 10; //get summary - hardcode if no summary is set private String summaryPattern = "number picked: %s"; private NumberPicker numPicker; private int numValue = minValue; public NumberPickerPreference(Context context, AttributeSet attrs) { super(context, attrs); // initializing attributes init(attrs); } /** * setting attributes from xml * attr attributeset */ private void init(AttributeSet attrs) { TypedArray a = getContext().obtainStyledAttributes( attrs, R.styleable.NumberPickerPreference); maxValue = a.getInt(R.styleable.NumberPickerPreference_MaxValue, maxValue); minValue = a.getInt(R.styleable.NumberPickerPreference_MinValue, minValue); summaryPattern = a.getString(R.styleable.NumberPickerPreference_android_summary); a.recycle(); } /** * @return dialog view with picker inside */ @Override protected View onCreateDialogView() { FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); layoutParams.gravity = Gravity.CENTER; numPicker = new NumberPicker(getContext()); numPicker.setLayoutParams(layoutParams); numPicker.setMinValue(minValue); numPicker.setMaxValue(maxValue); FrameLayout dialogView = new FrameLayout(getContext()); dialogView.addView(numPicker); return dialogView; } @Override protected void onBindDialogView(@NonNull View view) { super.onBindDialogView(view); numPicker.setValue(getValue()); } @Override public void onClick(DialogInterface dialog, int which) { numPicker.clearFocus(); SoftInputUtil.hideIMM(numPicker); super.onClick(dialog, which); } /** * update summary when dialog is closed */ @Override protected void onDialogClosed(boolean positiveResult) { if (positiveResult) { int pickerValue = numPicker.getValue(); updateSummary(pickerValue); setValue(pickerValue); } } /** * if no default value is set - then set min value */ @Override protected Object onGetDefaultValue(TypedArray a, int index) { return a.getInt(index, minValue); } /** * SetInitialValue */ @Override protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { if (restorePersistedValue) { setValue(getPersistedInt(minValue)); } else { setValue((Integer) defaultValue); } updateSummary(getValue()); } /** * @return current value */ public int getValue() { return this.numValue; } /** * @param value which will be stored in SharedPreferences */ private void setValue(int value) { this.numValue = value; persistInt(this.numValue); } /** * @return get summary pattern from xml file */ private String getSummaryPattern() { return this.summaryPattern; } /** * value insert into summaryPattern */ private void updateSummary(int val) { setSummary(String.format(getSummaryPattern(), Integer.toString(val))); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/ChapterProvider.java ================================================ package com.kunfei.bookshelf.widget.page; import android.text.Layout; import android.text.StaticLayout; import android.util.Log; import androidx.annotation.NonNull; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.help.ChapterContentHelp; import com.kunfei.bookshelf.utils.NetworkUtils; import java.util.ArrayList; import java.util.List; class ChapterProvider { private PageLoader pageLoader; private ChapterContentHelp contentHelper = new ChapterContentHelp(); ChapterProvider(PageLoader pageLoader) { this.pageLoader = pageLoader; } TxtChapter dealLoadPageList(BookChapterBean chapter, boolean isPrepare) { TxtChapter txtChapter = new TxtChapter(chapter.getDurChapterIndex()); // 判断章节是否存在 if (!isPrepare || pageLoader.noChapterData(chapter)) { if (pageLoader instanceof PageLoaderNet && !NetworkUtils.isNetWorkAvailable()) { txtChapter.setStatus(TxtChapter.Status.ERROR); txtChapter.setMsg("网络连接不可用"); } return txtChapter; } String content; try { content = pageLoader.getChapterContent(chapter); } catch (Exception e) { txtChapter.setStatus(TxtChapter.Status.ERROR); txtChapter.setMsg("读取内容出错\n" + e.getLocalizedMessage()); return txtChapter; } if (content == null) { txtChapter.setStatus(TxtChapter.Status.ERROR); txtChapter.setMsg("缓存文件不存在"); return txtChapter; } return loadPageList(chapter, content); } /** * 将章节数据,解析成页面列表 * * @param chapter:章节信息 * @param content:章节的文本 */ private TxtChapter loadPageList(BookChapterBean chapter, @NonNull String content) { //生成的页面 TxtChapter txtChapter = new TxtChapter(chapter.getDurChapterIndex()); if (pageLoader.book.isAudio()) { txtChapter.setStatus(TxtChapter.Status.FINISH); txtChapter.setMsg(content); TxtPage page = new TxtPage(txtChapter.getTxtPageList().size()); page.setTitle(chapter.getDurChapterName()); page.addLine(chapter.getDurChapterName()); page.addLine(content); page.setTitleLines(1); txtChapter.addPage(page); addTxtPageLength(txtChapter, page.getContent().length()); txtChapter.addPage(page); return txtChapter; } Log.i("content-1",chapter.getDurChapterName()+"\n"+content.substring(content.length()/3*2)); content = contentHelper.replaceContent(pageLoader.book.getBookInfoBean().getName(), pageLoader.book.getTag(), content, pageLoader.book.getReplaceEnable()); // Log.i("chapterName",chapter.getDurChapterName()); // 方便debug // if(chapter.getDurChapterName().matches(".*幽魂.*")) { // Log.i("content",content); content = contentHelper.LightNovelParagraph2(content,chapter.getDurChapterName()); } String[] allLine = content.split("\n"); List lines = new ArrayList<>(); List txtLists = new ArrayList<>();//记录每个字的位置 //pzl int rHeight = pageLoader.mVisibleHeight - pageLoader.contentMarginHeight * 2; int titleLinesCount = 0; boolean showTitle = pageLoader.readBookControl.getShowTitle(); // 是否展示标题 String paragraph = null; if (showTitle) { paragraph = contentHelper.replaceContent(pageLoader.book.getBookInfoBean().getName(), pageLoader.book.getTag(), chapter.getDurChapterName(), pageLoader.book.getReplaceEnable()); paragraph = paragraph.trim() + "\n"; } int i = 1; while (showTitle || i < allLine.length) { // 重置段落 if (!showTitle) { paragraph = allLine[i].replaceAll("\\s", " ").trim(); i++; if (paragraph.equals("")) continue; paragraph = pageLoader.indent + paragraph + "\n"; } addParagraphLength(txtChapter, paragraph.length()); int wordCount; String subStr; while (paragraph.length() > 0) { //当前空间,是否容得下一行文字 if (showTitle) { rHeight -= pageLoader.mTitlePaint.getTextSize(); } else { rHeight -= pageLoader.mTextPaint.getTextSize(); } // 一页已经填充满了,创建 TextPage if (rHeight <= 0) { // 创建Page TxtPage page = new TxtPage(txtChapter.getTxtPageList().size()); page.setTitle(chapter.getDurChapterName()); page.addLines(lines); page.setTxtLists(new ArrayList<>(txtLists)); page.setTitleLines(titleLinesCount); txtChapter.addPage(page); addTxtPageLength(txtChapter, page.getContent().length()); // 重置Lines lines.clear(); txtLists.clear();//pzl rHeight = pageLoader.mVisibleHeight - pageLoader.contentMarginHeight * 2; titleLinesCount = 0; continue; } //测量一行占用的字节数 if (showTitle) { Layout tempLayout = new StaticLayout(paragraph, pageLoader.mTitlePaint, pageLoader.mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false); wordCount = tempLayout.getLineEnd(0); } else { Layout tempLayout = new StaticLayout(paragraph, pageLoader.mTextPaint, pageLoader.mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false); wordCount = tempLayout.getLineEnd(0); } subStr = paragraph.substring(0, wordCount); if (!subStr.equals("\n")) { //将一行字节,存储到lines中 lines.add(subStr); //begin pzl //记录每个字的位置 char[] cs = subStr.toCharArray(); TxtLine txtList = new TxtLine();//每一行 txtList.setCharsData(new ArrayList()); for (char c : cs) { String mesasrustr = String.valueOf(c); float charwidth = pageLoader.mTextPaint.measureText(mesasrustr); if (showTitle) { charwidth = pageLoader.mTitlePaint.measureText(mesasrustr); } TxtChar txtChar = new TxtChar(); txtChar.setChardata(c); txtChar.setCharWidth(charwidth);//字宽 txtChar.setIndex(66);//每页每个字的位置 txtList.getCharsData().add(txtChar); } txtLists.add(txtList); //end pzl //设置段落间距 if (showTitle) { titleLinesCount += 1; rHeight -= pageLoader.mTitleInterval; } else { rHeight -= pageLoader.mTextInterval; } } //裁剪 paragraph = paragraph.substring(wordCount); } //增加段落的间距 if (!showTitle && lines.size() != 0) { rHeight = rHeight - pageLoader.mTextPara + pageLoader.mTextInterval; } if (showTitle) { rHeight = rHeight - pageLoader.mTitlePara + pageLoader.mTitleInterval; showTitle = false; } } if (lines.size() != 0) { //创建Page TxtPage page = new TxtPage(txtChapter.getTxtPageList().size()); page.setTitle(chapter.getDurChapterName()); page.addLines(lines); page.setTxtLists(new ArrayList<>(txtLists)); page.setTitleLines(titleLinesCount); txtChapter.addPage(page); addTxtPageLength(txtChapter, page.getContent().length()); //重置Lines lines.clear(); txtLists.clear(); } if (txtChapter.getPageSize() > 0) { txtChapter.setStatus(TxtChapter.Status.FINISH); } else { txtChapter.setStatus(TxtChapter.Status.ERROR); txtChapter.setMsg("未加载到内容"); } return txtChapter; } private void addTxtPageLength(TxtChapter txtChapter, int length) { if (txtChapter.getTxtPageLengthList().isEmpty()) { txtChapter.addTxtPageLength(length); } else { txtChapter.addTxtPageLength(txtChapter.getTxtPageLengthList().get(txtChapter.getTxtPageLengthList().size() - 1) + length); } } private void addParagraphLength(TxtChapter txtChapter, int length) { if (txtChapter.getParagraphLengthList().isEmpty()) { txtChapter.addParagraphLength(length); } else { txtChapter.addParagraphLength(txtChapter.getParagraphLengthList().get(txtChapter.getParagraphLengthList().size() - 1) + length); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoader.java ================================================ package com.kunfei.bookshelf.widget.page; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Typeface; import android.os.Build; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.widget.Toast; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.constant.AppConstant; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.help.ChapterContentHelp; import com.kunfei.bookshelf.help.ReadBookControl; import com.kunfei.bookshelf.service.ReadAloudService; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.utils.StringUtils; import com.kunfei.bookshelf.utils.theme.ThemeStore; import com.kunfei.bookshelf.widget.page.animation.PageAnimation; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.SingleOnSubscribe; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; /** * 页面加载器 */ public abstract class PageLoader { private static final String TAG = "PageLoader"; // 默认的显示参数配置 private static final int DEFAULT_MARGIN_HEIGHT = 20; public static final int DEFAULT_MARGIN_WIDTH = 15; private static final int DEFAULT_TIP_SIZE = 12; private static final float MAX_SCROLL_OFFSET = 100; private static final int TIP_ALPHA = 180; // 监听器 Callback callback; private Context mContext; BookShelfBean book; // 页面显示类 PageView mPageView; private List chapterContainers = new ArrayList<>(); // 绘制电池的画笔 private TextPaint mBatteryPaint; // 绘制提示的画笔(章节名称和时间) private TextPaint mTipPaint; private float pageOffset = 0; // 绘制标题的画笔 TextPaint mTitlePaint; // 绘制小说内容的画笔 TextPaint mTextPaint; // 绘制结束的画笔 private TextPaint mTextEndPaint; // 阅读器的配置选项 ReadBookControl readBookControl = ReadBookControl.getInstance(); //缩进 String indent; /*****************params**************************/ // 判断章节列表是否加载完成 boolean isChapterListPrepare; private boolean isClose; // 页面的翻页效果模式 private PageAnimation.Mode mPageMode; //书籍绘制区域的宽高 int mVisibleWidth; int mVisibleHeight; //应用的宽高 int mDisplayWidth; private int mDisplayHeight; //间距 private int mMarginTop; private int mMarginBottom; private int mMarginLeft; private int mMarginRight; int contentMarginHeight; private int tipMarginTop; private int tipMarginBottom; private int oneSpPx; //标题的大小 private int mTitleSize; //字体的大小 private int mTextSize; private int mTextEndSize; //行间距 int mTextInterval; //标题的行间距 int mTitleInterval; //段落距离(基于行间距的额外距离) int mTextPara; int mTitlePara; private int textInterval; private int textPara; private int titleInterval; private int titlePara; private float tipBottomTop; private float tipBottomBot; private float tipDistance; private float tipMarginLeft; private float displayRightEnd; private float tipVisibleWidth; private boolean hideStatusBar; private boolean showTimeBattery; //电池的百分比 private int mBatteryLevel; // 当前章 int mCurChapterPos; private int mCurPagePos; private int readTextLength; //已读字符数 private boolean resetReadAloud; //是否重新朗读 private int readAloudParagraph; //正在朗读章节 Bitmap cover; private int linePos = 0; private boolean isLastPage = false; CompositeDisposable compositeDisposable; //翻页时间 private long skipPageTime = 0; /*****************************init params*******************************/ PageLoader(PageView pageView, BookShelfBean book, Callback callback) { mPageView = pageView; this.book = book; this.callback = callback; for (int i = 0; i < 3; i++) { chapterContainers.add(new ChapterContainer()); } mContext = pageView.getContext(); mCurChapterPos = book.getDurChapter(); mCurPagePos = book.getDurChapterPage(); compositeDisposable = new CompositeDisposable(); oneSpPx = ScreenUtils.spToPx(1); // 初始化数据 initData(); // 初始化画笔 initPaint(); } private void initData() { // 获取配置参数 hideStatusBar = readBookControl.getHideStatusBar(); showTimeBattery = hideStatusBar && readBookControl.getShowTimeBattery(); mPageMode = PageAnimation.Mode.getPageMode(readBookControl.getPageMode()); // 初始化参数 indent = StringUtils.repeat(StringUtils.halfToFull(" "), readBookControl.getIndent()); // 配置文字有关的参数 setUpTextParams(); } /** * 屏幕大小变化处理 */ void prepareDisplay(int w, int h) { // 获取PageView的宽高 mDisplayWidth = w; mDisplayHeight = h; // 设置边距 mMarginTop = hideStatusBar ? ScreenUtils.dpToPx(readBookControl.getTipPaddingTop() + readBookControl.getPaddingTop() + DEFAULT_MARGIN_HEIGHT) : ScreenUtils.dpToPx(readBookControl.getPaddingTop()); mMarginBottom = ScreenUtils.dpToPx(readBookControl.getTipPaddingBottom() + readBookControl.getPaddingBottom() + DEFAULT_MARGIN_HEIGHT); mMarginLeft = ScreenUtils.dpToPx(readBookControl.getPaddingLeft()); mMarginRight = ScreenUtils.dpToPx(readBookControl.getPaddingRight()); contentMarginHeight = oneSpPx; tipMarginTop = ScreenUtils.dpToPx(readBookControl.getTipPaddingTop() + DEFAULT_MARGIN_HEIGHT); tipMarginBottom = ScreenUtils.dpToPx(readBookControl.getTipPaddingBottom() + DEFAULT_MARGIN_HEIGHT); Paint.FontMetrics fontMetrics = mTipPaint.getFontMetrics(); float tipMarginTopHeight = (tipMarginTop + fontMetrics.top - fontMetrics.bottom) / 2; float tipMarginBottomHeight = (tipMarginBottom + fontMetrics.top - fontMetrics.bottom) / 2; tipBottomTop = tipMarginTopHeight - fontMetrics.top; tipBottomBot = mDisplayHeight - fontMetrics.bottom - tipMarginBottomHeight; tipDistance = ScreenUtils.dpToPx(DEFAULT_MARGIN_WIDTH); tipMarginLeft = ScreenUtils.dpToPx(readBookControl.getTipPaddingLeft()); float tipMarginRight = ScreenUtils.dpToPx(readBookControl.getTipPaddingRight()); displayRightEnd = mDisplayWidth - tipMarginRight; tipVisibleWidth = mDisplayWidth - tipMarginLeft - tipMarginRight; // 获取内容显示位置的大小 mVisibleWidth = mDisplayWidth - mMarginLeft - mMarginRight; mVisibleHeight = readBookControl.getHideStatusBar() ? mDisplayHeight - mMarginTop - mMarginBottom : mDisplayHeight - mMarginTop - mMarginBottom - mPageView.getStatusBarHeight(); // 设置翻页模式 mPageView.setPageMode(mPageMode, mMarginTop, mMarginBottom); skipToChapter(mCurChapterPos, mCurPagePos); } /** * 设置与文字相关的参数 */ private void setUpTextParams() { // 文字大小 mTextSize = ScreenUtils.spToPx(readBookControl.getTextSize()); mTitleSize = mTextSize + oneSpPx; mTextEndSize = mTextSize - oneSpPx; // 行间距(大小为字体的一半) mTextInterval = (int) (mTextSize * readBookControl.getLineMultiplier() / 2); mTitleInterval = (int) (mTitleSize * readBookControl.getLineMultiplier() / 2); // 段落间距(大小为字体的高度) mTextPara = (int) (mTextSize * readBookControl.getLineMultiplier() * readBookControl.getParagraphSize() / 2); mTitlePara = (int) (mTitleSize * readBookControl.getLineMultiplier() * readBookControl.getParagraphSize() / 2); } /** * 初始化画笔 */ private void initPaint() { Typeface typeface; try { if (!TextUtils.isEmpty(readBookControl.getFontPath())) { typeface = Typeface.createFromFile(readBookControl.getFontPath()); } else { typeface = Typeface.SANS_SERIF; } } catch (Exception e) { Toast.makeText(mContext, "字体文件未找,到恢复默认字体", Toast.LENGTH_SHORT).show(); readBookControl.setReadBookFont(null); typeface = Typeface.SANS_SERIF; } // 绘制提示的画笔 mTipPaint = new TextPaint(); mTipPaint.setColor(readBookControl.getTextColor()); mTipPaint.setTextAlign(Paint.Align.LEFT); // 绘制的起始点 mTipPaint.setTextSize(ScreenUtils.spToPx(DEFAULT_TIP_SIZE)); // Tip默认的字体大小 mTipPaint.setTypeface(Typeface.create(typeface, Typeface.NORMAL)); mTipPaint.setAntiAlias(true); mTipPaint.setSubpixelText(true); // 绘制标题的画笔 mTitlePaint = new TextPaint(); mTitlePaint.setColor(readBookControl.getTextColor()); mTitlePaint.setTextSize(mTitleSize); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mTitlePaint.setLetterSpacing(readBookControl.getTextLetterSpacing()); } mTitlePaint.setStyle(Paint.Style.FILL_AND_STROKE); mTitlePaint.setTypeface(Typeface.create(typeface, Typeface.BOLD)); mTitlePaint.setTextAlign(Paint.Align.CENTER); mTitlePaint.setAntiAlias(true); // 绘制页面内容的画笔 mTextPaint = new TextPaint(); mTextPaint.setColor(readBookControl.getTextColor()); mTextPaint.setTextSize(mTextSize); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mTextPaint.setLetterSpacing(readBookControl.getTextLetterSpacing()); } int bold = readBookControl.getTextBold() ? Typeface.BOLD : Typeface.NORMAL; mTextPaint.setTypeface(Typeface.create(typeface, bold)); mTextPaint.setAntiAlias(true); // 绘制结束的画笔 mTextEndPaint = new TextPaint(); mTextEndPaint.setColor(readBookControl.getTextColor()); mTextEndPaint.setTextSize(mTextEndSize); mTextEndPaint.setTypeface(Typeface.create(typeface, Typeface.NORMAL)); mTextEndPaint.setAntiAlias(true); mTextEndPaint.setSubpixelText(true); mTextEndPaint.setTextAlign(Paint.Align.CENTER); // 绘制电池的画笔 mBatteryPaint = new TextPaint(); mBatteryPaint.setAntiAlias(true); mBatteryPaint.setDither(true); mBatteryPaint.setTextSize(ScreenUtils.spToPx(DEFAULT_TIP_SIZE - 3)); mBatteryPaint.setTypeface(Typeface.createFromAsset(mContext.getAssets(), "number.ttf")); setupTextInterval(); // 初始化页面样式 initPageStyle(); } /** * 设置文字相关参数 */ public void setTextSize() { // 设置文字相关参数 setUpTextParams(); initPaint(); skipToChapter(mCurChapterPos, mCurPagePos); } private void setupTextInterval() { textInterval = mTextInterval + (int) mTextPaint.getTextSize(); textPara = mTextPara + (int) mTextPaint.getTextSize(); titleInterval = mTitleInterval + (int) mTitlePaint.getTextSize(); titlePara = mTitlePara + (int) mTextPaint.getTextSize(); } /** * 设置页面样式 */ private void initPageStyle() { mTipPaint.setColor(readBookControl.getTextColor()); mTitlePaint.setColor(readBookControl.getTextColor()); mTextPaint.setColor(readBookControl.getTextColor()); mBatteryPaint.setColor(readBookControl.getTextColor()); mTextEndPaint.setColor(readBookControl.getTextColor()); mTipPaint.setAlpha(TIP_ALPHA); mBatteryPaint.setAlpha(TIP_ALPHA); mTextEndPaint.setAlpha(TIP_ALPHA); } /** * 设置翻页动画 */ public void setPageMode(PageAnimation.Mode pageMode) { mPageMode = pageMode; mPageView.setPageMode(mPageMode, mMarginTop, mMarginBottom); skipToChapter(mCurChapterPos, mCurPagePos); } /** * 设置内容与屏幕的间距 单位为 px */ public void upMargin() { prepareDisplay(mDisplayWidth, mDisplayHeight); } /** * 刷新界面 */ public void refreshUi() { initData(); initPaint(); mPageView.setPageMode(mPageMode, mMarginTop, mMarginBottom); skipToChapter(mCurChapterPos, mCurPagePos); } /** * 跳转到上一章 */ public void skipPreChapter() { if (mCurChapterPos <= 0) { return; } // 载入上一章。 mCurChapterPos = mCurChapterPos - 1; mCurPagePos = 0; Collections.swap(chapterContainers, 2, 1); Collections.swap(chapterContainers, 1, 0); prevChapter().txtChapter = null; parsePrevChapter(); chapterChangeCallback(); openChapter(mCurPagePos); pagingEnd(PageAnimation.Direction.NONE); } /** * 跳转到下一章 */ public boolean skipNextChapter() { if (mCurChapterPos + 1 >= book.getChapterListSize()) { return false; } //载入下一章 mCurChapterPos = mCurChapterPos + 1; mCurPagePos = 0; Collections.swap(chapterContainers, 0, 1); Collections.swap(chapterContainers, 1, 2); nextChapter().txtChapter = null; parseNextChapter(); chapterChangeCallback(); openChapter(mCurPagePos); pagingEnd(PageAnimation.Direction.NONE); return true; } /** * 跳转到指定章节页 */ public void skipToChapter(int chapterPos, int pagePos) { // 设置参数 mCurChapterPos = chapterPos; mCurPagePos = pagePos; prevChapter().txtChapter = null; curChapter().txtChapter = null; nextChapter().txtChapter = null; openChapter(pagePos); } /** * 跳转到指定的页 */ public void skipToPage(int pos) { if (!isChapterListPrepare) { return; } openChapter(pos); } /** * 翻到上一页 */ public void skipToPrePage() { if ((System.currentTimeMillis() - skipPageTime) > 300) { mPageView.autoPrevPage(); skipPageTime = System.currentTimeMillis(); } } /** * 翻到下一页 */ public void skipToNextPage() { if ((System.currentTimeMillis() - skipPageTime) > 300) { mPageView.autoNextPage(); skipPageTime = System.currentTimeMillis(); } } /** * 翻到下一页,无动画 */ private void noAnimationToNextPage() { if (getCurPagePos() < curChapter().txtChapter.getPageSize() - 1) { skipToPage(getCurPagePos() + 1); return; } skipNextChapter(); } /** * 更新时间 */ public void updateTime() { if (readBookControl.getHideStatusBar() && readBookControl.getShowTimeBattery()) { if (mPageMode == PageAnimation.Mode.SCROLL) { mPageView.drawBackground(0); } else { upPage(); } mPageView.invalidate(); } } /** * 更新电量 */ public boolean updateBattery(int level) { if (mBatteryLevel == level) { return false; } mBatteryLevel = level; if (readBookControl.getHideStatusBar() && readBookControl.getShowTimeBattery()) { if (mPageMode == PageAnimation.Mode.SCROLL) { mPageView.drawBackground(0); } else if (curChapter().txtChapter != null) { upPage(); } mPageView.invalidate(); return true; } return false; } /** * 获取当前页的状态 */ public TxtChapter.Status getPageStatus() { return curChapter().txtChapter != null ? curChapter().txtChapter.getStatus() : TxtChapter.Status.LOADING; } /** * 获取当前页的页码 */ int getCurPagePos() { return mCurPagePos; } /** * 更新状态 */ public void setStatus(TxtChapter.Status status) { curChapter().txtChapter.setStatus(status); reSetPage(); mPageView.invalidate(); } /** * 加载错误 */ void durDhapterError(String msg) { if (curChapter().txtChapter == null) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); } if (curChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) return; curChapter().txtChapter.setStatus(TxtChapter.Status.ERROR); curChapter().txtChapter.setMsg(msg); if (mPageMode != PageAnimation.Mode.SCROLL) { upPage(); } else { mPageView.drawPage(0); } mPageView.invalidate(); } /** * @return 当前章节所有内容 */ public String getAllContent() { return getContentStartPage(0); } /** * @return 本页未读内容 */ public String getContent() { if (curChapter().txtChapter == null) return null; if (curChapter().txtChapter.getPageSize() == 0) return null; TxtPage txtPage = curChapter().txtChapter.getPage(mCurPagePos); StringBuilder s = new StringBuilder(); int size = txtPage.size(); int start = mPageMode == PageAnimation.Mode.SCROLL ? Math.min(Math.max(0, linePos), size - 1) : 0; for (int i = start; i < size; i++) { s.append(txtPage.getLine(i)); } return s.toString(); } /** * @return 本章未读内容 */ public String getUnReadContent() { if (curChapter().txtChapter == null) return null; if (book.isAudio()) return curChapter().txtChapter.getMsg(); if (curChapter().txtChapter.getTxtPageList().isEmpty()) return null; StringBuilder s = new StringBuilder(); String content = getContent(); if (content != null) { s.append(content); } content = getContentStartPage(mCurPagePos + 1); if (content != null) { s.append(content); } readTextLength = mCurPagePos > 0 ? curChapter().txtChapter.getPageLength(mCurPagePos - 1) : 0; if (mPageMode == PageAnimation.Mode.SCROLL) { for (int i = 0; i < Math.min(Math.max(0, linePos), curChapter().txtChapter.getPage(mCurPagePos).size() - 1); i++) { readTextLength += curChapter().txtChapter.getPage(mCurPagePos).getLine(i).length(); } } return s.toString(); } /** * * @return curPageLength 当前页字数 */ public int curPageLength() { if (curChapter().txtChapter == null) return 0; if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) return 0; String str; int strLength = 0; TxtPage txtPage = curChapter().txtChapter.getPage(mCurPagePos); if (txtPage != null) { for (int i = txtPage.getTitleLines(); i < txtPage.size(); ++i) { str = txtPage.getLine(i); strLength = strLength + str.length(); } } return strLength; } /** * @param page 开始页数 * @return 从page页开始的的当前章节所有内容 */ private String getContentStartPage(int page) { if (curChapter().txtChapter == null) return null; if (curChapter().txtChapter.getTxtPageList().isEmpty()) return null; StringBuilder s = new StringBuilder(); if (curChapter().txtChapter.getPageSize() > page) { for (int i = page; i < curChapter().txtChapter.getPageSize(); i++) { s.append(curChapter().txtChapter.getPage(i).getContent()); } } return s.toString(); } /** * @param start 开始朗读字数 */ public void readAloudStart(int start) { start = readTextLength + start; int x = curChapter().txtChapter.getParagraphIndex(start); if (readAloudParagraph != x) { readAloudParagraph = x; mPageView.drawPage(0); mPageView.invalidate(); mPageView.drawPage(-1); mPageView.drawPage(1); mPageView.invalidate(); } } /** * @param readAloudLength 已朗读字数 */ public void readAloudLength(int readAloudLength) { if (curChapter().txtChapter == null) return; if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) return; if (curChapter().txtChapter.getPageLength(mCurPagePos) < 0) return; if (mPageView.isRunning()) return; readAloudLength = readTextLength + readAloudLength; if (readAloudLength >= curChapter().txtChapter.getPageLength(mCurPagePos)) { resetReadAloud = false; noAnimationToNextPage(); mPageView.invalidate(); } } /** * 刷新章节列表 */ public abstract void refreshChapterList(); /** * 获取章节的文本 */ protected abstract String getChapterContent(BookChapterBean chapter) throws Exception; /** * 章节数据是否存在 */ protected abstract boolean noChapterData(BookChapterBean chapter); /** * 打开当前章节指定页 */ void openChapter(int pagePos) { mCurPagePos = pagePos; if (!mPageView.isPrepare()) { return; } if (curChapter().txtChapter == null) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); reSetPage(); } else if (curChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) { reSetPage(); mPageView.invalidate(); pagingEnd(PageAnimation.Direction.NONE); return; } // 如果章节目录没有准备好 if (!isChapterListPrepare) { curChapter().txtChapter.setStatus(TxtChapter.Status.LOADING); reSetPage(); mPageView.invalidate(); return; } // 如果获取到的章节目录为空 if (callback.getChapterList().isEmpty()) { curChapter().txtChapter.setStatus(TxtChapter.Status.CATEGORY_EMPTY); reSetPage(); mPageView.invalidate(); return; } parseCurChapter(); resetPageOffset(); } /** * 重置页面 */ private void reSetPage() { if (mPageMode == PageAnimation.Mode.SCROLL) { resetPageOffset(); mPageView.invalidate(); } else { upPage(); } } /** * 更新页面 */ private void upPage() { if (mPageMode != PageAnimation.Mode.SCROLL) { mPageView.drawPage(0); if (mCurPagePos > 0 || curChapter().txtChapter.getPosition() > 0) { mPageView.drawPage(-1); } if (mCurPagePos < curChapter().txtChapter.getPageSize() - 1 || curChapter().txtChapter.getPosition() < callback.getChapterList().size() - 1) { mPageView.drawPage(1); } } } /** * 翻页完成 */ void pagingEnd(PageAnimation.Direction direction) { if (!isChapterListPrepare) { return; } switch (direction) { case NEXT: if (mCurPagePos < curChapter().txtChapter.getPageSize() - 1) { mCurPagePos = mCurPagePos + 1; } else if (mCurChapterPos < book.getChapterListSize() - 1) { mCurChapterPos = mCurChapterPos + 1; mCurPagePos = 0; Collections.swap(chapterContainers, 0, 1); Collections.swap(chapterContainers, 1, 2); nextChapter().txtChapter = null; parseNextChapter(); chapterChangeCallback(); } if (mPageMode != PageAnimation.Mode.SCROLL) { mPageView.drawPage(1); } break; case PREV: if (mCurPagePos > 0) { mCurPagePos = mCurPagePos - 1; } else if (mCurChapterPos > 0) { mCurChapterPos = mCurChapterPos - 1; mCurPagePos = prevChapter().txtChapter.getPageSize() - 1; Collections.swap(chapterContainers, 2, 1); Collections.swap(chapterContainers, 1, 0); prevChapter().txtChapter = null; parsePrevChapter(); chapterChangeCallback(); } if (mPageMode != PageAnimation.Mode.SCROLL) { mPageView.drawPage(-1); } break; } mPageView.setContentDescription(getContent()); book.setDurChapter(mCurChapterPos); book.setDurChapterPage(mCurPagePos); callback.onPageChange(mCurChapterPos, getCurPagePos(), resetReadAloud); resetReadAloud = true; } /** * 绘制页面 * pageOnCur: 位于当前页的位置, 小于0上一页, 0 当前页, 大于0下一页 */ synchronized void drawPage(Bitmap bitmap, int pageOnCur) { TxtChapter txtChapter; TxtPage txtPage = null; if (curChapter().txtChapter == null) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); } if (pageOnCur == 0) { //当前页 txtChapter = curChapter().txtChapter; txtPage = txtChapter.getPage(mCurPagePos); } else if (pageOnCur < 0) { //上一页 if (mCurPagePos > 0) { txtChapter = curChapter().txtChapter; txtPage = txtChapter.getPage(mCurPagePos - 1); } else { if (prevChapter().txtChapter == null) { txtChapter = new TxtChapter(mCurChapterPos + 1); txtChapter.setStatus(TxtChapter.Status.ERROR); txtChapter.setMsg("未加载完成"); } else { txtChapter = prevChapter().txtChapter; txtPage = txtChapter.getPage(txtChapter.getPageSize() - 1); } } } else { //下一页 if (mCurPagePos + 1 < curChapter().txtChapter.getPageSize()) { txtChapter = curChapter().txtChapter; txtPage = txtChapter.getPage(mCurPagePos + 1); } else { if (mCurChapterPos + 1 >= callback.getChapterList().size()) { txtChapter = new TxtChapter(mCurChapterPos + 1); txtChapter.setStatus(TxtChapter.Status.ERROR); txtChapter.setMsg("没有下一页"); } else if (nextChapter().txtChapter == null) { txtChapter = new TxtChapter(mCurChapterPos + 1); txtChapter.setStatus(TxtChapter.Status.ERROR); txtChapter.setMsg("未加载完成"); } else { txtChapter = nextChapter().txtChapter; txtPage = txtChapter.getPage(0); } } } if (bitmap != null) drawBackground(bitmap, txtChapter, txtPage); drawContent(bitmap, txtChapter, txtPage); } /** * 滚动模式绘制背景 */ void drawBackground(Canvas canvas) { if (curChapter().txtChapter == null) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); } drawBackground(canvas, curChapter().txtChapter, curChapter().txtChapter.getPage(mCurPagePos)); } /** * 横翻模式绘制背景 */ private synchronized void drawBackground(Bitmap bitmap, TxtChapter txtChapter, TxtPage txtPage) { if (bitmap == null) return; Canvas canvas = new Canvas(bitmap); if (!readBookControl.bgIsColor() && !readBookControl.bgBitmapIsNull()) { Rect mDestRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); canvas.drawBitmap(readBookControl.getBgBitmap(), null, mDestRect, null); } else { canvas.drawColor(readBookControl.getBgColor()); } drawBackground(canvas, txtChapter, txtPage); } /** * 绘制背景 */ @SuppressLint("DefaultLocale") private synchronized void drawBackground(final Canvas canvas, TxtChapter txtChapter, TxtPage txtPage) { if (canvas == null) return; if (!callback.getChapterList().isEmpty()) { String title = callback.getChapterList().size() > txtChapter.getPosition() ? callback.getChapterList().get(txtChapter.getPosition()).getDurChapterName() : ""; title = ChapterContentHelp.getInstance().replaceContent(book.getBookInfoBean().getName(), book.getTag(), title, book.getReplaceEnable()); String page = (txtChapter.getStatus() != TxtChapter.Status.FINISH || txtPage == null) ? "" : String.format("%d/%d", txtPage.getPosition() + 1, txtChapter.getPageSize()); String progress = (txtChapter.getStatus() != TxtChapter.Status.FINISH) ? "" : BookshelfHelp.getReadProgress(mCurChapterPos, book.getChapterListSize(), mCurPagePos, curChapter().txtChapter.getPageSize()); float tipBottom; float tipLeft; //初始化标题的参数 //需要注意的是:绘制text的y的起始点是text的基准线的位置,而不是从text的头部的位置 if (!hideStatusBar) { //显示状态栏 if (txtChapter.getStatus() != TxtChapter.Status.FINISH) { if (isChapterListPrepare) { //绘制标题 title = TextUtils.ellipsize(title, mTipPaint, tipVisibleWidth, TextUtils.TruncateAt.END).toString(); canvas.drawText(title, tipMarginLeft, tipBottomBot, mTipPaint); } } else { //绘制总进度 tipLeft = displayRightEnd - mTipPaint.measureText(progress); canvas.drawText(progress, tipLeft, tipBottomBot, mTipPaint); //绘制页码 tipLeft = tipLeft - tipDistance - mTipPaint.measureText(page); canvas.drawText(page, tipLeft, tipBottomBot, mTipPaint); //绘制标题 title = TextUtils.ellipsize(title, mTipPaint, tipLeft - tipDistance, TextUtils.TruncateAt.END).toString(); canvas.drawText(title, tipMarginLeft, tipBottomBot, mTipPaint); } if (readBookControl.getShowLine()) { //绘制分隔线 tipBottom = mDisplayHeight - tipMarginBottom; canvas.drawRect(tipMarginLeft, tipBottom, displayRightEnd, tipBottom + 2, mTipPaint); } } else { //隐藏状态栏 if (getPageStatus() != TxtChapter.Status.FINISH) { if (isChapterListPrepare) { //绘制标题 title = TextUtils.ellipsize(title, mTipPaint, tipVisibleWidth, TextUtils.TruncateAt.END).toString(); canvas.drawText(title, tipMarginLeft, tipBottomTop, mTipPaint); } } else { //绘制标题 float titleTipLength = showTimeBattery ? tipVisibleWidth - mTipPaint.measureText(progress) - tipDistance : tipVisibleWidth; title = TextUtils.ellipsize(title, mTipPaint, titleTipLength, TextUtils.TruncateAt.END).toString(); canvas.drawText(title, tipMarginLeft, tipBottomTop, mTipPaint); // 绘制页码 canvas.drawText(page, tipMarginLeft, tipBottomBot, mTipPaint); //绘制总进度 float progressTipLeft = displayRightEnd - mTipPaint.measureText(progress); float progressTipBottom = showTimeBattery ? tipBottomTop : tipBottomBot; canvas.drawText(progress, progressTipLeft, progressTipBottom, mTipPaint); } if (readBookControl.getShowLine()) { //绘制分隔线 tipBottom = tipMarginTop - 2; canvas.drawRect(tipMarginLeft, tipBottom, displayRightEnd, tipBottom + 2, mTipPaint); } } } int visibleRight = (int) displayRightEnd; if (hideStatusBar && showTimeBattery) { //绘制当前时间 String time = StringUtils.dateConvert(System.currentTimeMillis(), AppConstant.FORMAT_TIME); float timeTipLeft = (mDisplayWidth - mTipPaint.measureText(time)) / 2; canvas.drawText(time, timeTipLeft, tipBottomBot, mTipPaint); //绘制电池 int polarHeight = ScreenUtils.dpToPx(4); int polarWidth = ScreenUtils.dpToPx(2); int border = 2; int outFrameWidth = (int) mBatteryPaint.measureText("0000") + polarWidth; int outFrameHeight = (int) mBatteryPaint.getTextSize() + oneSpPx; int visibleBottom = mDisplayHeight - (tipMarginBottom - outFrameHeight) / 2; //电极的制作 int polarLeft = visibleRight - polarWidth; int polarTop = visibleBottom - (outFrameHeight + polarHeight) / 2; Rect polar = new Rect(polarLeft, polarTop, visibleRight, polarTop + polarHeight); mBatteryPaint.setStyle(Paint.Style.FILL); canvas.drawRect(polar, mBatteryPaint); //外框的制作 int outFrameLeft = polarLeft - outFrameWidth; int outFrameTop = visibleBottom - outFrameHeight; Rect outFrame = new Rect(outFrameLeft, outFrameTop, polarLeft, visibleBottom); mBatteryPaint.setStyle(Paint.Style.STROKE); mBatteryPaint.setStrokeWidth(border); canvas.drawRect(outFrame, mBatteryPaint); //绘制电量 mBatteryPaint.setStyle(Paint.Style.FILL); Paint.FontMetrics fontMetrics = mBatteryPaint.getFontMetrics(); String batteryLevel = String.valueOf(mBatteryLevel); float batTextLeft = outFrameLeft + (outFrameWidth - mBatteryPaint.measureText(batteryLevel)) / 2; float batTextBaseLine = visibleBottom - outFrameHeight / 2f - fontMetrics.top / 2 - fontMetrics.bottom / 2; canvas.drawText(batteryLevel, batTextLeft, batTextBaseLine, mBatteryPaint); } } /** * 绘制内容 */ private synchronized void drawContent(Bitmap bitmap, TxtChapter txtChapter, TxtPage txtPage) { if (bitmap == null) return; Canvas canvas = new Canvas(bitmap); if (mPageMode == PageAnimation.Mode.SCROLL) { bitmap.eraseColor(Color.TRANSPARENT); } Paint.FontMetrics fontMetricsForTitle = mTitlePaint.getFontMetrics(); Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); if (txtChapter.getStatus() != TxtChapter.Status.FINISH) { //绘制字体 String tip = getStatusText(txtChapter); drawErrorMsg(canvas, tip, 0); } else { float top = contentMarginHeight - fontMetrics.ascent; if (mPageMode != PageAnimation.Mode.SCROLL) { top += readBookControl.getHideStatusBar() ? mMarginTop : mPageView.getStatusBarHeight() + mMarginTop; } int ppp = 0;//pzl,文字位置 //对标题进行绘制 String str; int strLength = 0; boolean isLight; for (int i = 0; i < txtPage.getTitleLines(); ++i) { str = txtPage.getLine(i); strLength = strLength + str.length(); isLight = ReadAloudService.running && readAloudParagraph == 0; mTitlePaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor()); //进行绘制 canvas.drawText(str, mDisplayWidth / 2f, top, mTitlePaint); //pzl float leftposition = mDisplayWidth / 2; float rightposition = 0; float bottomposition = top + mTitlePaint.getFontMetrics().descent; float TextHeight = Math.abs(fontMetricsForTitle.ascent) + Math.abs(fontMetricsForTitle.descent); if (txtPage.getTxtLists() != null) { for (TxtChar c : txtPage.getTxtLists().get(i).getCharsData()) { rightposition = leftposition + c.getCharWidth(); Point tlp = new Point(); c.setTopLeftPosition(tlp); tlp.x = (int) leftposition; tlp.y = (int) (bottomposition - TextHeight); Point blp = new Point(); c.setBottomLeftPosition(blp); blp.x = (int) leftposition; blp.y = (int) bottomposition; Point trp = new Point(); c.setTopRightPosition(trp); trp.x = (int) rightposition; trp.y = (int) (bottomposition - TextHeight); Point brp = new Point(); c.setBottomRightPosition(brp); brp.x = (int) rightposition; brp.y = (int) bottomposition; ppp++; c.setIndex(ppp); leftposition = rightposition; } } //设置尾部间距 if (i == txtPage.getTitleLines() - 1) { top += titlePara; } else { //行间距 top += titleInterval; } } if (txtPage.getLines().isEmpty()) { return; } //对内容进行绘制 for (int i = txtPage.getTitleLines(); i < txtPage.size(); ++i) { str = txtPage.getLine(i); strLength = strLength + str.length(); int paragraphLength = txtPage.getPosition() == 0 ? strLength : txtChapter.getPageLength(txtPage.getPosition() - 1) + strLength; isLight = ReadAloudService.running && readAloudParagraph == txtChapter.getParagraphIndex(paragraphLength); mTextPaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor()); Layout tempLayout = new StaticLayout(str, mTextPaint, mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false); float width = StaticLayout.getDesiredWidth(str, tempLayout.getLineStart(0), tempLayout.getLineEnd(0), mTextPaint); if (needScale(str)) { drawScaledText(canvas, str, width, mTextPaint, top, i, txtPage.getTxtLists()); } else { canvas.drawText(str, mMarginLeft, top, mTextPaint); } //记录文字位置 --开始 pzl float leftposition = mMarginLeft; if (isFirstLineOfParagraph(str)) { String blanks = StringUtils.halfToFull(" "); //canvas.drawText(blanks, x, top, mTextPaint); float bw = StaticLayout.getDesiredWidth(blanks, mTextPaint); leftposition += bw; } float rightposition = 0; float bottomposition = top + mTextPaint.getFontMetrics().descent; float textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent); if (txtPage.getTxtLists() != null) { for (TxtChar c : txtPage.getTxtLists().get(i).getCharsData()) { rightposition = leftposition + c.getCharWidth(); Point tlp = new Point(); c.setTopLeftPosition(tlp); tlp.x = (int) leftposition; tlp.y = (int) (bottomposition - textHeight); Point blp = new Point(); c.setBottomLeftPosition(blp); blp.x = (int) leftposition; blp.y = (int) bottomposition; Point trp = new Point(); c.setTopRightPosition(trp); trp.x = (int) rightposition; trp.y = (int) (bottomposition - textHeight); Point brp = new Point(); c.setBottomRightPosition(brp); brp.x = (int) rightposition; brp.y = (int) bottomposition; leftposition = rightposition; ppp++; c.setIndex(ppp); } } //记录文字位置 --结束 pzl //设置尾部间距 if (str.endsWith("\n")) { top += textPara; } else { top += textInterval; } } } } public void drawCover(Canvas canvas, float top) { } private int getCoverHeight() { return cover == null ? 0 : cover.getHeight() + 20; } /** * 绘制内容-滚动 */ void drawContent(final Canvas canvas, float offset) { if (offset > MAX_SCROLL_OFFSET) { offset = MAX_SCROLL_OFFSET; } else if (offset < 0 - MAX_SCROLL_OFFSET) { offset = -MAX_SCROLL_OFFSET; } boolean pageChanged = false; Paint.FontMetrics fontMetricsForTitle = mTitlePaint.getFontMetrics(); Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); final float totalHeight = mVisibleHeight + titleInterval; if (curChapter().txtChapter == null) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); } if (!isLastPage || offset < 0) { pageOffset += offset; isLastPage = false; } // 首页 if (pageOffset < 0 && mCurChapterPos == 0 && mCurPagePos == 0) { pageOffset = 0; } float cHeight = getFixedPageHeight(curChapter().txtChapter, mCurPagePos); cHeight = cHeight > 0 ? cHeight : mVisibleHeight; if (offset > 0 && pageOffset > cHeight) { while (pageOffset > cHeight) { switchToPageOffset(1); pageOffset -= cHeight; cHeight = getFixedPageHeight(curChapter().txtChapter, mCurPagePos); cHeight = cHeight > 0 ? cHeight : mVisibleHeight; pageChanged = true; } } else if (offset < 0 && pageOffset < 0) { while (pageOffset < 0) { switchToPageOffset(-1); cHeight = getFixedPageHeight(curChapter().txtChapter, mCurPagePos); cHeight = cHeight > 0 ? cHeight : mVisibleHeight; pageOffset += cHeight; pageChanged = true; } } if (pageChanged) { chapterChangeCallback(); pagingEnd(PageAnimation.Direction.NONE); } float top = contentMarginHeight - mTextPaint.ascent() - pageOffset; int chapterPos = mCurChapterPos; int pagePos = mCurPagePos; boolean isLight; int ppp = 0;//pzl,文字位置 if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) { String tip = getStatusText(curChapter().txtChapter); drawErrorMsg(canvas, tip, pageOffset); top += mVisibleHeight; chapterPos += 1; pagePos = 0; } String str; linePos = 0; boolean linePosSet = false; boolean bookEnd = false; float startHeight = -2 * titleInterval; if (pageOffset < mTextPaint.getTextSize()) { linePos = 0; linePosSet = true; } while (top < totalHeight) { TxtChapter chapter = chapterPos == mCurChapterPos ? curChapter().txtChapter : nextChapter().txtChapter; if (chapter == null || chapterPos - mCurChapterPos > 1) break; if (chapter.getStatus() != TxtChapter.Status.FINISH) { String tip = getStatusText(chapter); drawErrorMsg(canvas, tip, 0 - top); top += mVisibleHeight; chapterPos += 1; pagePos = 0; continue; } if (chapter.getPageSize() == 0) break; TxtPage page = chapter.getPage(pagePos); if (page.getLines().isEmpty()) break; if (top > totalHeight) break; float topi = top; int strLength = 0; isLight = ReadAloudService.running && readAloudParagraph == 0; mTitlePaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor()); for (int i = 0; i < page.getTitleLines(); i++) { if (top > totalHeight) { break; } else if (top > startHeight) { str = page.getLine(i); strLength = strLength + str.length(); //进行绘制 canvas.drawText(str, mDisplayWidth / 2f, top, mTitlePaint); //pzl float leftposition = mDisplayWidth / 2f; float rightPosition = 0; float bottomPosition = top + mTitlePaint.getFontMetrics().descent; float TextHeight = Math.abs(fontMetricsForTitle.ascent) + Math.abs(fontMetricsForTitle.descent); if (page.getTxtLists() != null) { for (TxtChar c : page.getTxtLists().get(i).getCharsData()) { rightPosition = leftposition + c.getCharWidth(); Point tlp = new Point(); c.setTopLeftPosition(tlp); tlp.x = (int) leftposition; tlp.y = (int) (bottomPosition - TextHeight); Point blp = new Point(); c.setBottomLeftPosition(blp); blp.x = (int) leftposition; blp.y = (int) bottomPosition; Point trp = new Point(); c.setTopRightPosition(trp); trp.x = (int) rightPosition; trp.y = (int) (bottomPosition - TextHeight); Point brp = new Point(); c.setBottomRightPosition(brp); brp.x = (int) rightPosition; brp.y = (int) bottomPosition; ppp++; c.setIndex(ppp); leftposition = rightPosition; } } //pzl } top += (i == page.getTitleLines() - 1) ? titlePara : titleInterval; if (!linePosSet && chapterPos == mCurChapterPos && top > titlePara) { linePos = i; linePosSet = true; } } if (top > totalHeight) break; // 首页画封面 if (pagePos == 0 && chapterPos == 0) { drawCover(canvas, top); top += getCoverHeight(); } if (top > totalHeight) break; for (int i = page.getTitleLines(); i < page.size(); i++) { str = page.getLine(i); strLength = strLength + str.length(); int paragraphLength = page.getPosition() == 0 ? strLength : chapter.getPageLength(page.getPosition() - 1) + strLength; isLight = ReadAloudService.running && readAloudParagraph == chapter.getParagraphIndex(paragraphLength); mTextPaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor()); if (top > totalHeight) { break; } else if (top > startHeight) { Layout tempLayout = new StaticLayout(str, mTextPaint, mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false); float width = StaticLayout.getDesiredWidth(str, tempLayout.getLineStart(0), tempLayout.getLineEnd(0), mTextPaint); if (needScale(str)) { drawScaledText(canvas, str, width, mTextPaint, top, i, page.getTxtLists()); } else { canvas.drawText(str, mMarginLeft, top, mTextPaint); } //记录文字位置 --开始 pzl float leftposition = mMarginLeft; if (isFirstLineOfParagraph(str)) { String blanks = StringUtils.halfToFull(" "); //canvas.drawText(blanks, x, top, mTextPaint); float bw = StaticLayout.getDesiredWidth(blanks, mTextPaint); leftposition += bw; } float rightposition = 0; float bottomposition = top + mTextPaint.getFontMetrics().descent; float textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent); if (page.getTxtLists() != null) { for (TxtChar c : page.getTxtLists().get(i).getCharsData()) { rightposition = leftposition + c.getCharWidth(); Point tlp = new Point(); c.setTopLeftPosition(tlp); tlp.x = (int) leftposition; tlp.y = (int) (bottomposition - textHeight); Point blp = new Point(); c.setBottomLeftPosition(blp); blp.x = (int) leftposition; blp.y = (int) bottomposition; Point trp = new Point(); c.setTopRightPosition(trp); trp.x = (int) rightposition; trp.y = (int) (bottomposition - textHeight); Point brp = new Point(); c.setBottomRightPosition(brp); brp.x = (int) rightposition; brp.y = (int) bottomposition; leftposition = rightposition; ppp++; c.setIndex(ppp); } } //记录文字位置 --结束 pzl } top += str.endsWith("\n") ? textPara : textInterval; if (!linePosSet && chapterPos == mCurChapterPos && top >= textPara) { linePos = i; linePosSet = true; } } if (top > totalHeight) break; if (pagePos == chapter.getPageSize() - 1) { String sign = "\u23af \u23af"; if (chapterPos == book.getChapterListSize() - 1) { bookEnd = pagePos == mCurPagePos; str = sign + " 所有章节已读完 " + sign; } else { str = sign + " 本章完 " + sign; } top += textPara; canvas.drawText(str, mDisplayWidth / 2f, top, mTextEndPaint); top += textPara * 2; } if (top > totalHeight) break; if (chapter.getPageSize() == 1) { float pHeight = getFixedPageHeight(chapter, pagePos); if (top - topi < pHeight) { top = topi + pHeight; } if (top > totalHeight) break; } if (pagePos >= chapter.getPageSize() - 1) { chapterPos += 1; pagePos = 0; top += 60; } else { pagePos += 1; } if (bookEnd && top < mVisibleHeight) { isLastPage = true; break; } } } void resetPageOffset() { pageOffset = 0; linePos = 0; isLastPage = false; } private void switchToPageOffset(int offset) { switch (offset) { case 1: if (mCurPagePos < curChapter().txtChapter.getPageSize() - 1) { mCurPagePos = mCurPagePos + 1; } else if (mCurChapterPos < book.getChapterListSize() - 1) { mCurChapterPos = mCurChapterPos + 1; Collections.swap(chapterContainers, 0, 1); Collections.swap(chapterContainers, 1, 2); nextChapter().txtChapter = null; mCurPagePos = 0; if (curChapter().txtChapter == null) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); parseCurChapter(); } else { parseNextChapter(); } } break; case -1: if (mCurPagePos > 0) { mCurPagePos = mCurPagePos - 1; } else if (mCurChapterPos > 0) { mCurChapterPos = mCurChapterPos - 1; Collections.swap(chapterContainers, 2, 1); Collections.swap(chapterContainers, 1, 0); prevChapter().txtChapter = null; if (curChapter().txtChapter == null) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); mCurPagePos = 0; parseCurChapter(); } else { mCurPagePos = curChapter().txtChapter.getPageSize() - 1; parsePrevChapter(); } } break; default: break; } } private float getFixedPageHeight(TxtChapter chapter, int pagePos) { float height = getPageHeight(chapter, pagePos); if (height == 0) { return height; } int lastPageIndex = chapter.getPageSize() - 1; if (pagePos == lastPageIndex) { height += 60 + textPara * 3; } if (lastPageIndex <= 0 && height < mVisibleHeight / 2.0f) { height = mVisibleHeight / 2.0f; } return height; } private float getPageHeight(TxtChapter chapter, int pagePos) { float height = 0; if (chapter == null || chapter.getStatus() != TxtChapter.Status.FINISH) { return height; } if (pagePos >= 0 && pagePos < chapter.getPageSize()) { height = getPageHeight(chapter.getPage(pagePos)); } if (chapter.getPosition() == 0 && pagePos == 0) { height += getCoverHeight(); } return height; } private float getPageHeight(TxtPage page) { if (page.getLines().isEmpty()) return 0; float height = 0; if (page.getTitleLines() > 0) height += titleInterval * (page.getTitleLines() - 1) + titlePara; for (int i = page.getTitleLines(); i < page.size(); i++) { height += page.getLine(i).endsWith("\n") ? textPara : textInterval; } return height; } private void drawErrorMsg(Canvas canvas, String msg, float offset) { Layout tempLayout = new StaticLayout(msg, mTextPaint, mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false); List linesData = new ArrayList<>(); for (int i = 0; i < tempLayout.getLineCount(); i++) { linesData.add(msg.substring(tempLayout.getLineStart(i), tempLayout.getLineEnd(i))); } float pivotY = (mDisplayHeight - textInterval * linesData.size()) / 3f - offset; for (String str : linesData) { float textWidth = mTextPaint.measureText(str); float pivotX = (mDisplayWidth - textWidth) / 2; canvas.drawText(str, pivotX, pivotY, mTextPaint); pivotY += textInterval; } } /** * 获取状态文本 */ private String getStatusText(TxtChapter chapter) { String tip = ""; switch (chapter.getStatus()) { case LOADING: tip = mContext.getString(R.string.loading); break; case ERROR: tip = mContext.getString(R.string.load_error_msg, curChapter().txtChapter.getMsg()); break; case EMPTY: tip = mContext.getString(R.string.content_empty); break; case CATEGORY_EMPTY: tip = mContext.getString(R.string.chapter_list_empty); break; case CHANGE_SOURCE: tip = mContext.getString(R.string.on_change_source); } return tip; } /** * 判断是否存在上一页 */ boolean hasPrev() { // 以下情况禁止翻页 if (canNotTurnPage()) { return false; } if (getPageStatus() == TxtChapter.Status.FINISH) { // 先查看是否存在上一页 if (mCurPagePos > 0) { return true; } } return mCurChapterPos > 0; } /** * 判断是否下一页存在 */ boolean hasNext(int pageOnCur) { // 以下情况禁止翻页 if (canNotTurnPage()) { return false; } if (getPageStatus() == TxtChapter.Status.FINISH) { // 先查看是否存在下一页 if (mCurPagePos + pageOnCur < curChapter().txtChapter.getPageSize() - 1) { return true; } } return mCurChapterPos + 1 < book.getChapterListSize(); } /** * 解析当前页数据 */ void parseCurChapter() { if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) { Single.create((SingleOnSubscribe) e -> { ChapterProvider chapterProvider = new ChapterProvider(this); TxtChapter txtChapter = chapterProvider.dealLoadPageList(callback.getChapterList().get(mCurChapterPos), mPageView.isPrepare()); e.onSuccess(txtChapter); }) .compose(RxUtils::toSimpleSingle) .subscribe(new SingleObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onSuccess(TxtChapter txtChapter) { upTextChapter(txtChapter); } @Override public void onError(Throwable e) { if (curChapter().txtChapter == null || curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) { curChapter().txtChapter = new TxtChapter(mCurChapterPos); curChapter().txtChapter.setStatus(TxtChapter.Status.ERROR); curChapter().txtChapter.setMsg(e.getMessage()); } } }); } parsePrevChapter(); parseNextChapter(); } /** * 解析上一章数据 */ void parsePrevChapter() { final int prevChapterPos = mCurChapterPos - 1; if (prevChapterPos < 0) { prevChapter().txtChapter = null; return; } if (prevChapter().txtChapter == null) prevChapter().txtChapter = new TxtChapter(prevChapterPos); if (prevChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) { return; } Single.create((SingleOnSubscribe) e -> { ChapterProvider chapterProvider = new ChapterProvider(this); TxtChapter txtChapter = chapterProvider.dealLoadPageList(callback.getChapterList().get(prevChapterPos), mPageView.isPrepare()); e.onSuccess(txtChapter); }) .compose(RxUtils::toSimpleSingle) .subscribe(new SingleObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onSuccess(TxtChapter txtChapter) { upTextChapter(txtChapter); } @Override public void onError(Throwable e) { if (prevChapter().txtChapter == null || prevChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) { prevChapter().txtChapter = new TxtChapter(prevChapterPos); prevChapter().txtChapter.setStatus(TxtChapter.Status.ERROR); prevChapter().txtChapter.setMsg(e.getMessage()); } } }); } /** * 解析下一章数据 */ void parseNextChapter() { final int nextChapterPos = mCurChapterPos + 1; if (nextChapterPos >= callback.getChapterList().size()) { nextChapter().txtChapter = null; return; } if (nextChapter().txtChapter == null) nextChapter().txtChapter = new TxtChapter(nextChapterPos); if (nextChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) { return; } Single.create((SingleOnSubscribe) e -> { ChapterProvider chapterProvider = new ChapterProvider(this); TxtChapter txtChapter = chapterProvider.dealLoadPageList(callback.getChapterList().get(nextChapterPos), mPageView.isPrepare()); e.onSuccess(txtChapter); }) .compose(RxUtils::toSimpleSingle) .subscribe(new SingleObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onSuccess(TxtChapter txtChapter) { upTextChapter(txtChapter); } @Override public void onError(Throwable e) { if (nextChapter().txtChapter == null || nextChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) { nextChapter().txtChapter = new TxtChapter(nextChapterPos); nextChapter().txtChapter.setStatus(TxtChapter.Status.ERROR); nextChapter().txtChapter.setMsg(e.getMessage()); } } }); } private void upTextChapter(TxtChapter txtChapter) { if (txtChapter.getPosition() == mCurChapterPos - 1) { prevChapter().txtChapter = txtChapter; if (mPageMode == PageAnimation.Mode.SCROLL) { mPageView.drawContent(-1); } else { mPageView.drawPage(-1); } } else if (txtChapter.getPosition() == mCurChapterPos) { curChapter().txtChapter = txtChapter; reSetPage(); chapterChangeCallback(); pagingEnd(PageAnimation.Direction.NONE); } else if (txtChapter.getPosition() == mCurChapterPos + 1) { nextChapter().txtChapter = txtChapter; if (mPageMode == PageAnimation.Mode.SCROLL) { mPageView.drawContent(1); } else { mPageView.drawPage(1); } } } private void drawScaledText(Canvas canvas, String line, float lineWidth, TextPaint paint, float top, int y, List txtLists) { float x = mMarginLeft; if (isFirstLineOfParagraph(line)) { canvas.drawText(indent, x, top, paint); float bw = StaticLayout.getDesiredWidth(indent, paint); x += bw; line = line.substring(readBookControl.getIndent()); } int gapCount = line.length() - 1; int i = 0; TxtLine txtList = new TxtLine();//每一行pzl txtList.setCharsData(new ArrayList<>());//pzl float d = ((mDisplayWidth - (mMarginLeft + mMarginRight)) - lineWidth) / gapCount; for (; i < line.length(); i++) { String c = String.valueOf(line.charAt(i)); float cw = StaticLayout.getDesiredWidth(c, paint); canvas.drawText(c, x, top, paint); //pzl TxtChar txtChar = new TxtChar(); txtChar.setChardata(line.charAt(i)); if (i == 0) txtChar.setCharWidth(cw + d / 2); if (i == gapCount) txtChar.setCharWidth(cw + d / 2); txtChar.setCharWidth(cw + d); ;//字宽 //txtChar.Index = y;//每页每个字的位置 txtList.getCharsData().add(txtChar); //pzl x += cw + d; } if (txtLists != null) { txtLists.set(y, txtList);//pzl } } //判断是不是d'hou private boolean isFirstLineOfParagraph(String line) { return line.length() > 3 && line.charAt(0) == (char) 12288 && line.charAt(1) == (char) 12288; } private boolean needScale(String line) {//判断不是空行 return line != null && line.length() != 0 && line.charAt(line.length() - 1) != '\n'; } private void chapterChangeCallback() { if (callback != null) { readAloudParagraph = -1; callback.onChapterChange(mCurChapterPos); callback.onPageCountChange(curChapter().txtChapter != null ? curChapter().txtChapter.getPageSize() : 0); } } public abstract void updateChapter(); /** * 根据当前状态,决定是否能够翻页 */ private boolean canNotTurnPage() { return !isChapterListPrepare || getPageStatus() == TxtChapter.Status.CHANGE_SOURCE; } /** * 关闭书本 */ public void closeBook() { compositeDisposable.dispose(); compositeDisposable = null; isChapterListPrepare = false; isClose = true; prevChapter().txtChapter = null; curChapter().txtChapter = null; nextChapter().txtChapter = null; } public boolean isClose() { return isClose; } private ChapterContainer prevChapter() { return chapterContainers.get(0); } ChapterContainer curChapter() { return chapterContainers.get(1); } private ChapterContainer nextChapter() { return chapterContainers.get(2); } /*****************************************interface*****************************************/ static class ChapterContainer { TxtChapter txtChapter; } /** * -------------------- * 检测获取按压坐标所在位置的字符,没有的话返回null * -------------------- * author: huangwei * 2017年7月4日上午10:23:19 */ TxtChar detectPressTxtChar(float down_X2, float down_Y2) { TxtPage txtPage = curChapter().txtChapter.getPage(mCurPagePos); if (txtPage == null) return null; List txtLines = txtPage.getTxtLists(); if (txtLines == null) return null; for (TxtLine l : txtLines) { List txtChars = l.getCharsData(); if (txtChars != null) { for (TxtChar c : txtChars) { Point leftPoint = c.getBottomLeftPosition(); Point rightPoint = c.getBottomRightPosition(); if (leftPoint != null && down_Y2 > leftPoint.y) { break;// 说明是在下一行 } if (leftPoint != null && rightPoint != null && down_X2 >= leftPoint.x && down_X2 <= rightPoint.x) { return c; } } } } return null; } public interface Callback { List getChapterList(); /** * 作用:章节切换的时候进行回调 * * @param pos:切换章节的序号 */ void onChapterChange(int pos); /** * 作用:章节目录加载完成时候回调 * * @param chapters:返回章节目录 */ void onCategoryFinish(List chapters); /** * 作用:章节页码数量改变之后的回调。==> 字体大小的调整,或者是否关闭虚拟按钮功能都会改变页面的数量。 * * @param count:页面的数量 */ void onPageCountChange(int count); /** * 作用:当页面改变的时候回调 * * @param chapterIndex 章节序号 * @param pageIndex 页数 * @param resetReadAloud 是否重置朗读 */ void onPageChange(int chapterIndex, int pageIndex, boolean resetReadAloud); void vipPop(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoaderEpub.java ================================================ package com.kunfei.bookshelf.widget.page; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.text.TextUtils; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.StringUtils; import net.sf.jazzlib.ZipFile; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.TextNode; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.domain.MediaType; import nl.siegmann.epublib.domain.Metadata; import nl.siegmann.epublib.domain.Resource; import nl.siegmann.epublib.domain.SpineReference; import nl.siegmann.epublib.domain.TOCReference; import nl.siegmann.epublib.epub.EpubReader; import nl.siegmann.epublib.service.MediatypeService; public class PageLoaderEpub extends PageLoader { //编码类型 private Charset mCharset; private Book epubBook; private List chapterList; PageLoaderEpub(PageView pageView, BookShelfBean bookShelfBean, Callback callback) { super(pageView, bookShelfBean, callback); } @Override public void refreshChapterList() { if (book == null) return; Observable.create((ObservableOnSubscribe) e -> { File bookFile = new File(book.getNoteUrl()); epubBook = readBook(bookFile); if (epubBook == null) { e.onError(new Exception("文件解析失败")); return; } if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) { book.getBookInfoBean().setCharset("UTF-8"); } mCharset = Charset.forName(book.getBookInfoBean().getCharset()); e.onNext(book); e.onComplete(); }).subscribeOn(Schedulers.single()) .flatMap(this::checkChapterList) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(BookShelfBean bookShelfBean) { isChapterListPrepare = true; // 加载并显示当前章节 skipToChapter(bookShelfBean.getDurChapter(), bookShelfBean.getDurChapterPage()); } @Override public void onError(Throwable e) { durDhapterError(e.getMessage()); } }); } public static Book readBook(File file) { try { EpubReader epubReader = new EpubReader(); MediaType[] lazyTypes = { MediatypeService.CSS, MediatypeService.GIF, MediatypeService.JPG, MediatypeService.PNG, MediatypeService.MP3, MediatypeService.MP4}; ZipFile zipFile = new ZipFile(file); return epubReader.readEpubLazy(zipFile, "utf-8", Arrays.asList(lazyTypes)); } catch (Exception e) { return null; } } private void extractScaledCoverImage() { try { byte[] data = epubBook.getCoverImage().getData(); Bitmap rawCover = BitmapFactory.decodeByteArray(data, 0, data.length); int width = rawCover.getWidth(); int height = rawCover.getHeight(); if (width <= mVisibleWidth && width >= 0.8 * mVisibleWidth) { cover = rawCover; return; } height = Math.round(mVisibleWidth * 1.0f * height / width); cover = Bitmap.createScaledBitmap(rawCover, mVisibleWidth, height, true); } catch (Exception ignored) { } } @Override public void drawCover(Canvas canvas, float top) { if (cover == null) { extractScaledCoverImage(); } if (cover == null) return; int left = (mDisplayWidth - cover.getWidth()) / 2; canvas.drawBitmap(cover, left, top, mTextPaint); } private List loadChapters() { Metadata metadata = epubBook.getMetadata(); book.getBookInfoBean().setName(metadata.getFirstTitle()); if (metadata.getAuthors().size() > 0) { String author = metadata.getAuthors().get(0).toString().replaceAll("^, |, $", ""); book.getBookInfoBean().setAuthor(author); } if (metadata.getDescriptions().size() > 0) { book.getBookInfoBean().setIntroduce(Jsoup.parse(metadata.getDescriptions().get(0)).text()); } chapterList = new ArrayList<>(); List refs = epubBook.getTableOfContents().getTocReferences(); if (refs == null || refs.isEmpty()) { List spineReferences = epubBook.getSpine().getSpineReferences(); for (int i = 0, size = spineReferences.size(); i < size; i++) { Resource resource = spineReferences.get(i).getResource(); String title = resource.getTitle(); if (TextUtils.isEmpty(title)) { try { Document doc = Jsoup.parse(new String(resource.getData(), mCharset)); Elements elements = doc.getElementsByTag("title"); if (elements.size() > 0) { title = elements.get(0).text(); } } catch (IOException e) { e.printStackTrace(); } } BookChapterBean bean = new BookChapterBean(); bean.setDurChapterIndex(i); bean.setNoteUrl(bean.getNoteUrl()); bean.setDurChapterUrl(resource.getHref()); if (i == 0 && title.isEmpty()) { bean.setDurChapterName("封面"); } else { bean.setDurChapterName(title); } chapterList.add(bean); } } else { parseMenu(refs, 0); for (int i = 0; i < chapterList.size(); i++) { chapterList.get(i).setDurChapterIndex(i); } } return chapterList; } private void parseMenu(List refs, int level) { if (refs == null) return; for (TOCReference ref : refs) { if (ref.getResource() != null) { BookChapterBean bookChapterBean = new BookChapterBean(); bookChapterBean.setNoteUrl(book.getNoteUrl()); bookChapterBean.setDurChapterName(ref.getTitle()); bookChapterBean.setDurChapterUrl(ref.getCompleteHref()); chapterList.add(bookChapterBean); } if (ref.getChildren() != null && !ref.getChildren().isEmpty()) { parseMenu(ref.getChildren(), level + 1); } } } @Override protected String getChapterContent(BookChapterBean chapter) throws Exception { Resource resource = epubBook.getResources().getByHref(chapter.getDurChapterUrl()); StringBuilder content = new StringBuilder(); Document doc = Jsoup.parse(new String(resource.getData(), mCharset)); Elements elements = doc.getAllElements(); for (Element element : elements) { List contentEs = element.textNodes(); for (int i = 0; i < contentEs.size(); i++) { String text = contentEs.get(i).text().trim(); text = StringUtils.formatHtml(text); if (elements.size() > 1) { if (text.length() > 0) { if (content.length() > 0) { content.append("\r\n"); } content.append("\u3000\u3000").append(text); } } else { content.append(text); } } } return content.toString(); } private Observable checkChapterList(BookShelfBean collBook) { if (!collBook.getHasUpdate() && !callback.getChapterList().isEmpty()) { return Observable.just(collBook); } else { return Observable.create((ObservableOnSubscribe>) e -> { List chapterList = loadChapters(); if (!chapterList.isEmpty()) { e.onNext(chapterList); } else { e.onError(new IllegalAccessException("epubBook sub-chapter failed!")); } e.onComplete(); }) .flatMap(chapterList -> { collBook.setChapterListSize(chapterList.size()); callback.onCategoryFinish(chapterList); return Observable.just(collBook); }) .doOnNext(bookShelfBean -> { // 存储章节到数据库 bookShelfBean.setHasUpdate(false); bookShelfBean.setFinalRefreshData(System.currentTimeMillis()); }); } } @Override protected boolean noChapterData(BookChapterBean chapter) { return false; } @Override public void updateChapter() { mPageView.getActivity().toast("目录更新中"); Observable.create((ObservableOnSubscribe) e -> { if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) { book.getBookInfoBean().setCharset("UTF-8"); } mCharset = Charset.forName(book.getBookInfoBean().getCharset()); //清除原目录 BookshelfHelp.delChapterList(book.getNoteUrl()); callback.getChapterList().clear(); e.onNext(book); e.onComplete(); }).flatMap(this::checkChapterList) .compose(RxUtils::toSimpleSingle) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(BookShelfBean bookShelfBean) { mPageView.getActivity().toast("更新完成"); isChapterListPrepare = true; // 加载并显示当前章节 skipToChapter(bookShelfBean.getDurChapter(), bookShelfBean.getDurChapterPage()); } @Override public void onError(Throwable e) { durDhapterError(e.getMessage()); } @Override public void onComplete() { } }); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoaderNet.java ================================================ package com.kunfei.bookshelf.widget.page; import android.annotation.SuppressLint; import com.kunfei.bookshelf.base.observer.MyObserver; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookContentBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.WebBookModel; import com.kunfei.bookshelf.model.content.VipThrowable; import com.kunfei.bookshelf.model.content.WebBook; import com.kunfei.bookshelf.utils.NetworkUtils; import com.kunfei.bookshelf.utils.RxUtils; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; import io.reactivex.Observer; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; /** * 网络页面加载器 */ public class PageLoaderNet extends PageLoader { private static final String TAG = "PageLoaderNet"; private List downloadingChapterList = new ArrayList<>(); private ExecutorService executorService; private Scheduler scheduler; PageLoaderNet(PageView pageView, BookShelfBean bookShelfBean, Callback callback) { super(pageView, bookShelfBean, callback); executorService = Executors.newFixedThreadPool(20); scheduler = Schedulers.from(executorService); } @Override public void refreshChapterList() { if (!callback.getChapterList().isEmpty()) { isChapterListPrepare = true; // 打开章节 skipToChapter(book.getDurChapter(), book.getDurChapterPage()); } else { WebBookModel.getInstance().getChapterList(book) .compose(RxUtils::toSimpleSingle) .subscribe(new MyObserver>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(List chapterBeanList) { isChapterListPrepare = true; // 目录加载完成 if (!chapterBeanList.isEmpty()) { BookshelfHelp.delChapterList(book.getNoteUrl()); callback.onCategoryFinish(chapterBeanList); } // 加载并显示当前章节 skipToChapter(book.getDurChapter(), book.getDurChapterPage()); } @Override public void onError(Throwable e) { if (e instanceof WebBook.NoSourceThrowable) { mPageView.autoChangeSource(); } else { durDhapterError(e.getMessage()); } } }); } } public void changeSourceFinish(BookShelfBean bookShelfBean) { if (bookShelfBean == null) { openChapter(book.getDurChapter()); } else { this.book = bookShelfBean; refreshChapterList(); } } @SuppressLint("DefaultLocale") private synchronized void loadContent(final int chapterIndex) { if (downloadingChapterList.size() >= 20) return; if (chapterIndex >= callback.getChapterList().size() || DownloadingList(listHandle.CHECK, callback.getChapterList().get(chapterIndex).getDurChapterUrl())) return; if (null != book && callback.getChapterList().size() > 0) { Observable.create((ObservableOnSubscribe) e -> { if (shouldRequestChapter(chapterIndex)) { DownloadingList(listHandle.ADD, callback.getChapterList().get(chapterIndex).getDurChapterUrl()); e.onNext(chapterIndex); } e.onComplete(); }) .flatMap(index -> WebBookModel.getInstance().getBookContent(book, callback.getChapterList().get(chapterIndex), null)) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new MyObserver() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @SuppressLint("DefaultLocale") @Override public void onNext(BookContentBean bookContentBean) { DownloadingList(listHandle.REMOVE, bookContentBean.getDurChapterUrl()); finishContent(bookContentBean.getDurChapterIndex()); } @Override public void onError(Throwable e) { DownloadingList(listHandle.REMOVE, callback.getChapterList().get(chapterIndex).getDurChapterUrl()); if (chapterIndex == book.getDurChapter()) { if (e instanceof WebBook.NoSourceThrowable) { mPageView.autoChangeSource(); } else if (e instanceof VipThrowable) { callback.vipPop(); } else { durDhapterError(e.getMessage()); } } } }); } } /** * 编辑下载列表 */ private synchronized boolean DownloadingList(listHandle editType, String value) { if (editType == listHandle.ADD) { downloadingChapterList.add(value); return true; } else if (editType == listHandle.REMOVE) { downloadingChapterList.remove(value); return true; } else { return downloadingChapterList.indexOf(value) != -1; } } /** * 章节下载完成 */ private void finishContent(int chapterIndex) { if (chapterIndex == mCurChapterPos) { super.parseCurChapter(); } if (chapterIndex == mCurChapterPos - 1) { super.parsePrevChapter(); } if (chapterIndex == mCurChapterPos + 1) { super.parseNextChapter(); } } /** * 刷新当前章节 */ @SuppressLint("DefaultLocale") public void refreshDurChapter() { if (callback.getChapterList().isEmpty()) { updateChapter(); return; } if (callback.getChapterList().size() - 1 < mCurChapterPos) { mCurChapterPos = callback.getChapterList().size() - 1; } BookshelfHelp.delChapter(BookshelfHelp.getCachePathName(book.getBookInfoBean().getName(), book.getTag()), mCurChapterPos, callback.getChapterList().get(mCurChapterPos).getDurChapterName()); skipToChapter(mCurChapterPos, 0); } @Override protected String getChapterContent(BookChapterBean chapter) { return BookshelfHelp.getChapterCache(book, chapter); } @SuppressLint("DefaultLocale") @Override protected boolean noChapterData(BookChapterBean chapter) { return !BookshelfHelp.isChapterCached(book.getBookInfoBean().getName(), book.getTag(), chapter, book.isAudio()); } private boolean shouldRequestChapter(Integer chapterIndex) { return NetworkUtils.isNetWorkAvailable() && noChapterData(callback.getChapterList().get(chapterIndex)); } // 装载上一章节的内容 @Override void parsePrevChapter() { if (mCurChapterPos >= 1) { loadContent(mCurChapterPos - 1); } super.parsePrevChapter(); } // 装载当前章内容。 @Override void parseCurChapter() { for (int i = mCurChapterPos; i < Math.min(mCurChapterPos + 5, book.getChapterListSize()); i++) { loadContent(i); } super.parseCurChapter(); } // 装载下一章节的内容 @Override void parseNextChapter() { for (int i = mCurChapterPos; i < Math.min(mCurChapterPos + 5, book.getChapterListSize()); i++) { loadContent(i); } super.parseNextChapter(); } @Override public void updateChapter() { mPageView.getActivity().toast("目录更新中"); WebBookModel.getInstance().getChapterList(book) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onNext(List chapterBeanList) { isChapterListPrepare = true; if (chapterBeanList.size() > callback.getChapterList().size()) { mPageView.getActivity().toast("更新完成,有新章节"); callback.onCategoryFinish(chapterBeanList); } else { mPageView.getActivity().toast("更新完成,无新章节"); } // 加载并显示当前章节 skipToChapter(book.getDurChapter(), book.getDurChapterPage()); } @Override public void onError(Throwable e) { durDhapterError(e.getMessage()); } @Override public void onComplete() { } }); } @Override public void closeBook() { super.closeBook(); executorService.shutdown(); } public enum listHandle { ADD, REMOVE, CHECK } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoaderText.java ================================================ package com.kunfei.bookshelf.widget.page; import android.text.TextUtils; import com.kunfei.bookshelf.bean.BookChapterBean; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.BookshelfHelp; import com.kunfei.bookshelf.model.TxtChapterRuleManager; import com.kunfei.bookshelf.utils.EncodingDetect; import com.kunfei.bookshelf.utils.IOUtils; import com.kunfei.bookshelf.utils.MD5Utils; import com.kunfei.bookshelf.utils.RxUtils; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.SingleOnSubscribe; import io.reactivex.disposables.Disposable; import static com.kunfei.bookshelf.help.FileHelp.BLANK; /** * 加载本地书籍 */ public class PageLoaderText extends PageLoader { private static final String TAG = "PageLoaderText"; //默认从文件中获取数据的长度 private final static int BUFFER_SIZE = 512 * 1024; //没有标题的时候,每个章节的最大长度 private final static int MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024; private List chapterPatterns = new ArrayList<>(); //章节解析模式 private Pattern mChapterPattern = null; //获取书本的文件 private File mBookFile; //编码类型 private Charset mCharset; PageLoaderText(PageView pageView, BookShelfBean bookShelfBean, Callback callback) { super(pageView, bookShelfBean, callback); } @Override public void refreshChapterList() { Single.create((SingleOnSubscribe>) e -> { // 对于文件是否存在,或者为空的判断,不作处理。 ==> 在文件打开前处理过了。 mBookFile = new File(book.getNoteUrl()); //获取文件编码 if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) { book.getBookInfoBean().setCharset(EncodingDetect.getJavaEncode(mBookFile)); } mCharset = Charset.forName(book.getBookInfoBean().getCharset()); Long lastModified = mBookFile.lastModified(); if (book.getFinalRefreshData() < lastModified) { book.setFinalRefreshData(lastModified); book.setHasUpdate(true); } if (book.getHasUpdate() || callback.getChapterList().size() == 0) { List chapterBeanList = loadChapters(); book.setHasUpdate(false); e.onSuccess(chapterBeanList); } else { e.onSuccess(new ArrayList<>()); } }).compose(RxUtils::toSimpleSingle) .subscribe(new SingleObserver>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onSuccess(List bookChapterBeans) { isChapterListPrepare = true; // 目录加载完成,执行回调操作。 if (!bookChapterBeans.isEmpty()) { callback.onCategoryFinish(bookChapterBeans); } // 打开章节 skipToChapter(book.getDurChapter(), book.getDurChapterPage()); } @Override public void onError(Throwable e) { durDhapterError(e.getMessage()); } }); } @Override protected String getChapterContent(BookChapterBean chapter) { //从文件中获取数据 byte[] content = getChapterContentByte(chapter); return new String(content, mCharset); } @Override protected boolean noChapterData(BookChapterBean chapter) { return false; } @Override public void updateChapter() { mPageView.getActivity().toast("目录更新中"); Single.create((SingleOnSubscribe>) e -> { BookshelfHelp.delChapterList(book.getNoteUrl()); //获取文件编码 if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) { book.getBookInfoBean().setCharset(EncodingDetect.getJavaEncode(mBookFile)); } mCharset = Charset.forName(book.getBookInfoBean().getCharset()); e.onSuccess(loadChapters()); }) .compose(RxUtils::toSimpleSingle) .subscribe(new SingleObserver>() { @Override public void onSubscribe(Disposable d) { compositeDisposable.add(d); } @Override public void onSuccess(List value) { isChapterListPrepare = true; mPageView.getActivity().toast("更新完成"); book.setHasUpdate(false); // 提示目录加载完成 if (callback != null) { callback.onCategoryFinish(value); } // 加载并显示当前章节 openChapter(book.getDurChapterPage()); } @Override public void onError(Throwable e) { durDhapterError(e.getMessage()); } }); } /** * 从文件中提取一章的内容 */ private byte[] getChapterContentByte(BookChapterBean chapter) { RandomAccessFile bookStream = null; try { bookStream = new RandomAccessFile(mBookFile, "r"); bookStream.seek(chapter.getStart()); int extent = (int) (chapter.getEnd() - chapter.getStart()); byte[] content = new byte[extent]; bookStream.read(content, 0, extent); return content; } catch (Exception e) { e.printStackTrace(); } finally { IOUtils.close(bookStream); } return new byte[0]; } /** * 1. 检查文件中是否存在章节名 * 2. 判断文件中使用的章节名类型的正则表达式 * * @return 是否存在章节名 */ private boolean checkChapterType(RandomAccessFile bookStream) throws IOException { chapterPatterns.clear(); if (TextUtils.isEmpty(book.getBookInfoBean().getChapterUrl())) { chapterPatterns.addAll(TxtChapterRuleManager.enabledRuleList()); } else { chapterPatterns.add(book.getBookInfoBean().getChapterUrl()); } //首先获取128k的数据 byte[] buffer = new byte[BUFFER_SIZE / 4]; int length = bookStream.read(buffer, 0, buffer.length); //进行章节匹配 for (String str : chapterPatterns) { Pattern pattern = Pattern.compile(str, Pattern.MULTILINE); Matcher matcher = pattern.matcher(new String(buffer, 0, length, mCharset)); //如果匹配存在,那么就表示当前章节使用这种匹配方式 if (matcher.find()) { mChapterPattern = pattern; //重置指针位置 bookStream.seek(0); return true; } } //重置指针位置 bookStream.seek(0); return false; } /** * 未完成的部分: * 1. 序章的添加 * 2. 章节存在的书本的虚拟分章效果 */ private List loadChapters() throws IOException { List mChapterList = new ArrayList<>(); //获取文件流 RandomAccessFile bookStream = new RandomAccessFile(mBookFile, "r"); //寻找匹配文章标题的正则表达式,判断是否存在章节名 boolean hasChapter = checkChapterType(bookStream); //加载章节 byte[] buffer = new byte[BUFFER_SIZE]; //获取到的块起始点,在文件中的位置 long curOffset = 0; //block的个数 int blockPos = 0; //读取的长度 int length; int allLength = 0; //获取文件中的数据到buffer,直到没有数据为止 while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) { ++blockPos; //如果存在Chapter if (hasChapter) { //将数据转换成String String blockContent = new String(buffer, 0, length, mCharset); int lastN = blockContent.lastIndexOf("\n"); if (lastN != 0) { blockContent = blockContent.substring(0, lastN); length = blockContent.getBytes(mCharset).length; allLength = allLength + length; bookStream.seek(allLength); } //当前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(); if (mChapterList.size() == 0) { //如果当前没有章节,那么就是序章 //加入简介 book.getBookInfoBean().setIntroduce(chapterContent); //创建当前章节 BookChapterBean curChapter = new BookChapterBean(); curChapter.setDurChapterName(matcher.group()); curChapter.setStart((long) chapterContent.getBytes(mCharset).length); mChapterList.add(curChapter); } else { //否则就block分割之后,上一个章节的剩余内容 //获取上一章节 BookChapterBean lastChapter = mChapterList.get(mChapterList.size() - 1); //将当前段落添加上一章去 lastChapter.setEnd(lastChapter.getEnd() + chapterContent.getBytes(mCharset).length); //创建当前章节 BookChapterBean curChapter = new BookChapterBean(); curChapter.setDurChapterName(matcher.group()); curChapter.setStart(lastChapter.getEnd()); mChapterList.add(curChapter); } } else { //是否存在章节 if (mChapterList.size() != 0) { //获取章节内容 String chapterContent = blockContent.substring(seekPos, matcher.start()); seekPos += chapterContent.length(); //获取上一章节 BookChapterBean lastChapter = mChapterList.get(mChapterList.size() - 1); lastChapter.setEnd(lastChapter.getStart() + chapterContent.getBytes(mCharset).length); //创建当前章节 BookChapterBean curChapter = new BookChapterBean(); curChapter.setDurChapterName(matcher.group()); curChapter.setStart(lastChapter.getEnd()); mChapterList.add(curChapter); } else { //如果章节不存在则创建章节 BookChapterBean curChapter = new BookChapterBean(); curChapter.setDurChapterName(matcher.group()); curChapter.setStart(0L); curChapter.setEnd(0L); mChapterList.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] == BLANK) { end = i; break; } } BookChapterBean chapter = new BookChapterBean(); chapter.setDurChapterName("第" + blockPos + "章" + "(" + chapterPos + ")"); chapter.setStart(curOffset + chapterOffset + 1); chapter.setEnd(curOffset + end); mChapterList.add(chapter); //减去已经被分配的长度 strLength = strLength - (end - chapterOffset); //设置偏移的位置 chapterOffset = end; } else { BookChapterBean chapter = new BookChapterBean(); chapter.setDurChapterName("第" + blockPos + "章" + "(" + chapterPos + ")"); chapter.setStart(curOffset + chapterOffset + 1); chapter.setEnd(curOffset + length); mChapterList.add(chapter); strLength = 0; } } } //block的偏移点 curOffset += length; if (hasChapter) { //设置上一章的结尾 BookChapterBean lastChapter = mChapterList.get(mChapterList.size() - 1); lastChapter.setEnd(curOffset); } //当添加的block太多的时候,执行GC if (blockPos % 15 == 0) { System.gc(); System.runFinalization(); } } for (int i = 0; i < mChapterList.size(); i++) { BookChapterBean bean = mChapterList.get(i); bean.setDurChapterIndex(i); bean.setNoteUrl(book.getNoteUrl()); bean.setDurChapterUrl(MD5Utils.strToMd5By16(mBookFile.getAbsolutePath() + i + bean.getDurChapterName())); } IOUtils.close(bookStream); System.gc(); System.runFinalization(); return mChapterList; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/PageView.java ================================================ package com.kunfei.bookshelf.widget.page; import static com.kunfei.bookshelf.utils.ScreenUtils.getDisplayMetrics; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import com.kunfei.bookshelf.bean.BookShelfBean; import com.kunfei.bookshelf.help.FileHelp; import com.kunfei.bookshelf.help.ReadBookControl; import com.kunfei.bookshelf.utils.ContextExtensionsKt; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.view.activity.ReadBookActivity; import com.kunfei.bookshelf.widget.page.animation.CoverPageAnim; import com.kunfei.bookshelf.widget.page.animation.HorizonPageAnim; import com.kunfei.bookshelf.widget.page.animation.NonePageAnim; import com.kunfei.bookshelf.widget.page.animation.PageAnimation; import com.kunfei.bookshelf.widget.page.animation.ScrollPageAnim; import com.kunfei.bookshelf.widget.page.animation.SimulationPageAnim; import com.kunfei.bookshelf.widget.page.animation.SlidePageAnim; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * 绘制页面显示内容的类 */ public class PageView extends View implements PageAnimation.OnPageChangeListener { private ReadBookActivity activity; private int mViewWidth = 0; // 当前View的宽 private int mViewHeight = 0; // 当前View的高 private int statusBarHeight = 0; //状态栏高度 private boolean actionFromEdge = false; // 初始化参数 private ReadBookControl readBookControl = ReadBookControl.getInstance(); private boolean isPrepare; // 动画类 private PageAnimation mPageAnim; //点击监听 private TouchListener mTouchListener; //内容加载器 private PageLoader mPageLoader; //文字选择画笔 private Paint mTextSelectPaint = null; //文字选择画笔颜色 private int TextSelectColor = Color.parseColor("#77fadb08"); private Path mSelectTextPath = new Path(); //触摸到起始位置 private int mStartX = 0; private int mStartY = 0; // 是否发触了长按事件 private boolean isLongPress = false; //第一个选择的文字 private TxtChar firstSelectTxtChar = null; //最后选择的一个文字 private TxtChar lastSelectTxtChar = null; //选择模式 private SelectMode selectMode = SelectMode.Normal; //文本高度 private float textHeight = 0; // 唤醒菜单的区域 private RectF mCenterRect = null; //是否在移动 private boolean isMove = false; //长按的runnable private Runnable mLongPressRunnable; //长按时间 private static final int LONG_PRESS_TIMEOUT = 1000; //选择的列 private List mSelectLines = new ArrayList(); public PageView(Context context) { this(context, null); } public PageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { //初始化画笔 mTextSelectPaint = new Paint(); mTextSelectPaint.setAntiAlias(true); mTextSelectPaint.setTextSize(19); mTextSelectPaint.setColor(TextSelectColor); mLongPressRunnable = () -> { if (mPageLoader == null) return; performLongClick(); if (mStartX > 0 && mStartY > 0) {// 说明还没释放,是长按事件 isLongPress = true;//长按 TxtChar p = mPageLoader.detectPressTxtChar(mStartX, mStartY);//找到长按的点 firstSelectTxtChar = p;//设置开始位置字符 lastSelectTxtChar = p;//设置结束位置字符 selectMode = SelectMode.PressSelectText;//设置模式为长按选择 mTouchListener.onLongPress();//响应长按事件,供上层调用 } }; } @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldWidth, oldHeight); mViewWidth = width; mViewHeight = height; isPrepare = true; if (mPageLoader != null) { mPageLoader.prepareDisplay(width, height); } //设置中间区域范围 mCenterRect = new RectF(mViewWidth / 3f, mViewHeight / 3f, mViewWidth * 2f / 3, mViewHeight * 2f / 3); } //设置翻页的模式 void setPageMode(PageAnimation.Mode pageMode, int marginTop, int marginBottom) { //视图未初始化的时候,禁止调用 if (mViewWidth == 0 || mViewHeight == 0 || mPageLoader == null) return; if (!readBookControl.getHideStatusBar()) { marginTop = marginTop + statusBarHeight; } switch (pageMode) { case COVER: mPageAnim = new CoverPageAnim(mViewWidth, mViewHeight, this, this); break; case SLIDE: mPageAnim = new SlidePageAnim(mViewWidth, mViewHeight, this, this); break; case NONE: mPageAnim = new NonePageAnim(mViewWidth, mViewHeight, this, this); break; case SCROLL: mPageAnim = new ScrollPageAnim(mViewWidth, mViewHeight, 0, marginTop, marginBottom, this, this); break; default: mPageAnim = new SimulationPageAnim(mViewWidth, mViewHeight, this, this); } } public ReadBookActivity getActivity() { return activity; } public int getStatusBarHeight() { return statusBarHeight; } public Bitmap getBgBitmap(int pageOnCur) { if (mPageAnim == null) return null; return mPageAnim.getBgBitmap(pageOnCur); } public void autoPrevPage() { if (mPageAnim instanceof ScrollPageAnim) { ((ScrollPageAnim) mPageAnim).startAnim(PageAnimation.Direction.PREV); } else { startHorizonPageAnim(PageAnimation.Direction.PREV); } } public void autoNextPage() { if (mPageAnim instanceof ScrollPageAnim) { ((ScrollPageAnim) mPageAnim).startAnim(PageAnimation.Direction.NEXT); } else { startHorizonPageAnim(PageAnimation.Direction.NEXT); } } private synchronized void startHorizonPageAnim(PageAnimation.Direction direction) { if (mTouchListener == null) return; //结束动画 mPageAnim.abortAnim(); if (direction == PageAnimation.Direction.NEXT) { int x = mViewWidth; int y = mViewHeight; //初始化动画 mPageAnim.setStartPoint(x, y); //设置点击点 mPageAnim.setTouchPoint(x, y); //设置方向 boolean hasNext = hasNextPage(0); mPageAnim.setDirection(direction); if (!hasNext) { ((HorizonPageAnim) mPageAnim).setNoNext(true); return; } } else if (direction == PageAnimation.Direction.PREV) { int x = 0; int y = mViewHeight; //初始化动画 mPageAnim.setStartPoint(x, y); //设置点击点 mPageAnim.setTouchPoint(x, y); mPageAnim.setDirection(direction); //设置方向方向 boolean hashPrev = hasPrevPage(); if (!hashPrev) { ((HorizonPageAnim) mPageAnim).setNoNext(true); return; } } else { return; } ((HorizonPageAnim) mPageAnim).setNoNext(false); ((HorizonPageAnim) mPageAnim).setCancel(false); mPageAnim.startAnim(); } public void drawPage(int pageOnCur) { if (!isPrepare) return; if (mPageLoader != null) { mPageLoader.drawPage(getBgBitmap(pageOnCur), pageOnCur); } invalidate(); } /** * 绘制滚动背景 */ @Override public void drawBackground(Canvas canvas) { if (!isPrepare) return; if (mPageLoader != null) { mPageLoader.drawBackground(canvas); } } /** * 绘制滚动内容 */ @Override public void drawContent(Canvas canvas, float offset) { if (!isPrepare) return; if (mPageLoader != null) { mPageLoader.drawContent(canvas, offset); } } /** * 绘制横翻背景 */ public void drawBackground(int pageOnCur) { if (!isPrepare) return; if (mPageLoader != null) { mPageLoader.drawPage(getBgBitmap(pageOnCur), pageOnCur); } invalidate(); } /** * 绘制横翻内容 * * @param pageOnCur 相对当前页的位置 */ public void drawContent(int pageOnCur) { if (!isPrepare) return; if (mPageLoader != null) { mPageLoader.drawPage(getBgBitmap(pageOnCur), pageOnCur); } invalidate(); } @Override protected void onDraw(Canvas canvas) { if (mPageAnim instanceof ScrollPageAnim) super.onDraw(canvas); //绘制动画 if (mPageAnim != null) { mPageAnim.draw(canvas); } if (selectMode != SelectMode.Normal && !isRunning() && !isMove) { DrawSelectText(canvas); } } private void DrawSelectText(Canvas canvas) { if (selectMode == SelectMode.PressSelectText) { drawPressSelectText(canvas); } else if (selectMode == SelectMode.SelectMoveForward) { drawMoveSelectText(canvas); } else if (selectMode == SelectMode.SelectMoveBack) { drawMoveSelectText(canvas); } } private void drawPressSelectText(Canvas canvas) { if (lastSelectTxtChar != null) {// 找到了选择的字符 mSelectTextPath.reset(); mSelectTextPath.moveTo(firstSelectTxtChar.getTopLeftPosition().x, firstSelectTxtChar.getTopLeftPosition().y); mSelectTextPath.lineTo(firstSelectTxtChar.getTopRightPosition().x, firstSelectTxtChar.getTopRightPosition().y); mSelectTextPath.lineTo(firstSelectTxtChar.getBottomRightPosition().x, firstSelectTxtChar.getBottomRightPosition().y); mSelectTextPath.lineTo(firstSelectTxtChar.getBottomLeftPosition().x, firstSelectTxtChar.getBottomLeftPosition().y); canvas.drawPath(mSelectTextPath, mTextSelectPaint); } } public String getSelectStr() { if (mSelectLines.size() == 0) { return String.valueOf(firstSelectTxtChar.getChardata()); } StringBuilder sb = new StringBuilder(); for (TxtLine l : mSelectLines) { //Log.e("selectline", l.getLineData() + ""); sb.append(l.getLineData()); } return sb.toString(); } private void drawMoveSelectText(Canvas canvas) { if (firstSelectTxtChar == null || lastSelectTxtChar == null) return; getSelectData(); drawSelectLines(canvas); } List mLinseData = null; private void getSelectData() { TxtPage txtPage = mPageLoader.curChapter().txtChapter.getPage(mPageLoader.getCurPagePos()); if (txtPage != null) { mLinseData = txtPage.getTxtLists(); Boolean Started = false; Boolean Ended = false; mSelectLines.clear(); // 找到选择的字符数据,转化为选择的行,然后将行选择背景画出来 for (TxtLine l : mLinseData) { TxtLine selectline = new TxtLine(); selectline.setCharsData(new ArrayList<>()); for (TxtChar c : l.getCharsData()) { if (!Started) { if (c.getIndex() == firstSelectTxtChar.getIndex()) { Started = true; selectline.getCharsData().add(c); if (c.getIndex() == lastSelectTxtChar.getIndex()) { Ended = true; break; } } } else { if (c.getIndex() == lastSelectTxtChar.getIndex()) { Ended = true; if (!selectline.getCharsData().contains(c)) { selectline.getCharsData().add(c); } break; } else { selectline.getCharsData().add(c); } } } mSelectLines.add(selectline); if (Started && Ended) { break; } } } } public SelectMode getSelectMode() { return selectMode; } public void setSelectMode(SelectMode mCurrentMode) { this.selectMode = mCurrentMode; } private void drawSelectLines(Canvas canvas) { drawOaleSeletLinesBg(canvas); } public void clearSelect() { firstSelectTxtChar = null; lastSelectTxtChar = null; selectMode = SelectMode.Normal; mSelectTextPath.reset(); invalidate(); } //根据当前坐标返回文字 public TxtChar getCurrentTxtChar(float x, float y) { return mPageLoader.detectPressTxtChar(x, y); } private void drawOaleSeletLinesBg(Canvas canvas) {// 绘制椭圆型的选中背景 for (TxtLine l : mSelectLines) { if (l.getCharsData() != null && l.getCharsData().size() > 0) { TxtChar fistchar = l.getCharsData().get(0); TxtChar lastchar = l.getCharsData().get(l.getCharsData().size() - 1); float fw = fistchar.getCharWidth(); float lw = lastchar.getCharWidth(); RectF rect = new RectF(fistchar.getTopLeftPosition().x, fistchar.getTopLeftPosition().y, lastchar.getTopRightPosition().x, lastchar.getBottomRightPosition().y); canvas.drawRoundRect(rect, fw / 4, textHeight /4, mTextSelectPaint); } } } public TxtChar getFirstSelectTxtChar() { return firstSelectTxtChar; } public void setFirstSelectTxtChar(TxtChar firstSelectTxtChar) { this.firstSelectTxtChar = firstSelectTxtChar; } public TxtChar getLastSelectTxtChar() { return lastSelectTxtChar; } public void setLastSelectTxtChar(TxtChar lastSelectTxtChar) { this.lastSelectTxtChar = lastSelectTxtChar; } @Override public void computeScroll() { //进行滑动 if (mPageAnim != null) { mPageAnim.scrollAnim(); } super.computeScroll(); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); if (mPageAnim == null) return true; if (mPageLoader == null) return true; Paint.FontMetrics fontMetrics = mPageLoader.mTextPaint.getFontMetrics(); textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent); if (actionFromEdge) { if (event.getAction() == MotionEvent.ACTION_UP) actionFromEdge = false; return true; } int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPageAnim.initTouch(x, y); if (event.getEdgeFlags() != 0 || event.getRawY() < ScreenUtils.dpToPx(5) || event.getRawY() > getDisplayMetrics().heightPixels - ScreenUtils.dpToPx(5)) { actionFromEdge = true; return true; } mStartX = x; mStartY = y; isMove = false; // if (readBookControl.isCanSelectText() && mPageLoader.getPageStatus() == TxtChapter.Status.FINISH) { postDelayed(mLongPressRunnable, LONG_PRESS_TIMEOUT); } // isLongPress = false; mTouchListener.onTouch(); mPageAnim.onTouchEvent(event); selectMode = SelectMode.Normal; mTouchListener.onTouchClearCursor(); break; case MotionEvent.ACTION_MOVE: mPageAnim.initTouch(x, y); // 判断是否大于最小滑动值。 int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); if (!isMove) { isMove = Math.abs(mStartX - event.getX()) > slop || Math.abs(mStartY - event.getY()) > slop; } // 如果滑动了,且不是长按,则进行翻页。 if (isMove) { if (readBookControl.isCanSelectText()) { removeCallbacks(mLongPressRunnable); } mPageAnim.onTouchEvent(event); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mPageAnim.initTouch(x, y); mPageAnim.setTouchInitFalse(); if (!isMove) { if (readBookControl.isCanSelectText()) { removeCallbacks(mLongPressRunnable); } //是否点击了中间 if (mCenterRect.contains(x, y)) { if (firstSelectTxtChar == null) { if (mTouchListener != null) { mTouchListener.center(); } } else { if (mSelectTextPath != null) {//长安选择删除选中状态 if (!isLongPress) { firstSelectTxtChar = null; mSelectTextPath.reset(); invalidate(); } } //清除移动选择状态 } return true; } if (!readBookControl.getCanClickTurn()) { return true; } if (mPageAnim instanceof ScrollPageAnim && readBookControl.disableScrollClickTurn()) { return true; } } if (firstSelectTxtChar == null || isMove) {//长安选择删除选中状态 mPageAnim.onTouchEvent(event); } else { if (!isLongPress) { //释放了 if (LONG_PRESS_TIMEOUT != 0) { removeCallbacks(mLongPressRunnable); } firstSelectTxtChar = null; mSelectTextPath.reset(); invalidate(); } } break; } return true; } /** * 判断是否存在上一页 */ private boolean hasPrevPage() { if (mPageLoader.hasPrev()) { return true; } else { showSnackBar("没有上一页"); return false; } } /** * 判断是否下一页存在 */ private boolean hasNextPage(int pageOnCur) { if (mPageLoader.hasNext(pageOnCur)) { return true; } else { showSnackBar("没有下一页"); return false; } } public boolean isRunning() { return mPageAnim != null && mPageAnim.isRunning(); } public boolean isPrepare() { return isPrepare; } public void setTouchListener(TouchListener mTouchListener) { this.mTouchListener = mTouchListener; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mPageAnim != null) { mPageAnim.abortAnim(); mPageAnim.clear(); } mPageLoader = null; mPageAnim = null; } @Override public void resetScroll() { mPageLoader.resetPageOffset(); } @Override public boolean hasPrev() { return PageView.this.hasPrevPage(); } @Override public boolean hasNext(int pageOnCur) { return PageView.this.hasNextPage(pageOnCur); } @Override public void changePage(PageAnimation.Direction direction) { mPageLoader.pagingEnd(direction); } /** * 获取 PageLoader */ public PageLoader getPageLoader(ReadBookActivity activity, BookShelfBean bookShelfBean, PageLoader.Callback callback) { this.activity = activity; this.statusBarHeight = ContextExtensionsKt.getStatusBarHeight(activity); // 判是否已经存在 if (mPageLoader != null) { return mPageLoader; } // 根据书籍类型,获取具体的加载器 if (!Objects.equals(bookShelfBean.getTag(), BookShelfBean.LOCAL_TAG)) { mPageLoader = new PageLoaderNet(this, bookShelfBean, callback); } else { String fileSuffix = FileHelp.getFileSuffix(bookShelfBean.getNoteUrl()); if (fileSuffix.equalsIgnoreCase(FileHelp.SUFFIX_EPUB)) { mPageLoader = new PageLoaderEpub(this, bookShelfBean, callback); } else { mPageLoader = new PageLoaderText(this, bookShelfBean, callback); } } // 判断是否 PageView 已经初始化完成 if (mViewWidth != 0 || mViewHeight != 0) { // 初始化 PageLoader 的屏幕大小 mPageLoader.prepareDisplay(mViewWidth, mViewHeight); } return mPageLoader; } public void autoChangeSource() { mPageLoader.setStatus(TxtChapter.Status.CHANGE_SOURCE); activity.autoChangeSource(); } public void showSnackBar(String msg) { activity.showSnackBar(this, msg); } public enum SelectMode { Normal, PressSelectText, SelectMoveForward, SelectMoveBack } public interface TouchListener { void onTouch(); void onTouchClearCursor(); void onLongPress(); void center(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/TxtChapter.kt ================================================ package com.kunfei.bookshelf.widget.page import java.util.* import kotlin.math.max import kotlin.math.min /** * 章节 */ class TxtChapter internal constructor(val position: Int) { val txtPageList = ArrayList() val txtPageLengthList = ArrayList() val paragraphLengthList = ArrayList() var status = Status.LOADING var msg: String? = null val pageSize: Int get() = txtPageList.size fun addPage(txtPage: TxtPage) { txtPageList.add(txtPage) } fun getPage(page: Int): TxtPage? { return if (txtPageList.isNotEmpty()) { txtPageList[max(0, min(page, txtPageList.size - 1))] } else null } fun getPageLength(position: Int): Int { return if (position >= 0 && position < txtPageLengthList.size) { txtPageLengthList[position] } else -1 } fun addTxtPageLength(length: Int) { txtPageLengthList.add(length) } fun addParagraphLength(length: Int) { paragraphLengthList.add(length) } fun getParagraphIndex(length: Int): Int { for (i in paragraphLengthList.indices) { if ((i == 0 || paragraphLengthList[i - 1] < length) && length <= paragraphLengthList[i]) { return i } } return -1 } enum class Status { LOADING, FINISH, ERROR, EMPTY, CATEGORY_EMPTY, CHANGE_SOURCE } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/TxtChar.kt ================================================ package com.kunfei.bookshelf.widget.page import android.graphics.Point class TxtChar { var chardata: Char = ' '//字符数据 var selected: Boolean? = false//当前字符是否被选中 //记录文字的左上右上左下右下四个点坐标 var topLeftPosition: Point? = null//左上 var topRightPosition: Point? = null//右上 var bottomLeftPosition: Point? = null//左下 var bottomRightPosition: Point? = null//右下 var charWidth = 0f//字符宽度 var Index = 0//当前字符位置 override fun toString(): String { return ("ShowChar [chardata=" + chardata + ", Selected=" + selected + ", TopLeftPosition=" + topLeftPosition + ", TopRightPosition=" + topRightPosition + ", BottomLeftPosition=" + bottomLeftPosition + ", BottomRightPosition=" + bottomRightPosition + ", charWidth=" + charWidth + ", Index=" + Index + "]"); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/TxtLine.kt ================================================ package com.kunfei.bookshelf.widget.page class TxtLine { var charsData: List? = null fun getLineData(): String { var linedata = "" if (charsData == null) return linedata charsData?.let { if (it.isEmpty()) return linedata for (c in it) { linedata += c.chardata } } return linedata } override fun toString(): String { return "ShowLine [Linedata=" + getLineData() + "]" } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/TxtPage.kt ================================================ package com.kunfei.bookshelf.widget.page import java.util.* /** * 页面 */ class TxtPage(val position: Int) { var title: String? = null var titleLines: Int = 0 //当前 lines 中为 title 的行数。 private val lines = ArrayList() //存放每个字的位置 var txtLists: List? = null val content: String get() { val s = StringBuilder() for (i in lines.indices) { s.append(lines[i]) } return s.toString() } fun addLine(line: String) { lines.add(line) } fun addLines(lines: List) { this.lines.addAll(lines) } fun getLine(i: Int): String { return lines[i] } fun getLines(): List { return lines } fun size(): Int { return lines.size } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/animation/CoverPageAnim.java ================================================ package com.kunfei.bookshelf.widget.page.animation; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.view.View; /** * 覆盖翻页 */ public class CoverPageAnim extends HorizonPageAnim { private Rect mSrcRect, mDestRect; private GradientDrawable mBackShadowDrawableLR; public CoverPageAnim(int w, int h, View view, OnPageChangeListener listener) { super(w, h, view, listener); mSrcRect = new Rect(0, 0, mViewWidth, mViewHeight); mDestRect = new Rect(0, 0, mViewWidth, mViewHeight); int[] mBackShadowColors = new int[]{0x66000000, 0x00000000}; mBackShadowDrawableLR = new GradientDrawable( GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors); mBackShadowDrawableLR.setGradientType(GradientDrawable.LINEAR_GRADIENT); } @Override public void drawMove(Canvas canvas) { int dis; if (mDirection == Direction.NEXT) { dis = (int) (mViewWidth - mStartX + mTouchX); if (dis > mViewWidth) { dis = mViewWidth; } //计算bitmap截取的区域 mSrcRect.left = mViewWidth - dis; //计算bitmap在canvas显示的区域 mDestRect.right = dis; canvas.drawBitmap(bitmapList.get(2), 0, 0, null); canvas.drawBitmap(bitmapList.get(1), mSrcRect, mDestRect, null); addShadow(dis, canvas); } else { dis = (int) (mTouchX - mStartX); if (dis > mViewWidth) { dis = mViewWidth; } mSrcRect.left = mViewWidth - dis; mDestRect.right = dis; canvas.drawBitmap(bitmapList.get(1), 0, 0, null); canvas.drawBitmap(bitmapList.get(0), mSrcRect, mDestRect, null); addShadow(dis, canvas); } } //添加阴影 private void addShadow(int left, Canvas canvas) { mBackShadowDrawableLR.setBounds(left, 0, left + 30, mScreenHeight); mBackShadowDrawableLR.draw(canvas); } @Override public void startAnim() { int dx; if (mDirection == Direction.NEXT) { if (isCancel) { int dis = (int) ((mViewWidth - mStartX) + mTouchX); if (dis > mViewWidth) { dis = mViewWidth; } dx = mViewWidth - dis; } else { dx = (int) -(mTouchX + (mViewWidth - mStartX)); } } else { if (isCancel) { dx = (int) -(mTouchX - mStartX); } else { dx = (int) (mViewWidth - (mTouchX - mStartX)); } } //滑动速度保持一致 int duration = (animationSpeed * Math.abs(dx)) / mViewWidth; mScroller.startScroll((int) mTouchX, 0, dx, 0, duration); super.startAnim(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/animation/HorizonPageAnim.java ================================================ package com.kunfei.bookshelf.widget.page.animation; import android.graphics.Bitmap; import android.graphics.Canvas; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 横向动画的模板 */ public abstract class HorizonPageAnim extends PageAnimation { private static final String TAG = "HorizonPageAnim"; List bitmapList = new ArrayList<>(); HorizonPageAnim(int w, int h, View view, OnPageChangeListener listener) { super(w, h, view, listener); //创建图片 for (int i = 0; i < 3; i++) { bitmapList.add(Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.ARGB_8888)); } } /** * 转换页面,在显示下一章的时候,必须首先调用此方法 */ @Override public boolean changePage() { if (isCancel) return false; switch (mDirection) { case NEXT: Collections.swap(bitmapList, 0, 1); Collections.swap(bitmapList, 1, 2); break; case PREV: Collections.swap(bitmapList, 1, 2); Collections.swap(bitmapList, 0, 1); break; default: return false; } return true; } public abstract void drawMove(Canvas canvas); @Override public void onTouchEvent(MotionEvent event) { abortAnim(); final int slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop(); //获取点击位置 int x = (int) event.getX(); int y = (int) event.getY(); //设置触摸点 setTouchPoint(x, y); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: //判断是否移动了 if (!isMove) { isMove = Math.abs(mStartX - x) > slop || Math.abs(mStartY - y) > slop; } if (isMove) { //判断是否是准备移动的状态(将要移动但是还没有移动) if (mMoveX == 0 && mMoveY == 0) { //判断翻得是上一页还是下一页 if (x - mStartX > 0) { //上一页的参数配置 isNext = false; boolean hasPrev = mListener.hasPrev(); setDirection(Direction.PREV); //如果上一页不存在 if (!hasPrev) { noNext = true; return; } } else { //进行下一页的配置 isNext = true; //判断是否下一页存在 boolean hasNext = mListener.hasNext(0); //如果存在设置动画方向 setDirection(Direction.NEXT); //如果不存在表示没有下一页了 if (!hasNext) { noNext = true; return; } } } else { //判断是否取消翻页 isCancel = isNext ? x - mMoveX > 0 : x - mMoveX < 0; } mMoveX = x; mMoveY = y; isRunning = true; mView.invalidate(); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: isRunning = false; if (!isMove) { if (!readBookControl.getCanClickTurn()) { return; } isNext = x > mScreenWidth / 2 || readBookControl.getClickAllNext(); if (isNext) { //判断是否下一页存在 boolean hasNext = mListener.hasNext(0); //设置动画方向 if (!hasNext) { return; } setDirection(Direction.NEXT); } else { boolean hasPrev = mListener.hasPrev(); if (!hasPrev) { return; } setDirection(Direction.PREV); } } else { isCancel = Math.abs(mLastX - mStartX) < slop * 3 || isCancel; } // 开启翻页效果 if (!noNext) { startAnim(); } mView.invalidate(); break; } } @Override public void draw(Canvas canvas) { if (isRunning && !noNext) { drawMove(canvas); } else { canvas.drawBitmap(getBgBitmap(0), 0, 0, null); isCancel = true; } } @Override public void abortAnim() { if (!mScroller.isFinished()) { mScroller.abortAnimation(); if (changePage()) { mListener.changePage(mDirection); setDirection(PageAnimation.Direction.NONE); } movingFinish(); setTouchPoint(mScroller.getFinalX(), mScroller.getFinalY()); mView.invalidate(); } } @Override public Bitmap getBgBitmap(int pageOnCur) { if (pageOnCur < 0) { return bitmapList.get(0); } else if (pageOnCur > 0) { return bitmapList.get(2); } return bitmapList.get(1); } public void setCancel(boolean cancel) { isCancel = cancel; } public void setNoNext(boolean noNext) { this.noNext = noNext; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/animation/NonePageAnim.java ================================================ package com.kunfei.bookshelf.widget.page.animation; import android.graphics.Canvas; import android.view.View; /** * 无动画翻页 */ public class NonePageAnim extends HorizonPageAnim { public NonePageAnim(int w, int h, View view, OnPageChangeListener listener) { super(w, h, view, listener); } @Override public void drawMove(Canvas canvas) { canvas.drawBitmap(bitmapList.get(1), 0, 0, null); } @Override public void startAnim() { super.startAnim(); isRunning = false; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/animation/PageAnimation.java ================================================ package com.kunfei.bookshelf.widget.page.animation; import android.graphics.Bitmap; import android.graphics.Canvas; import android.view.MotionEvent; import android.view.View; import android.view.animation.LinearInterpolator; import android.widget.Scroller; import androidx.annotation.NonNull; import com.kunfei.bookshelf.MApplication; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.help.ReadBookControl; /** * 翻页动画抽象类 */ public abstract class PageAnimation { //动画速度 static final int animationSpeed = 300; //正在使用的View protected View mView; protected ReadBookControl readBookControl = ReadBookControl.getInstance(); //滑动装置 Scroller mScroller; //监听器 protected OnPageChangeListener mListener; //移动方向 Direction mDirection = Direction.NONE; //屏幕的尺寸 int mScreenWidth; int mScreenHeight; int mMarginTop; //视图的尺寸 int mViewWidth; int mViewHeight; //起始点 float mStartX; float mStartY; //触碰点 float mTouchX; float mTouchY; //上一个触碰点 float mLastX; float mLastY; private boolean isMoving = false; boolean isRunning = false; private boolean touchInit = false; //是否取消翻页 boolean isCancel = false; //可以使用 mLast代替 int mMoveX = 0; int mMoveY = 0; //是否移动了 boolean isMove = false; //是否翻阅下一页。true表示翻到下一页,false表示上一页。 boolean isNext = false; //是否没下一页或者上一页 boolean noNext = false; PageAnimation(int w, int h, View view, OnPageChangeListener listener) { this(w, h, 0, 0, 0, view, listener); } PageAnimation(int w, int h, int marginWidth, int marginTop, int marginBottom, View view, OnPageChangeListener listener) { mScreenWidth = w; mScreenHeight = h; //屏幕的间距 mMarginTop = marginTop; mViewWidth = mScreenWidth - marginWidth * 2; mViewHeight = mScreenHeight - mMarginTop - marginBottom; mView = view; mListener = listener; mScroller = new Scroller(mView.getContext(), new LinearInterpolator()); } public Scroller getScroller() { return mScroller; } public void setStartPoint(float x, float y) { mStartX = x; mStartY = y; mLastX = mStartX; mLastY = mStartY; } public void setTouchPoint(float x, float y) { mLastX = mTouchX; mLastY = mTouchY; mTouchX = x; mTouchY = y; } public void setTouchInitFalse() { touchInit = false; } public void initTouch(int x, int y) { if (!touchInit) { //移动的点击位置 mMoveX = 0; mMoveY = 0; //是否移动 isMove = false; //是否存在下一章 noNext = false; //是下一章还是前一章 isNext = false; //是否正在执行动画 isRunning = false; //取消 isCancel = false; //设置起始位置的触摸点 setStartPoint(x, y); touchInit = true; } } public boolean isRunning() { return isRunning; } void movingFinish() { isMoving = false; isRunning = false; } /** * 开启翻页动画 */ public void startAnim() { isRunning = true; isMoving = true; mView.invalidate(); } public void setDirection(Direction direction) { mDirection = direction; } public void clear() { mView = null; } /** * 点击事件的处理 */ public abstract void onTouchEvent(MotionEvent event); /** * 绘制图形 */ public abstract void draw(Canvas canvas); /** * 滚动动画 * 必须放在computeScroll()方法中执行 */ public void scrollAnim() { if (mScroller.computeScrollOffset()) { int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); setTouchPoint(x, y); mView.postInvalidate(); } else if (isMoving) { if (changePage()) { mListener.changePage(mDirection); setDirection(PageAnimation.Direction.NONE); } movingFinish(); } } /** * 取消动画 */ public abstract void abortAnim(); public abstract boolean changePage(); /** * 获取背景板 * pageOnCur: 位于当前页的位置, 小于0上一页, 0 当前页, 大于0下一页 */ public abstract Bitmap getBgBitmap(int pageOnCur); /** * 翻页模式 */ public enum Mode { COVER(MApplication.getAppResources().getString(R.string.page_mode_COVER)), SIMULATION(MApplication.getAppResources().getString(R.string.page_mode_SIMULATION)), SLIDE(MApplication.getAppResources().getString(R.string.page_mode_SLIDE)), SCROLL(MApplication.getAppResources().getString(R.string.page_mode_SCROLL)), NONE(MApplication.getAppResources().getString(R.string.page_mode_NONE)); private String name; Mode(String name) { this.name = name; } public static Mode getPageMode(int pageMode) { switch (pageMode) { case 0: return COVER; case 1: return SIMULATION; case 2: return SLIDE; case 3: return SCROLL; case 4: return NONE; default: return COVER; } } public static String[] getAllPageMode() { return new String[]{COVER.name, SIMULATION.name, SLIDE.name, SCROLL.name, NONE.name}; } @NonNull @Override public String toString() { return this.name; } } /** * 翻页方向 */ public enum Direction { NONE(true), NEXT(true), PREV(true); public final boolean isHorizontal; Direction(boolean isHorizontal) { this.isHorizontal = isHorizontal; } } public interface OnPageChangeListener { void resetScroll(); boolean hasPrev(); boolean hasNext(int pageOnCur); void drawContent(Canvas canvas, float offset); void drawBackground(Canvas canvas); void changePage(Direction direction); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/animation/ScrollPageAnim.java ================================================ package com.kunfei.bookshelf.widget.page.animation; import android.graphics.Bitmap; import android.graphics.Canvas; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; public class ScrollPageAnim extends PageAnimation { private static final String TAG = "ScrollAnimation"; // 滑动追踪的时间 private static final int VELOCITY_DURATION = 1000; private VelocityTracker mVelocity; // 整个Bitmap的背景显示 private Bitmap mBgBitmap; public ScrollPageAnim(int w, int h, int marginWidth, int marginTop, int marginBottom, View view, OnPageChangeListener listener) { super(w, h, marginWidth, marginTop, marginBottom, view, listener); mListener.resetScroll(); mBgBitmap = Bitmap.createBitmap(mScreenWidth, mScreenHeight, Bitmap.Config.ARGB_8888); } @Override public void onTouchEvent(MotionEvent event) { final int slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop(); int x = (int) event.getX(); int y = (int) event.getY(); // 初始化速度追踪器 if (mVelocity == null) { mVelocity = VelocityTracker.obtain(); } mVelocity.addMovement(event); // 设置触碰点 setTouchPoint(x, y); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isMove = false; isRunning = false; // 设置起始点 setStartPoint(x, y); // 停止动画 abortAnim(); break; case MotionEvent.ACTION_MOVE: if (!isMove) { isMove = Math.abs(mStartX - x) > slop || Math.abs(mStartY - y) > slop; } mVelocity.computeCurrentVelocity(VELOCITY_DURATION); isRunning = true; // 进行刷新 mView.postInvalidate(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: isRunning = false; if (!isMove) { if (!readBookControl.getCanClickTurn() || readBookControl.disableScrollClickTurn()) { return; } //是否翻阅下一页。true表示翻到下一页,false表示上一页。 boolean isNext = x > mScreenWidth / 2 || readBookControl.getClickAllNext(); if (isNext) { startAnim(Direction.NEXT); } else { startAnim(Direction.PREV); } } else { // 开启动画 startAnim(); } // 删除检测器 if (mVelocity != null) { mVelocity.recycle(); mVelocity = null; } break; } } @Override public void draw(Canvas canvas) { //进行布局 float offset = mLastY - mTouchY; //绘制背景 mListener.drawBackground(canvas); //绘制内容 canvas.save(); //移动位置 canvas.translate(0, mMarginTop); //裁剪显示区域 canvas.clipRect(0, 0, mViewWidth, mViewHeight); mListener.drawContent(canvas, offset); canvas.restore(); } @Override public synchronized void startAnim() { super.startAnim(); //惯性滚动 mScroller.fling(0, (int) mTouchY, 0, (int) mVelocity.getYVelocity(), 0, 0, -10 * mViewHeight, 10 * mViewHeight); } /** * 翻页动画 */ public void startAnim(Direction direction) { setStartPoint(0, 0); setTouchPoint(0, 0); switch (direction) { case NEXT: super.startAnim(); mScroller.startScroll(0, 0, 0, -mViewHeight + 300, animationSpeed); break; case PREV: super.startAnim(); mScroller.startScroll(0, 0, 0, mViewHeight - 300, animationSpeed); break; } } @Override public void abortAnim() { if (!mScroller.isFinished()) { mScroller.abortAnimation(); isRunning = false; } } @Override public boolean changePage() { return false; } @Override public Bitmap getBgBitmap(int pageOnCur) { return mBgBitmap; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/animation/SimulationPageAnim.java ================================================ package com.kunfei.bookshelf.widget.page.animation; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.Region; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.view.View; /** * 仿真翻页 */ public class SimulationPageAnim extends HorizonPageAnim { private static final String TAG = "SimulationPageAnim"; private int mCornerX = 1; // 拖拽点对应的页脚 private int mCornerY = 1; private Path mPath0; private Path mPath1; private PointF mBezierStart1 = new PointF(); // 贝塞尔曲线起始点 private PointF mBezierControl1 = new PointF(); // 贝塞尔曲线控制点 private PointF mBezierVertex1 = new PointF(); // 贝塞尔曲线顶点 private PointF mBezierEnd1 = new PointF(); // 贝塞尔曲线结束点 private PointF mBezierStart2 = new PointF(); // 另一条贝塞尔曲线 private PointF mBezierControl2 = new PointF(); private PointF mBezierVertex2 = new PointF(); private PointF mBezierEnd2 = new PointF(); private float mMiddleX; private float mMiddleY; private float mDegrees; private float mTouchToCornerDis; private ColorMatrixColorFilter mColorMatrixFilter; private Matrix mMatrix; private float[] mMatrixArray = {0, 0, 0, 0, 0, 0, 0, 0, 1.0f}; private boolean mIsRT_LB; // 是否属于右上左下 private float mMaxLength; private int[] mBackShadowColors;// 背面颜色组 private int[] mFrontShadowColors;// 前面颜色组 private GradientDrawable mBackShadowDrawableLR; // 有阴影的GradientDrawable private GradientDrawable mBackShadowDrawableRL; private GradientDrawable mFolderShadowDrawableLR; private GradientDrawable mFolderShadowDrawableRL; private GradientDrawable mFrontShadowDrawableHBT; private GradientDrawable mFrontShadowDrawableHTB; private GradientDrawable mFrontShadowDrawableVLR; private GradientDrawable mFrontShadowDrawableVRL; private Paint mPaint; public SimulationPageAnim(int w, int h, View view, OnPageChangeListener listener) { super(w, h, view, listener); mPath0 = new Path(); mPath1 = new Path(); mMaxLength = (float) Math.hypot(mScreenWidth, mScreenHeight); mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); createDrawable(); ColorMatrix cm = new ColorMatrix();//设置颜色数组 float[] array = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; cm.set(array); mColorMatrixFilter = new ColorMatrixColorFilter(cm); mMatrix = new Matrix(); mTouchX = 0.01f; // 不让x,y为0,否则在点计算时会有问题 mTouchY = 0.01f; } @Override public void setStartPoint(float x, float y) { super.setStartPoint(x, y); calcCornerXY(x, y); } @Override public void setTouchPoint(float x, float y) { super.setTouchPoint(x, y); //触摸y中间位置吧y变成屏幕高度 if ((mStartY > mScreenHeight / 3.0 && mStartY < mScreenHeight * 2 / 3.0) || mDirection.equals(Direction.PREV)) { mTouchY = mScreenHeight; } if (mStartY > mScreenHeight / 3.0 && mStartY < mScreenHeight / 2.0 && mDirection.equals(Direction.NEXT)) { mTouchY = 1; } } @Override public void setDirection(Direction direction) { super.setDirection(direction); switch (direction) { case PREV: //上一页滑动不出现对角 if (mStartX > mScreenWidth / 2.0) { calcCornerXY(mStartX, mScreenHeight); } else { calcCornerXY(mScreenWidth - mStartX, mScreenHeight); } break; case NEXT: if (mScreenWidth / 2.0 > mStartX) { calcCornerXY(mScreenWidth - mStartX, mStartY); } break; } } @Override public void startAnim() { int dx, dy; // dx 水平方向滑动的距离,负值会使滚动向左滚动 // dy 垂直方向滑动的距离,负值会使滚动向上滚动 if (isCancel) { if (mCornerX > 0 && mDirection.equals(Direction.NEXT)) { dx = (int) (mScreenWidth - mTouchX); } else { dx = -(int) mTouchX; } if (!mDirection.equals(Direction.NEXT)) { dx = (int) -(mScreenWidth + mTouchX); } if (mCornerY > 0) { dy = (int) (mScreenHeight - mTouchY); } else { dy = -(int) mTouchY; // 防止mTouchY最终变为0 } } else { if (mCornerX > 0 && mDirection.equals(Direction.NEXT)) { dx = -(int) (mScreenWidth + mTouchX); } else { dx = (int) (mScreenWidth - mTouchX + mScreenWidth); } if (mCornerY > 0) { dy = (int) (mScreenHeight - mTouchY); } else { dy = (int) (1 - mTouchY); // 防止mTouchY最终变为0 } } mScroller.startScroll((int) mTouchX, (int) mTouchY, dx, dy, animationSpeed); super.startAnim(); } @Override public void drawMove(Canvas canvas) { if (mDirection == Direction.NEXT) { calcPoints(); drawCurrentPageArea(canvas, bitmapList.get(1));//绘制翻页时的正面页 drawNextPageAreaAndShadow(canvas, bitmapList.get(2)); drawCurrentPageShadow(canvas); drawCurrentBackArea(canvas, bitmapList.get(1)); } else { calcPoints(); drawCurrentPageArea(canvas, bitmapList.get(0)); drawNextPageAreaAndShadow(canvas, bitmapList.get(1)); drawCurrentPageShadow(canvas); drawCurrentBackArea(canvas, bitmapList.get(0)); } } /** * 创建阴影的GradientDrawable */ private void createDrawable() { int[] color = {0x333333, 0xb0333333}; mFolderShadowDrawableRL = new GradientDrawable( GradientDrawable.Orientation.RIGHT_LEFT, color); mFolderShadowDrawableRL .setGradientType(GradientDrawable.LINEAR_GRADIENT); mFolderShadowDrawableLR = new GradientDrawable( GradientDrawable.Orientation.LEFT_RIGHT, color); mFolderShadowDrawableLR .setGradientType(GradientDrawable.LINEAR_GRADIENT); mBackShadowColors = new int[]{0xff111111, 0x111111}; mBackShadowDrawableRL = new GradientDrawable( GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors); mBackShadowDrawableRL.setGradientType(GradientDrawable.LINEAR_GRADIENT); mBackShadowDrawableLR = new GradientDrawable( GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors); mBackShadowDrawableLR.setGradientType(GradientDrawable.LINEAR_GRADIENT); mFrontShadowColors = new int[]{0x80111111, 0x111111}; mFrontShadowDrawableVLR = new GradientDrawable( GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors); mFrontShadowDrawableVLR .setGradientType(GradientDrawable.LINEAR_GRADIENT); mFrontShadowDrawableVRL = new GradientDrawable( GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors); mFrontShadowDrawableVRL .setGradientType(GradientDrawable.LINEAR_GRADIENT); mFrontShadowDrawableHTB = new GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors); mFrontShadowDrawableHTB .setGradientType(GradientDrawable.LINEAR_GRADIENT); mFrontShadowDrawableHBT = new GradientDrawable( GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors); mFrontShadowDrawableHBT .setGradientType(GradientDrawable.LINEAR_GRADIENT); } /** * 绘制翻起页背面 */ private void drawCurrentBackArea(Canvas canvas, Bitmap bitmap) { int i = (int) (mBezierStart1.x + mBezierControl1.x) / 2; float f1 = Math.abs(i - mBezierControl1.x); int i1 = (int) (mBezierStart2.y + mBezierControl2.y) / 2; float f2 = Math.abs(i1 - mBezierControl2.y); float f3 = Math.min(f1, f2); mPath1.reset(); mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y); mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y); mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y); mPath1.lineTo(mTouchX, mTouchY); mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y); mPath1.close(); GradientDrawable mFolderShadowDrawable; int left; int right; if (mIsRT_LB) { left = (int) (mBezierStart1.x - 1); right = (int) (mBezierStart1.x + f3 + 1); mFolderShadowDrawable = mFolderShadowDrawableLR; } else { left = (int) (mBezierStart1.x - f3 - 1); right = (int) (mBezierStart1.x + 1); mFolderShadowDrawable = mFolderShadowDrawableRL; } canvas.save(); try { canvas.clipPath(mPath0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { canvas.clipPath(mPath1); } else { canvas.clipPath(mPath1, Region.Op.INTERSECT); } } catch (Exception ignored) { } mPaint.setColorFilter(mColorMatrixFilter); float dis = (float) Math.hypot(mCornerX - mBezierControl1.x, mBezierControl2.y - mCornerY); float f8 = (mCornerX - mBezierControl1.x) / dis; float f9 = (mBezierControl2.y - mCornerY) / dis; mMatrixArray[0] = 1 - 2 * f9 * f9; mMatrixArray[1] = 2 * f8 * f9; mMatrixArray[3] = mMatrixArray[1]; mMatrixArray[4] = 1 - 2 * f8 * f8; mMatrix.reset(); mMatrix.setValues(mMatrixArray); mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y); mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y); canvas.drawBitmap(bitmap, mMatrix, mPaint); mPaint.setColorFilter(null); canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y); mFolderShadowDrawable.setBounds( left, (int) mBezierStart1.y, right, (int) (mBezierStart1.y + mMaxLength) ); mFolderShadowDrawable.draw(canvas); canvas.restore(); } /** * 绘制翻起页的阴影 */ private void drawCurrentPageShadow(Canvas canvas) { double degree; if (mIsRT_LB) { degree = Math.PI / 4 - Math.atan2(mBezierControl1.y - mTouchY, mTouchX - mBezierControl1.x); } else { degree = Math.PI / 4 - Math.atan2(mTouchY - mBezierControl1.y, mTouchX - mBezierControl1.x); } // 翻起页阴影顶点与touch点的距离 double d1 = (float) 25 * 1.414 * Math.cos(degree); double d2 = (float) 25 * 1.414 * Math.sin(degree); float x = (float) (mTouchX + d1); float y; if (mIsRT_LB) { y = (float) (mTouchY + d2); } else { y = (float) (mTouchY - d2); } mPath1.reset(); mPath1.moveTo(x, y); mPath1.lineTo(mTouchX, mTouchY); mPath1.lineTo(mBezierControl1.x, mBezierControl1.y); mPath1.lineTo(mBezierStart1.x, mBezierStart1.y); mPath1.close(); canvas.save(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { canvas.clipOutPath(mPath0); } else { canvas.clipPath(mPath0, Region.Op.XOR); } canvas.clipPath(mPath1, Region.Op.INTERSECT); } catch (Exception ignored) { } int leftx; int rightx; GradientDrawable mCurrentPageShadow; if (mIsRT_LB) { leftx = (int) (mBezierControl1.x); rightx = (int) mBezierControl1.x + 25; mCurrentPageShadow = mFrontShadowDrawableVLR; } else { leftx = (int) (mBezierControl1.x - 25); rightx = (int) mBezierControl1.x + 1; mCurrentPageShadow = mFrontShadowDrawableVRL; } float rotateDegrees; rotateDegrees = (float) Math.toDegrees(Math.atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY)); canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y); mCurrentPageShadow.setBounds( leftx, (int) (mBezierControl1.y - mMaxLength), rightx, (int) (mBezierControl1.y)); mCurrentPageShadow.draw(canvas); canvas.restore(); mPath1.reset(); mPath1.moveTo(x, y); mPath1.lineTo(mTouchX, mTouchY); mPath1.lineTo(mBezierControl2.x, mBezierControl2.y); mPath1.lineTo(mBezierStart2.x, mBezierStart2.y); mPath1.close(); canvas.save(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { canvas.clipOutPath(mPath0); } else { canvas.clipPath(mPath0, Region.Op.XOR); } canvas.clipPath(mPath1); } catch (Exception ignored) { } if (mIsRT_LB) { leftx = (int) (mBezierControl2.y); rightx = (int) (mBezierControl2.y + 25); mCurrentPageShadow = mFrontShadowDrawableHTB; } else { leftx = (int) (mBezierControl2.y - 25); rightx = (int) (mBezierControl2.y + 1); mCurrentPageShadow = mFrontShadowDrawableHBT; } rotateDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX)); canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y); float temp; if (mBezierControl2.y < 0) temp = mBezierControl2.y - mScreenHeight; else temp = mBezierControl2.y; int hmg = (int) Math.hypot(mBezierControl2.x, temp); if (hmg > mMaxLength) mCurrentPageShadow.setBounds( (int) (mBezierControl2.x - 25) - hmg, leftx, (int) (mBezierControl2.x + mMaxLength) - hmg, rightx ); else mCurrentPageShadow.setBounds( (int) (mBezierControl2.x - mMaxLength), leftx, (int) (mBezierControl2.x), rightx); mCurrentPageShadow.draw(canvas); canvas.restore(); } private void drawNextPageAreaAndShadow(Canvas canvas, Bitmap bitmap) { mPath1.reset(); mPath1.moveTo(mBezierStart1.x, mBezierStart1.y); mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y); mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y); mPath1.lineTo(mBezierStart2.x, mBezierStart2.y); mPath1.lineTo(mCornerX, mCornerY); mPath1.close(); mDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl1.x - mCornerX, mBezierControl2.y - mCornerY)); int leftx; int rightx; GradientDrawable mBackShadowDrawable; if (mIsRT_LB) { //左下及右上 leftx = (int) (mBezierStart1.x); rightx = (int) (mBezierStart1.x + mTouchToCornerDis / 4); mBackShadowDrawable = mBackShadowDrawableLR; } else { leftx = (int) (mBezierStart1.x - mTouchToCornerDis / 4); rightx = (int) mBezierStart1.x; mBackShadowDrawable = mBackShadowDrawableRL; } canvas.save(); try { canvas.clipPath(mPath0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { canvas.clipPath(mPath1); } else { canvas.clipPath(mPath1, Region.Op.INTERSECT); } //canvas.clipPath(mPath1, Region.Op.INTERSECT); } catch (Exception ignored) { } canvas.drawBitmap(bitmap, 0, 0, null); canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y); mBackShadowDrawable.setBounds( leftx, (int) mBezierStart1.y, rightx, (int) (mMaxLength + mBezierStart1.y));//左上及右下角的xy坐标值,构成一个矩形 mBackShadowDrawable.draw(canvas); canvas.restore(); } private void drawCurrentPageArea(Canvas canvas, Bitmap bitmap) { mPath0.reset(); mPath0.moveTo(mBezierStart1.x, mBezierStart1.y); mPath0.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, mBezierEnd1.y); mPath0.lineTo(mTouchX, mTouchY); mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y); mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y); mPath0.lineTo(mCornerX, mCornerY); mPath0.close(); canvas.save(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { canvas.clipOutPath(mPath0); } else { canvas.clipPath(mPath0, Region.Op.XOR); } canvas.drawBitmap(bitmap, 0, 0, null); try { canvas.restore(); } catch (Exception e) { e.printStackTrace(); } } /** * 计算拖拽点对应的拖拽脚 */ private void calcCornerXY(float x, float y) { if (x <= mScreenWidth / 2.0) { mCornerX = 0; } else { mCornerX = mScreenWidth; } if (y <= mScreenHeight / 2.0) { mCornerY = 0; } else { mCornerY = mScreenHeight; } mIsRT_LB = (mCornerX == 0 && mCornerY == mScreenHeight) || (mCornerX == mScreenWidth && mCornerY == 0); } private void calcPoints() { mMiddleX = (mTouchX + mCornerX) / 2; mMiddleY = (mTouchY + mCornerY) / 2; mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX); mBezierControl1.y = mCornerY; mBezierControl2.x = mCornerX; if (mCornerY - mMiddleY == 0) { mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f; } else { mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY); } mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2; mBezierStart1.y = mCornerY; // 固定左边上下两个点 if (mTouchX > 0 && mTouchX < mScreenWidth) { if (mBezierStart1.x < 0 || mBezierStart1.x > mScreenWidth) { if (mBezierStart1.x < 0) mBezierStart1.x = mScreenWidth - mBezierStart1.x; float f1 = Math.abs(mCornerX - mTouchX); float f2 = mScreenWidth * f1 / mBezierStart1.x; mTouchX = Math.abs(mCornerX - f2); float f3 = Math.abs(mCornerX - mTouchX) * Math.abs(mCornerY - mTouchY) / f1; mTouchY = Math.abs(mCornerY - f3); mMiddleX = (mTouchX + mCornerX) / 2; mMiddleY = (mTouchY + mCornerY) / 2; mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX); mBezierControl1.y = mCornerY; mBezierControl2.x = mCornerX; float f5 = mCornerY - mMiddleY; if (f5 == 0) { mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f; } else { mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY); } mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2; } } mBezierStart2.x = mCornerX; mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2; mTouchToCornerDis = (float) Math.hypot((mTouchX - mCornerX), (mTouchY - mCornerY)); mBezierEnd1 = getCross(new PointF(mTouchX, mTouchY), mBezierControl1, mBezierStart1, mBezierStart2); mBezierEnd2 = getCross(new PointF(mTouchX, mTouchY), mBezierControl2, mBezierStart1, mBezierStart2); mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4; mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4; mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4; mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4; } /** * 求解直线P1P2和直线P3P4的交点坐标 */ private PointF getCross(PointF P1, PointF P2, PointF P3, PointF P4) { PointF CrossP = new PointF(); // 二元函数通式: y=ax+b float a1 = (P2.y - P1.y) / (P2.x - P1.x); float b1 = ((P1.x * P2.y) - (P2.x * P1.y)) / (P1.x - P2.x); float a2 = (P4.y - P3.y) / (P4.x - P3.x); float b2 = ((P3.x * P4.y) - (P4.x * P3.y)) / (P3.x - P4.x); CrossP.x = (b2 - b1) / (a1 - a2); CrossP.y = a1 * CrossP.x + b1; return CrossP; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/page/animation/SlidePageAnim.java ================================================ package com.kunfei.bookshelf.widget.page.animation; import android.graphics.Canvas; import android.graphics.Rect; import android.view.View; /** * 滑动翻页 */ public class SlidePageAnim extends HorizonPageAnim { private Rect mSrcRect, mDestRect, mNextSrcRect, mNextDestRect; public SlidePageAnim(int w, int h, View view, OnPageChangeListener listener) { super(w, h, view, listener); mSrcRect = new Rect(0, 0, mViewWidth, mViewHeight); mDestRect = new Rect(0, 0, mViewWidth, mViewHeight); mNextSrcRect = new Rect(0, 0, mViewWidth, mViewHeight); mNextDestRect = new Rect(0, 0, mViewWidth, mViewHeight); } @Override public void drawMove(Canvas canvas) { int dis; if (mDirection == Direction.NEXT) {//左半边的剩余区域 dis = (int) (mScreenWidth - mStartX + mTouchX); if (dis > mScreenWidth) { dis = mScreenWidth; } //计算bitmap截取的区域 mSrcRect.left = mScreenWidth - dis; //计算bitmap在canvas显示的区域 mDestRect.right = dis; //计算下一页截取的区域 mNextSrcRect.right = mScreenWidth - dis; //计算下一页在canvas显示的区域 mNextDestRect.left = dis; canvas.drawBitmap(bitmapList.get(2), mNextSrcRect, mNextDestRect, null); canvas.drawBitmap(bitmapList.get(1), mSrcRect, mDestRect, null); } else { dis = (int) (mTouchX - mStartX); if (dis < 0) { dis = 0; mStartX = mTouchX; } mSrcRect.left = mScreenWidth - dis; mDestRect.right = dis; //计算下一页截取的区域 mNextSrcRect.right = mScreenWidth - dis; //计算下一页在canvas显示的区域 mNextDestRect.left = dis; canvas.drawBitmap(bitmapList.get(1), mNextSrcRect, mNextDestRect, null); canvas.drawBitmap(bitmapList.get(0), mSrcRect, mDestRect, null); } } @Override public void startAnim() { int dx; if (mDirection == Direction.NEXT) { if (isCancel) { int dis = (int) ((mScreenWidth - mStartX) + mTouchX); if (dis > mScreenWidth) { dis = mScreenWidth; } dx = mScreenWidth - dis; } else { dx = (int) -(mTouchX + (mScreenWidth - mStartX)); } } else { if (isCancel) { dx = (int) -Math.abs(mTouchX - mStartX); } else { dx = (int) (mScreenWidth - (mTouchX - mStartX)); } } //滑动速度保持一致 int duration = (animationSpeed * Math.abs(dx)) / mScreenWidth; mScroller.startScroll((int) mTouchX, 0, dx, 0, duration); super.startAnim(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/prefs/ATEPreferenceCategory.java ================================================ package com.kunfei.bookshelf.widget.prefs; import android.content.Context; import android.os.Build; import android.preference.PreferenceCategory; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import androidx.annotation.RequiresApi; import com.kunfei.bookshelf.utils.theme.ThemeStore; @SuppressWarnings("unused") public class ATEPreferenceCategory extends PreferenceCategory { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public ATEPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public ATEPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public ATEPreferenceCategory(Context context, AttributeSet attrs) { super(context, attrs); } public ATEPreferenceCategory(Context context) { super(context); } @Override protected void onBindView(View view) { super.onBindView(view); if (view.isInEditMode()) { return; } if (view instanceof TextView) { TextView tv = (TextView) view; tv.setTextColor(ThemeStore.accentColor(view.getContext()));//设置title文本的颜色 } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/prefs/ATESwitchPreference.java ================================================ package com.kunfei.bookshelf.widget.prefs; import android.content.Context; import android.os.Build; import android.preference.SwitchPreference; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.Switch; import androidx.annotation.RequiresApi; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; import java.util.LinkedList; @SuppressWarnings("unused") public class ATESwitchPreference extends SwitchPreference { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public ATESwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public ATESwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public ATESwitchPreference(Context context, AttributeSet attrs) { super(context, attrs); } public ATESwitchPreference(Context context) { super(context); } @Override protected void onBindView(View view) { super.onBindView(view); if (view.isInEditMode()) { return; } if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; LinkedList queue = new LinkedList<>(); queue.add(viewGroup); while (!queue.isEmpty()) { ViewGroup current = queue.removeFirst(); for (int i = 0; i < current.getChildCount(); i++) { if (current.getChildAt(i) instanceof Switch) { ATH.setTint(current.getChildAt(i), ThemeStore.accentColor(view.getContext())); return; } else if (current.getChildAt(i) instanceof ViewGroup) { queue.addLast((ViewGroup) current.getChildAt(i)); } } } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/prefs/IconListPreference.java ================================================ package com.kunfei.bookshelf.widget.prefs; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog.Builder; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.preference.ListPreference; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; import android.widget.ImageView; import android.widget.ListAdapter; import com.kunfei.bookshelf.R; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; public class IconListPreference extends ListPreference { private List mEntryDrawables = new ArrayList<>(); public IconListPreference(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0); CharSequence[] drawables; try { drawables = a.getTextArray(R.styleable.IconListPreference_icons); } finally { a.recycle(); } for (CharSequence drawable : drawables) { int resId = context.getResources().getIdentifier(drawable.toString(), "mipmap", context.getPackageName()); Drawable d = context.getResources().getDrawable(resId); mEntryDrawables.add(d); } setWidgetLayoutResource(R.layout.view_icon); } private ListAdapter createListAdapter() { final String selectedValue = getValue(); int selectedIndex = findIndexOfValue(selectedValue); return new AppArrayAdapter(getContext(), R.layout.item_icon_preference, getEntries(), mEntryDrawables, selectedIndex); } @Override protected void onBindView(View view) { super.onBindView(view); String selectedValue = getValue(); int selectedIndex = findIndexOfValue(selectedValue); Drawable drawable = mEntryDrawables.get(selectedIndex); ((ImageView) view.findViewById(R.id.preview)).setImageDrawable(drawable); } @Override protected void onPrepareDialogBuilder(Builder builder) { builder.setAdapter(createListAdapter(), this); super.onPrepareDialogBuilder(builder); } public static class AppArrayAdapter extends ArrayAdapter { private List mImageDrawables = null; private int mSelectedIndex = 0; AppArrayAdapter(Context context, int textViewResourceId, CharSequence[] objects, List imageDrawables, int selectedIndex) { super(context, textViewResourceId, objects); mSelectedIndex = selectedIndex; mImageDrawables = imageDrawables; } @NotNull @Override @SuppressLint("ViewHolder") public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = ((Activity) getContext()).getLayoutInflater(); View view = inflater.inflate(R.layout.item_icon_preference, parent, false); CheckedTextView textView = view.findViewById(R.id.label); textView.setText(getItem(position)); textView.setChecked(position == mSelectedIndex); ImageView imageView = view.findViewById(R.id.icon); imageView.setImageDrawable(mImageDrawables.get(position)); return view; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/BaseExpandAbleViewHolder.java ================================================ package com.kunfei.bookshelf.widget.recycler.expandable; import android.content.Context; import android.view.View; import android.view.ViewGroup; import androidx.recyclerview.widget.RecyclerView; /** * author:Drawthink * describe:BaseViewHolder * date: 2017/5/22 */ public abstract class BaseExpandAbleViewHolder extends RecyclerView.ViewHolder { public static final int VIEW_TYPE_PARENT = 1; public static final int VIEW_TYPE_CHILD = 2; public ViewGroup childView; public ViewGroup groupView; public BaseExpandAbleViewHolder(Context ctx, View itemView, int viewType) { super(itemView); switch (viewType) { case VIEW_TYPE_PARENT: groupView = (ViewGroup) itemView.findViewById(getGroupViewResId()); break; case VIEW_TYPE_CHILD: childView = (ViewGroup) itemView.findViewById(getChildViewResId()); break; } } /** * return ChildView root layout id */ public abstract int getChildViewResId(); /** * return GroupView root layout id */ public abstract int getGroupViewResId(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/BaseExpandableRecyclerAdapter.java ================================================ package com.kunfei.bookshelf.widget.recycler.expandable; import android.content.Context; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.widget.recycler.expandable.bean.BaseItem; import com.kunfei.bookshelf.widget.recycler.expandable.bean.GroupItem; import com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData; import java.util.ArrayList; import java.util.List; import static com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandAbleViewHolder.VIEW_TYPE_CHILD; import static com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandAbleViewHolder.VIEW_TYPE_PARENT; /** * author:Drawthink * describe: * date: 2017/5/22 * T :group data * S :child data * VH :ViewHolder */ @SuppressWarnings("unchecked") public abstract class BaseExpandableRecyclerAdapter extends RecyclerView.Adapter { public static final String TAG = BaseExpandableRecyclerAdapter.class.getSimpleName(); private Context ctx; /** * all data */ private List allDatas; /** * showing datas */ private List showingDatas = new ArrayList<>(); /** * child datas */ private List> childDatas; private boolean canExpandAll; private OnRecyclerViewListener.OnItemClickListener itemClickListener; private OnRecyclerViewListener.OnItemLongClickListener itemLongClickListener; private OnRecyclerViewListener.OnGroupCollapseListener groupCollapseListener; private OnRecyclerViewListener.OnGroupExpandedListener groupExpandedListener; public BaseExpandableRecyclerAdapter(Context ctx, List datas) { this.ctx = ctx; this.allDatas = datas; setShowingDatas(); this.notifyDataSetChanged(); } public void setOnItemClickListener(OnRecyclerViewListener.OnItemClickListener listener) { this.itemClickListener = listener; } public void setOnItemLongClickListener(OnRecyclerViewListener.OnItemLongClickListener longClickListener) { this.itemLongClickListener = longClickListener; } public void setGroupCollapseListener(OnRecyclerViewListener.OnGroupCollapseListener groupCollapseListener) { this.groupCollapseListener = groupCollapseListener; } public void setGroupExpandedListener(OnRecyclerViewListener.OnGroupExpandedListener groupExpandedListener) { this.groupExpandedListener = groupExpandedListener; } public List getAllDatas() { return allDatas; } public void setAllDatas(List allDatas) { this.allDatas = allDatas; setShowingDatas(); this.notifyDataSetChanged(); } public void clearAll() { this.allDatas.clear(); setShowingDatas(); this.notifyDataSetChanged(); } @Override public int getItemCount() { return null == showingDatas ? 0 : showingDatas.size(); } @Override public int getItemViewType(int position) { if (showingDatas.get(position) instanceof GroupItem) { return VIEW_TYPE_PARENT; } else { return VIEW_TYPE_CHILD; } } @NonNull @Override public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = null; switch (viewType) { case VIEW_TYPE_PARENT: view = getGroupView(parent); break; case VIEW_TYPE_CHILD: view = getChildView(parent); break; } return createRealViewHolder(ctx, view, viewType); } @Override public void onBindViewHolder(@NonNull final VH holder, final int position) { final Object item = showingDatas.get(position); final int gp = getGroupPosition(position); final int cp = getChildPosition(gp, position); if (item != null && item instanceof GroupItem) { onBindGroupHolder(holder, gp, position, (T) ((GroupItem) item).getGroupData()); holder.groupView.setOnClickListener(v -> { if (null != itemClickListener) { itemClickListener.onGroupItemClick(position, gp, holder.groupView); } if (((GroupItem) item).isExpand()) { collapseGroup(position); } else { expandGroup(position); } }); holder.groupView.setOnLongClickListener(v -> { if (null != itemLongClickListener) { itemLongClickListener.onGroupItemLongClick(position, gp, holder.groupView); } return true; }); } else { onBindChildpHolder(holder, gp, cp, position, (S) item); holder.childView.setOnClickListener(v -> { if (null != itemClickListener) { itemClickListener.onChildItemClick(position, gp, cp, holder.childView); } }); holder.childView.setOnLongClickListener(v -> { if (null != itemLongClickListener) { int gp1 = getGroupPosition(position); itemLongClickListener.onChildItemLongClick(position, gp1, cp, holder.childView); } return true; }); } } /** * setup showing datas */ private void setShowingDatas() { if (null != showingDatas) { showingDatas.clear(); } if (this.childDatas == null) { this.childDatas = new ArrayList<>(); } childDatas.clear(); GroupItem groupItem; for (int i = 0; i < allDatas.size(); i++) { if (allDatas.get(i).getGroupItem() != null) { groupItem = allDatas.get(i).getGroupItem(); } else { break; } childDatas.add(i, groupItem.getChildDatas()); showingDatas.add(groupItem); if (groupItem.hasChilds() && groupItem.isExpand()) { showingDatas.addAll(groupItem.getChildDatas()); } } } /** * expandGroup * * @param position showingDatas position */ public void expandGroup(int position) { Object item = showingDatas.get(position); if (null == item) { return; } if (!(item instanceof GroupItem)) { return; } if (((GroupItem) item).isExpand()) { return; } if (!canExpandAll()) { for (int i = 0; i < showingDatas.size(); i++) { if (i != position) { int tempPositino = collapseGroup(i); if (tempPositino != -1) { position = tempPositino; } } } } List tempChilds; if (((GroupItem) item).hasChilds()) { if (groupExpandedListener != null) { groupExpandedListener.onGroupExpanded(position); } tempChilds = ((GroupItem) item).getChildDatas(); ((GroupItem) item).onExpand(); if (canExpandAll()) { showingDatas.addAll(position + 1, tempChilds); notifyItemRangeInserted(position + 1, tempChilds.size()); notifyItemRangeChanged(position, showingDatas.size() - (position + 1)); } else { int tempPsi = showingDatas.indexOf(item); showingDatas.addAll(tempPsi + 1, tempChilds); notifyItemRangeInserted(tempPsi + 1, tempChilds.size()); notifyItemRangeChanged(tempPsi, showingDatas.size() - (tempPsi + 1)); } } } /** * collapseGroup * * @param position showingDatas position */ private int collapseGroup(int position) { Object item = showingDatas.get(position); if (null == item) { return -1; } if (!(item instanceof GroupItem)) { return -1; } if (!((GroupItem) item).isExpand()) { return -1; } int tempSize = showingDatas.size(); List tempChilds; if (((GroupItem) item).hasChilds()) { if (groupCollapseListener != null) { groupCollapseListener.onGroupCollapse(position); } tempChilds = ((GroupItem) item).getChildDatas(); ((GroupItem) item).onExpand(); showingDatas.removeAll(tempChilds); notifyItemRangeRemoved(position + 1, tempChilds.size()); notifyItemRangeChanged(position, tempSize - (position + 1)); return position; } return -1; } /** * @param position showingDatas position * @return GroupPosition */ private int getGroupPosition(int position) { Object item = showingDatas.get(position); if (item instanceof GroupItem) { for (int j = 0; j < allDatas.size(); j++) { if (allDatas.get(j).getGroupItem().equals(item)) { return j; } } } for (int i = 0; i < childDatas.size(); i++) { if (childDatas.get(i).contains(item)) { return i; } } return -1; } private int getChildPosition(int groupPosition, int showDataPosition) { Object item = showingDatas.get(showDataPosition); try { return childDatas.get(groupPosition).indexOf(item); } catch (IndexOutOfBoundsException ignored) { } return 0; } /** * return groupView */ public abstract View getGroupView(ViewGroup parent); /** * return childView */ public abstract View getChildView(ViewGroup parent); /** * return instance */ public abstract VH createRealViewHolder(Context ctx, View view, int viewType); /** * onBind groupData to groupView */ public abstract void onBindGroupHolder(VH holder, int groupPos, int position, T groupData); /** * onBind childData to childView */ public abstract void onBindChildpHolder(VH holder, int groupPos, int childPos, int position, S childData); /** * if return true Allow all expand otherwise Only one can be expand at the same time */ public boolean canExpandAll() { return canExpandAll; } public void setCanExpandAll(boolean canExpandAll) { this.canExpandAll = canExpandAll; } /** * 对原数据进行增加删除,调用此方法进行notify */ public void notifyRecyclerViewData() { notifyDataSetChanged(); setShowingDatas(); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/OnRecyclerViewListener.java ================================================ package com.kunfei.bookshelf.widget.recycler.expandable; import android.view.View; /** * author:Drawthink * describe:RecyclerViewListener * date: 2017/5/22 */ public interface OnRecyclerViewListener { /** * 单击事件 */ interface OnItemClickListener { /** * position 当前在列表中的position */ void onGroupItemClick(int position, int groupPosition, View view); void onChildItemClick(int position, int groupPosition, int childPosition, View view); } /** * 长按事件 */ interface OnItemLongClickListener { void onGroupItemLongClick(int position, int groupPosition, View view); void onChildItemLongClick(int position, int groupPosition, int childPosition, View view); } interface OnGroupExpandedListener { /** * 分组展开 * * @param groupPosition 分组的位置 */ void onGroupExpanded(int groupPosition); } interface OnGroupCollapseListener { /** * 分组收起 * * @param groupPosition 分组的位置 */ void onGroupCollapse(int groupPosition); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/bean/BaseItem.java ================================================ package com.kunfei.bookshelf.widget.recycler.expandable.bean; /** * author:Drawthink * describe: * date: 2017/5/22 */ public abstract class BaseItem { public abstract boolean isParent(); // public abstract boolean isExpand(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/bean/GroupItem.java ================================================ package com.kunfei.bookshelf.widget.recycler.expandable.bean; import java.util.List; /** * author:Drawthink * describe: * date: 2017/5/22 * T 为group数据对象 * S 为child数据对象 */ public class GroupItem extends BaseItem { /** * head data */ private T groupData; /** * childDatas */ private List childDatas; /** * 是否展开, 默认展开 */ private boolean isExpand = true; public GroupItem(T groupData, List childDatas, boolean isExpand) { this.groupData = groupData; this.childDatas = childDatas; this.isExpand = isExpand; } /** * 返回是否是父节点 */ @Override public boolean isParent() { return true; } public boolean isExpand() { return isExpand; } public void onExpand() { isExpand = !isExpand; } public boolean hasChilds() { return getChildDatas() != null && !getChildDatas().isEmpty(); } public List getChildDatas() { return childDatas; } public void setChildDatas(List childDatas) { this.childDatas = childDatas; } public void removeChild(int childPosition) { if (childDatas == null || childDatas.size() == 0) { return; } childDatas.remove(childPosition); } public T getGroupData() { return groupData; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/bean/RecyclerViewData.java ================================================ package com.kunfei.bookshelf.widget.recycler.expandable.bean; import java.util.List; /** * author:Drawthink * describe: * date: 2017/5/22 * T 为group数据对象 * S 为child数据对象 */ @SuppressWarnings("unchecked") public class RecyclerViewData { private GroupItem groupItem; /** * @param isExpand 初始化展示数据时,该组数据是否展开 */ public RecyclerViewData(T groupData, List childDatas, boolean isExpand) { this.groupItem = new GroupItem(groupData, childDatas, isExpand); } public RecyclerViewData(T groupData, List childDatas) { this.groupItem = new GroupItem(groupData, childDatas, false); } public GroupItem getGroupItem() { return groupItem; } public void setGroupItem(GroupItem groupItem) { this.groupItem = groupItem; } public T getGroupData() { return (T) groupItem.getGroupData(); } public List getChildList() { return groupItem.getChildDatas(); } public void removeChild(int position) { if (null == groupItem || !groupItem.hasChilds()) { return; } groupItem.getChildDatas().remove(position); } public S getChild(int childPosition) { return (S) groupItem.getChildDatas().get(childPosition); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/BaseRefreshListener.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; public interface BaseRefreshListener { public void startRefresh(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/OnLoadMoreListener.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; public interface OnLoadMoreListener { public void startLoadMore(); public void loadMoreErrorTryAgain(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/OnRefreshWithProgressListener.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; public interface OnRefreshWithProgressListener extends BaseRefreshListener { public int getMaxProgress(); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshLayout.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; import android.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.kunfei.bookshelf.R; /** * Created by newbiechen on 17-4-22. * 功能: * 1. 加载动画 * 2. 加载错误点击重新加载 */ public class RefreshLayout extends FrameLayout { protected static final int STATUS_LOADING = 0; protected static final int STATUS_FINISH = 1; protected static final int STATUS_ERROR = 2; protected static final int STATUS_EMPTY = 3; private static final String TAG = "RefreshLayout"; private Context mContext; private int mEmptyViewId; private int mErrorViewId; private int mLoadingViewId; private View mEmptyView; private View mErrorView; private View mLoadingView; private View mContentView; private OnReloadingListener mListener; private int mStatus = 0; public RefreshLayout(Context context) { this(context, null); } public RefreshLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; initAttrs(attrs); initView(); } private void initAttrs(AttributeSet attrs) { TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.RefreshLayout); mEmptyViewId = typedArray.getResourceId(R.styleable.RefreshLayout_layout_refresh_empty, R.layout.view_empty); mErrorViewId = typedArray.getResourceId(R.styleable.RefreshLayout_layout_refresh_error, R.layout.view_net_error); mLoadingViewId = typedArray.getResourceId(R.styleable.RefreshLayout_layout_refresh_loading, R.layout.view_loading); typedArray.recycle(); } private void initView() { //添加在empty、error、loading 情况下的布局 mEmptyView = inflateView(mEmptyViewId); mErrorView = inflateView(mErrorViewId); mLoadingView = inflateView(mLoadingViewId); addView(mEmptyView); addView(mErrorView); addView(mLoadingView); //设置监听器 mErrorView.setOnClickListener( (view) -> { if (mListener != null) { toggleStatus(STATUS_LOADING); mListener.onReload(); } } ); } @Override protected void onFinishInflate() { super.onFinishInflate(); toggleStatus(STATUS_LOADING); } @Override public void onViewAdded(View child) { super.onViewAdded(child); if (getChildCount() == 4) { mContentView = child; } } //除了自带的数据,保证子类只能够添加一个子View @Override public void addView(View child) { if (getChildCount() > 4) { throw new IllegalStateException("RefreshLayout can host only one direct child"); } super.addView(child); } @Override public void addView(View child, int index) { if (getChildCount() > 4) { throw new IllegalStateException("RefreshLayout can host only one direct child"); } super.addView(child, index); } @Override public void addView(View child, ViewGroup.LayoutParams params) { if (getChildCount() > 4) { throw new IllegalStateException("RefreshLayout can host only one direct child"); } super.addView(child, params); } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (getChildCount() > 4) { throw new IllegalStateException("RefreshLayout can host only one direct child"); } super.addView(child, index, params); } public void showLoading() { if (mStatus != STATUS_LOADING) { toggleStatus(STATUS_LOADING); } } public void showFinish() { if (mStatus == STATUS_LOADING) { toggleStatus(STATUS_FINISH); } } public void showError() { if (mStatus != STATUS_ERROR) { toggleStatus(STATUS_ERROR); } } public void showEmpty() { if (mStatus != STATUS_EMPTY) { toggleStatus(STATUS_EMPTY); } } //视图根据状态切换 private void toggleStatus(int status) { switch (status) { case STATUS_LOADING: mLoadingView.setVisibility(VISIBLE); mEmptyView.setVisibility(GONE); mErrorView.setVisibility(GONE); if (mContentView != null) { mContentView.setVisibility(GONE); } break; case STATUS_FINISH: if (mContentView != null) { mContentView.setVisibility(VISIBLE); } mLoadingView.setVisibility(GONE); mEmptyView.setVisibility(GONE); mErrorView.setVisibility(GONE); break; case STATUS_ERROR: mErrorView.setVisibility(VISIBLE); mLoadingView.setVisibility(GONE); mEmptyView.setVisibility(GONE); if (mContentView != null) { mContentView.setVisibility(GONE); } break; case STATUS_EMPTY: mEmptyView.setVisibility(VISIBLE); mErrorView.setVisibility(GONE); mLoadingView.setVisibility(GONE); if (mContentView != null) { mContentView.setVisibility(GONE); } break; } mStatus = status; } protected int getStatus() { return mStatus; } public void setOnReloadingListener(OnReloadingListener listener) { mListener = listener; } private View inflateView(int id) { return LayoutInflater.from(mContext) .inflate(id, this, false); } //数据存储 @Override protected Parcelable onSaveInstanceState() { Parcelable superParcel = super.onSaveInstanceState(); SavedState savedState = new SavedState(superParcel); savedState.status = mStatus; return savedState; } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); //刷新状态 toggleStatus(savedState.status); } //添加错误重新加载的监听 public interface OnReloadingListener { void onReload(); } static class SavedState extends BaseSavedState { public static final Creator CREATOR = new Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; int status; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); status = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(status); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshProgressBar.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.os.Looper; import android.util.AttributeSet; import android.view.View; import com.kunfei.bookshelf.R; public class RefreshProgressBar extends View { int a = 1; private int maxProgress = 100; private int durProgress = 0; private int secondMaxProgress = 100; private int secondDurProgress = 0; private int bgColor = 0x00000000; private int secondColor = 0xFFC1C1C1; private int fontColor = 0xFF363636; private int speed = 1; private int secondFinalProgress = 0; private Paint paint; private Boolean isAutoLoading = false; private Rect bgRect = new Rect(); private Rect secondRect = new Rect(); private RectF fontRectF = new RectF(); public RefreshProgressBar(Context context) { this(context, null); } public RefreshProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RefreshProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } public Boolean getIsAutoLoading() { return isAutoLoading; } public void setIsAutoLoading(Boolean loading) { if (loading && getVisibility() != View.VISIBLE) { setVisibility(View.VISIBLE); } isAutoLoading = loading; if (!isAutoLoading) { secondDurProgress = 0; secondFinalProgress = 0; } maxProgress = 0; invalidate(); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { paint = new Paint(); paint.setStyle(Paint.Style.FILL); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshProgressBar); speed = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_speed, speed); maxProgress = a.getInt(R.styleable.RefreshProgressBar_max_progress, maxProgress); durProgress = a.getInt(R.styleable.RefreshProgressBar_dur_progress, durProgress); secondDurProgress = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_second_dur_progress, secondDurProgress); secondFinalProgress = secondDurProgress; secondMaxProgress = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_second_max_progress, secondMaxProgress); bgColor = a.getColor(R.styleable.RefreshProgressBar_bg_color, bgColor); secondColor = a.getColor(R.styleable.RefreshProgressBar_second_color, secondColor); fontColor = a.getColor(R.styleable.RefreshProgressBar_font_color, fontColor); a.recycle(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setColor(bgColor); bgRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); canvas.drawRect(bgRect, paint); if (secondDurProgress > 0 && secondMaxProgress > 0) { int secondDur = secondDurProgress; if (secondDur > secondMaxProgress) { secondDur = secondMaxProgress; } paint.setColor(secondColor); int tempW = (int) (getMeasuredWidth() * 1.0f * (secondDur * 1.0f / secondMaxProgress)); secondRect.set(getMeasuredWidth() / 2 - tempW / 2, 0, getMeasuredWidth() / 2 + tempW / 2, getMeasuredHeight()); canvas.drawRect(secondRect, paint); } if (durProgress > 0 && maxProgress > 0) { paint.setColor(fontColor); fontRectF.set(0, 0, getMeasuredWidth() * 1.0f * (durProgress * 1.0f / maxProgress), getMeasuredHeight()); canvas.drawRect(fontRectF, paint); } if (isAutoLoading) { if (secondDurProgress >= secondMaxProgress) { a = -1; } else if (secondDurProgress <= 0) { a = 1; } secondDurProgress += (a * speed); if (secondDurProgress < 0) secondDurProgress = 0; else if (secondDurProgress > secondMaxProgress) secondDurProgress = secondMaxProgress; secondFinalProgress = secondDurProgress; invalidate(); } else { if (secondDurProgress != secondFinalProgress) { if (secondDurProgress > secondFinalProgress) { secondDurProgress -= speed; if (secondDurProgress < secondFinalProgress) { secondDurProgress = secondFinalProgress; } } else { secondDurProgress += speed; if (secondDurProgress > secondFinalProgress) { secondDurProgress = secondFinalProgress; } } this.invalidate(); } if (secondDurProgress == 0 && durProgress == 0 && secondFinalProgress == 0 && getVisibility() == View.VISIBLE) { setVisibility(View.INVISIBLE); } } } public int getMaxProgress() { return maxProgress; } public void setMaxProgress(int maxProgress) { this.maxProgress = maxProgress; } public int getDurProgress() { return durProgress; } public void setDurProgress(int durProgress) { if (durProgress < 0) { durProgress = 0; } if (durProgress > maxProgress) { durProgress = maxProgress; } this.durProgress = durProgress; if (Looper.myLooper() == Looper.getMainLooper()) { this.invalidate(); } else { this.postInvalidate(); } } public int getSecondMaxProgress() { return secondMaxProgress; } public void setSecondMaxProgress(int secondMaxProgress) { this.secondMaxProgress = secondMaxProgress; } public int getSecondDurProgress() { return secondDurProgress; } public void setSecondDurProgress(int secondDur) { this.secondDurProgress = secondDur; this.secondFinalProgress = secondDurProgress; if (Looper.myLooper() == Looper.getMainLooper()) { this.invalidate(); } else { this.postInvalidate(); } } public void setSecondDurProgressWithAnim(int secondDur) { if (secondDur < 0) { secondDur = 0; } if (secondDur > secondMaxProgress) { secondDur = secondMaxProgress; } this.secondFinalProgress = secondDur; if (Looper.myLooper() == Looper.getMainLooper()) { this.invalidate(); } else { this.postInvalidate(); } } public void clean() { durProgress = 0; secondDurProgress = 0; secondFinalProgress = 0; if (Looper.myLooper() == Looper.getMainLooper()) { this.invalidate(); } else { this.postInvalidate(); } } public int getBgColor() { return bgColor; } public void setBgColor(int bgColor) { this.bgColor = bgColor; } public int getSecondColor() { return secondColor; } public void setSecondColor(int secondColor) { this.secondColor = secondColor; } public int getFontColor() { return fontColor; } public void setFontColor(int fontColor) { this.fontColor = fontColor; } public int getSpeed() { return speed; } public void setSpeed(int speed) { this.speed = speed; } public int getSecondFinalProgress() { return secondFinalProgress; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshRecyclerView.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.databinding.ViewRefreshRecyclerBinding; import java.util.Objects; public class RefreshRecyclerView extends FrameLayout { private ViewRefreshRecyclerBinding binding = ViewRefreshRecyclerBinding.inflate(LayoutInflater.from(getContext()), this, true); private View noDataView; private View refreshErrorView; private float durTouchX = -1000000; private float durTouchY = -1000000; private BaseRefreshListener baseRefreshListener; private OnLoadMoreListener loadMoreListener; private OnTouchListener refreshTouchListener = new OnTouchListener() { @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: durTouchX = event.getX(); durTouchY = event.getY(); break; case MotionEvent.ACTION_MOVE: if (durTouchX == -1000000) { durTouchX = event.getX(); } if (durTouchY == -1000000) durTouchY = event.getY(); float dY = event.getY() - durTouchY; //>0下拉 durTouchY = event.getY(); if (baseRefreshListener != null && ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getIsRequesting() == 0 && binding.rpb.getSecondDurProgress() == binding.rpb.getSecondFinalProgress()) { if (binding.rpb.getVisibility() != View.VISIBLE) { binding.rpb.setVisibility(View.VISIBLE); } if (binding.recyclerView.getAdapter().getItemCount() > 0) { if (0 == ((LinearLayoutManager) Objects.requireNonNull(binding.recyclerView.getLayoutManager())).findFirstCompletelyVisibleItemPosition()) { binding.rpb.setSecondDurProgress((int) (binding.rpb.getSecondDurProgress() + dY)); } } else { binding.rpb.setSecondDurProgress((int) (binding.rpb.getSecondDurProgress() + dY)); } return binding.rpb.getSecondDurProgress() > 0; } break; case MotionEvent.ACTION_UP: if (baseRefreshListener != null && binding.rpb.getSecondMaxProgress() > 0 && binding.rpb.getSecondDurProgress() > 0) { if (binding.rpb.getSecondDurProgress() >= binding.rpb.getSecondMaxProgress() && ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getIsRequesting() == 0) { if (baseRefreshListener instanceof OnRefreshWithProgressListener) { //带有进度的 //执行刷新响应 ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(false, false); ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsRequesting(1, true); binding.rpb.setMaxProgress(((OnRefreshWithProgressListener) baseRefreshListener).getMaxProgress()); baseRefreshListener.startRefresh(); if (noDataView != null) { noDataView.setVisibility(GONE); } if (refreshErrorView != null) { refreshErrorView.setVisibility(GONE); } } else { //不带进度的 ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(false, false); ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsRequesting(1, true); baseRefreshListener.startRefresh(); if (noDataView != null) { noDataView.setVisibility(GONE); } if (refreshErrorView != null) { refreshErrorView.setVisibility(GONE); } binding.rpb.setIsAutoLoading(true); } } else { if (((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getIsRequesting() != 1) binding.rpb.setSecondDurProgressWithAnim(0); } } durTouchX = -1000000; durTouchY = -1000000; break; } return false; } }; public RefreshRecyclerView(Context context) { this(context, null); } public RefreshRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RefreshRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); @SuppressLint("CustomViewStyleable") TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshProgressBar); binding.rpb.setSpeed(a.getDimensionPixelSize(R.styleable.RefreshProgressBar_speed, binding.rpb.getSpeed())); binding.rpb.setMaxProgress(a.getInt(R.styleable.RefreshProgressBar_max_progress, binding.rpb.getMaxProgress())); binding.rpb.setSecondMaxProgress(a.getDimensionPixelSize(R.styleable.RefreshProgressBar_second_max_progress, binding.rpb.getSecondMaxProgress())); binding.rpb.setBgColor(a.getColor(R.styleable.RefreshProgressBar_bg_color, binding.rpb.getBgColor())); binding.rpb.setSecondColor(a.getColor(R.styleable.RefreshProgressBar_second_color, binding.rpb.getSecondColor())); binding.rpb.setFontColor(a.getColor(R.styleable.RefreshProgressBar_font_color, binding.rpb.getFontColor())); a.recycle(); bindEvent(); } public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) { binding.recyclerView.addItemDecoration(decor); } public void setBaseRefreshListener(BaseRefreshListener baseRefreshListener) { this.baseRefreshListener = baseRefreshListener; } public void setLoadMoreListener(OnLoadMoreListener loadMoreListener) { this.loadMoreListener = loadMoreListener; } @SuppressLint("ClickableViewAccessibility") private void bindEvent() { binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (((RefreshRecyclerViewAdapter) Objects.requireNonNull(recyclerView.getAdapter())).canLoadMore() && recyclerView.getAdapter().getItemCount() - 1 == ((LinearLayoutManager) Objects.requireNonNull(recyclerView.getLayoutManager())).findLastVisibleItemPosition()) { if (!((RefreshRecyclerViewAdapter) recyclerView.getAdapter()).getLoadMoreError()) { if (null != loadMoreListener) { ((RefreshRecyclerViewAdapter) recyclerView.getAdapter()).setIsRequesting(2, false); loadMoreListener.startLoadMore(); } } } } }); binding.recyclerView.setOnTouchListener(refreshTouchListener); } public RefreshProgressBar getRpb() { return binding.rpb; } public RecyclerView getRecyclerView() { return binding.recyclerView; } public void refreshError() { binding.rpb.setIsAutoLoading(false); binding.rpb.clean(); ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, true); if (noDataView != null) { noDataView.setVisibility(GONE); } if (refreshErrorView != null) { refreshErrorView.setVisibility(VISIBLE); } } public void startRefresh() { if (baseRefreshListener instanceof OnRefreshWithProgressListener) { ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsAll(false, false); ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsRequesting(1, false); binding.rpb.setSecondDurProgress(binding.rpb.getSecondMaxProgress()); binding.rpb.setMaxProgress(((OnRefreshWithProgressListener) baseRefreshListener).getMaxProgress()); } else { ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(1, true); binding.rpb.setIsAutoLoading(true); if (noDataView != null) { noDataView.setVisibility(GONE); } if (refreshErrorView != null) { refreshErrorView.setVisibility(GONE); } } } public void finishRefresh(Boolean needNotify) { finishRefresh(((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getICount() == 0, needNotify); } public void finishRefresh(Boolean isAll, Boolean needNotify) { binding.rpb.setDurProgress(0); if (isAll) { ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, false); binding.rpb.setIsAutoLoading(false); ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(true, needNotify); } else { binding.rpb.setIsAutoLoading(false); ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, needNotify); } if (isAll) { if (noDataView != null) { binding.recyclerView.post(() -> { if (((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).getICount() == 0) { noDataView.setVisibility(VISIBLE); } else { noDataView.setVisibility(GONE); } }); } if (refreshErrorView != null) { refreshErrorView.setVisibility(GONE); } } } public void finishLoadMore(Boolean isAll, Boolean needNoti) { if (isAll) { ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, false); ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(true, needNoti); } else { ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, needNoti); } if (noDataView != null) { noDataView.setVisibility(GONE); } if (refreshErrorView != null) { refreshErrorView.setVisibility(GONE); } } public void setRefreshRecyclerViewAdapter(RefreshRecyclerViewAdapter refreshRecyclerViewAdapter, RecyclerView.LayoutManager layoutManager) { refreshRecyclerViewAdapter.setClickTryAgainListener(() -> { if (loadMoreListener != null) loadMoreListener.loadMoreErrorTryAgain(); }); binding.recyclerView.setLayoutManager(layoutManager); binding.recyclerView.setAdapter(refreshRecyclerViewAdapter); } public void setRefreshRecyclerViewAdapter(View headerView, RefreshRecyclerViewAdapter refreshRecyclerViewAdapter, RecyclerView.LayoutManager layoutManager) { refreshRecyclerViewAdapter.setClickTryAgainListener(() -> { if (loadMoreListener != null) loadMoreListener.loadMoreErrorTryAgain(); }); binding.llContent.addView(headerView, 0); binding.recyclerView.setLayoutManager(layoutManager); binding.recyclerView.setAdapter(refreshRecyclerViewAdapter); } public void setItemTouchHelperCallback(ItemTouchHelper.Callback callback) { ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback); itemTouchHelper.attachToRecyclerView(binding.recyclerView); } public void loadMoreError() { binding.rpb.setIsAutoLoading(false); binding.rpb.clean(); ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setLoadMoreError(true, true); } public void setNoDataAndRefreshErrorView(View noData, View refreshError) { if (noData != null) { noDataView = noData; noDataView.setVisibility(GONE); addView(noDataView, getChildCount() - 1); } if (refreshError != null) { refreshErrorView = refreshError; addView(refreshErrorView, 2); refreshErrorView.setVisibility(GONE); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshRecyclerViewAdapter.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; import android.os.Handler; import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; public abstract class RefreshRecyclerViewAdapter extends RecyclerView.Adapter { private final int LOAD_MORE_TYPE = 2001; private Handler handler; private int isRequesting = 0; //0是未执行网络请求 1是正在下拉刷新 2是正在加载更多 private Boolean needLoadMore; private Boolean isAll = false; //判断是否还有更多 private Boolean loadMoreError = false; private OnClickTryAgainListener clickTryAgainListener; public RefreshRecyclerViewAdapter(Boolean needLoadMore) { this.needLoadMore = needLoadMore; handler = new Handler(); } public int getIsRequesting() { return isRequesting; } public void setIsRequesting(int isRequesting, Boolean needNotify) { this.isRequesting = isRequesting; if (this.isRequesting == 1) { isAll = false; } if (needNotify) { if (Looper.myLooper() == Looper.getMainLooper()) { notifyItemRangeChanged(getItemCount(), getItemCount() - getICount()); } else { handler.post(this::notifyDataSetChanged); } } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == LOAD_MORE_TYPE) { return new LoadMoreViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.view_refresh_load_more, parent, false)); } else return onCreateIViewHolder(parent, viewType); } public abstract RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType); @Override public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { if (holder.getItemViewType() == LOAD_MORE_TYPE) { LoadMoreViewHolder loadHolder = (LoadMoreViewHolder) holder; if (!loadMoreError) { loadHolder.tvLoadMore.setText("正在加载..."); } else { loadHolder.tvLoadMore.setText("加载失败,点击重试"); } ((LoadMoreViewHolder) holder).llLoadMore.setOnClickListener(v -> { if (null != clickTryAgainListener && loadMoreError) { clickTryAgainListener.loadMoreErrorTryAgain(); loadMoreError = false; ((LoadMoreViewHolder) holder).tvLoadMore.setText("正在加载..."); } }); } else onBindIViewHolder(holder, position); } public abstract void onBindIViewHolder(RecyclerView.ViewHolder holder, int position); @Override public int getItemViewType(int position) { if (needLoadMore && isRequesting != 1 && !isAll && position == getItemCount() - 1 && getICount() > 0) { return LOAD_MORE_TYPE; } else { return getIViewType(position); } } public abstract int getIViewType(int position); @Override public int getItemCount() { if (needLoadMore && isRequesting != 1 && !isAll && getICount() > 0) { return getICount() + 1; } else return getICount(); } public abstract int getICount(); public void setIsAll(Boolean isAll, Boolean needNotify) { this.isAll = isAll; if (needNotify) { if (Looper.myLooper() == Looper.getMainLooper()) { if (getItemCount() > getICount()) { notifyItemRangeChanged(getItemCount(), getItemCount() - getICount()); } else notifyItemRemoved(getItemCount() + 1); } else { handler.post(this::notifyDataSetChanged); } } } public Boolean canLoadMore() { return needLoadMore && isRequesting == 0 && !isAll && getICount() > 0; } public OnClickTryAgainListener getClickTryAgainListener() { return clickTryAgainListener; } public void setClickTryAgainListener(OnClickTryAgainListener clickTryAgainListener) { this.clickTryAgainListener = clickTryAgainListener; } public Boolean getLoadMoreError() { return loadMoreError; } public void setLoadMoreError(Boolean loadMoreError, Boolean needNotify) { this.isRequesting = 0; this.loadMoreError = loadMoreError; if (needNotify) { if (Looper.myLooper() == Looper.getMainLooper()) { notifyDataSetChanged(); } else { handler.post(this::notifyDataSetChanged); } } } public interface OnClickTryAgainListener { void loadMoreErrorTryAgain(); } class LoadMoreViewHolder extends RecyclerView.ViewHolder { FrameLayout llLoadMore; TextView tvLoadMore; LoadMoreViewHolder(View itemView) { super(itemView); llLoadMore = itemView.findViewById(R.id.ll_loadmore); tvLoadMore = itemView.findViewById(R.id.tv_loadmore); } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshScrollView.java ================================================ package com.kunfei.bookshelf.widget.recycler.refresh; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.ScrollView; import androidx.annotation.NonNull; public class RefreshScrollView extends ScrollView { private RefreshProgressBar rpb; private float durTouchY = -1000000; private BaseRefreshListener baseRefreshListener; private Boolean isRefreshing = false; public RefreshScrollView(Context context) { this(context, null); } public RefreshScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RefreshScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public RefreshScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public void setRpb(@NonNull RefreshProgressBar rpb) { this.rpb = rpb; init(); } @SuppressLint("ClickableViewAccessibility") private void init() { this.setOnTouchListener((View v, MotionEvent event) -> { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: durTouchY = event.getY(); break; case MotionEvent.ACTION_MOVE: if (durTouchY == -1000000) durTouchY = event.getY(); float dY = event.getY() - durTouchY; //>0下拉 durTouchY = event.getY(); if (baseRefreshListener != null && !isRefreshing && rpb.getSecondDurProgress() == rpb.getSecondFinalProgress() && getScrollY() <= 0) { if (rpb.getVisibility() != View.VISIBLE) { rpb.setVisibility(View.VISIBLE); } rpb.setSecondDurProgress((int) (rpb.getSecondDurProgress() + dY)); return rpb.getSecondDurProgress() > 0; } break; case MotionEvent.ACTION_UP: if (baseRefreshListener != null && rpb.getSecondMaxProgress() > 0 && rpb.getSecondDurProgress() > 0) { if (rpb.getSecondDurProgress() >= rpb.getSecondMaxProgress() && !isRefreshing) { startRefresh(); } else { rpb.setSecondDurProgressWithAnim(0); } } durTouchY = -1000000; break; } return false; }); } public void setBaseRefreshListener(BaseRefreshListener baseRefreshListener) { this.baseRefreshListener = baseRefreshListener; } public void startRefresh() { if (baseRefreshListener != null) { isRefreshing = true; rpb.setIsAutoLoading(true); baseRefreshListener.startRefresh(); } } public void finishRefresh() { isRefreshing = false; rpb.setDurProgress(0); rpb.setIsAutoLoading(false); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/scroller/FastScrollRecyclerView.java ================================================ package com.kunfei.bookshelf.widget.recycler.scroller; import android.content.Context; import android.util.AttributeSet; import android.view.ViewGroup; import android.view.ViewParent; import androidx.annotation.ColorInt; import androidx.recyclerview.widget.RecyclerView; import com.kunfei.bookshelf.R; @SuppressWarnings("unused") public class FastScrollRecyclerView extends RecyclerView { private FastScroller mFastScroller; public FastScrollRecyclerView(Context context) { super(context); layout(context, null); setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); } public FastScrollRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); layout(context, attrs); } @Override public void setAdapter(Adapter adapter) { super.setAdapter(adapter); if (adapter instanceof FastScroller.SectionIndexer) { setSectionIndexer((FastScroller.SectionIndexer) adapter); } else if (adapter == null) { setSectionIndexer(null); } } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); mFastScroller.setVisibility(visibility); } /** * Set the {@link FastScroller.SectionIndexer} for the {@link FastScroller}. * * @param sectionIndexer The SectionIndexer that provides section text for the FastScroller */ public void setSectionIndexer(FastScroller.SectionIndexer sectionIndexer) { mFastScroller.setSectionIndexer(sectionIndexer); } /** * Set the enabled state of fast scrolling. * * @param enabled True to enable fast scrolling, false otherwise */ public void setFastScrollEnabled(boolean enabled) { mFastScroller.setEnabled(enabled); } /** * Hide the scrollbar when not scrolling. * * @param hideScrollbar True to hide the scrollbar, false to show */ public void setHideScrollbar(boolean hideScrollbar) { mFastScroller.setFadeScrollbar(hideScrollbar); } /** * Display a scroll track while scrolling. * * @param visible True to show scroll track, false to hide */ public void setTrackVisible(boolean visible) { mFastScroller.setTrackVisible(visible); } /** * Set the color of the scroll track. * * @param color The color for the scroll track */ public void setTrackColor(@ColorInt int color) { mFastScroller.setTrackColor(color); } /** * Set the color for the scroll handle. * * @param color The color for the scroll handle */ public void setHandleColor(@ColorInt int color) { mFastScroller.setHandleColor(color); } /** * Show the section bubble while scrolling. * * @param visible True to show the bubble, false to hide */ public void setBubbleVisible(boolean visible) { mFastScroller.setBubbleVisible(visible); } /** * Set the background color of the index bubble. * * @param color The background color for the index bubble */ public void setBubbleColor(@ColorInt int color) { mFastScroller.setBubbleColor(color); } /** * Set the text color of the index bubble. * * @param color The text color for the index bubble */ public void setBubbleTextColor(@ColorInt int color) { mFastScroller.setBubbleTextColor(color); } /** * Set the fast scroll state change listener. * * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events */ public void setFastScrollStateChangeListener(FastScrollStateChangeListener fastScrollStateChangeListener) { mFastScroller.setFastScrollStateChangeListener(fastScrollStateChangeListener); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mFastScroller.attachRecyclerView(this); ViewParent parent = getParent(); if (parent instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) parent; viewGroup.addView(mFastScroller); mFastScroller.setLayoutParams(viewGroup); } } @Override protected void onDetachedFromWindow() { mFastScroller.detachRecyclerView(); super.onDetachedFromWindow(); } private void layout(Context context, AttributeSet attrs) { mFastScroller = new FastScroller(context, attrs); mFastScroller.setId(R.id.fast_scroller); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/scroller/FastScrollStateChangeListener.java ================================================ package com.kunfei.bookshelf.widget.recycler.scroller; public interface FastScrollStateChangeListener { /** * Called when fast scrolling begins */ void onFastScrollStart(FastScroller fastScroller); /** * Called when fast scrolling ends */ void onFastScrollStop(FastScroller fastScroller); } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/scroller/FastScroller.java ================================================ package com.kunfei.bookshelf.widget.recycler.scroller; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class FastScroller extends LinearLayout { private static final int sBubbleAnimDuration = 100; private static final int sScrollbarAnimDuration = 300; private static final int sScrollbarHideDelay = 1000; private static final int sTrackSnapRange = 5; @ColorInt private int mBubbleColor; @ColorInt private int mHandleColor; private int mBubbleHeight; private int mHandleHeight; private int mViewHeight; private boolean mFadeScrollbar; private boolean mShowBubble; private SectionIndexer mSectionIndexer; private ViewPropertyAnimator mScrollbarAnimator; private ViewPropertyAnimator mBubbleAnimator; private RecyclerView mRecyclerView; private TextView mBubbleView; private ImageView mHandleView; private ImageView mTrackView; private View mScrollbar; private Drawable mBubbleImage; private Drawable mHandleImage; private Drawable mTrackImage; private FastScrollStateChangeListener mFastScrollStateChangeListener; private Runnable mScrollbarHider = this::hideScrollbar; private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (!mHandleView.isSelected() && isEnabled()) { setViewPositions(getScrollProportion(recyclerView)); } } @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (isEnabled()) { switch (newState) { case RecyclerView.SCROLL_STATE_DRAGGING: getHandler().removeCallbacks(mScrollbarHider); cancelAnimation(mScrollbarAnimator); if (!isViewVisible(mScrollbar)) { showScrollbar(); } break; case RecyclerView.SCROLL_STATE_IDLE: if (mFadeScrollbar && !mHandleView.isSelected()) { getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay); } break; } } } }; public FastScroller(Context context) { super(context); layout(context, null); setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); } public FastScroller(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FastScroller(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); layout(context, attrs); setLayoutParams(generateLayoutParams(attrs)); } @Override public void setLayoutParams(@NonNull ViewGroup.LayoutParams params) { params.width = LayoutParams.WRAP_CONTENT; super.setLayoutParams(params); } public void setLayoutParams(@NonNull ViewGroup viewGroup) { @IdRes int recyclerViewId = mRecyclerView != null ? mRecyclerView.getId() : NO_ID; int marginTop = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top); int marginBottom = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom); if (recyclerViewId == NO_ID) { throw new IllegalArgumentException("RecyclerView must have a view ID"); } if (viewGroup instanceof ConstraintLayout) { ConstraintSet constraintSet = new ConstraintSet(); @IdRes int layoutId = getId(); constraintSet.clone((ConstraintLayout) viewGroup); constraintSet.connect(layoutId, ConstraintSet.TOP, recyclerViewId, ConstraintSet.TOP); constraintSet.connect(layoutId, ConstraintSet.BOTTOM, recyclerViewId, ConstraintSet.BOTTOM); constraintSet.connect(layoutId, ConstraintSet.END, recyclerViewId, ConstraintSet.END); constraintSet.applyTo((ConstraintLayout) viewGroup); ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) getLayoutParams(); layoutParams.setMargins(0, marginTop, 0, marginBottom); setLayoutParams(layoutParams); } else if (viewGroup instanceof CoordinatorLayout) { CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) getLayoutParams(); layoutParams.setAnchorId(recyclerViewId); layoutParams.anchorGravity = GravityCompat.END; layoutParams.setMargins(0, marginTop, 0, marginBottom); setLayoutParams(layoutParams); } else if (viewGroup instanceof FrameLayout) { FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); layoutParams.gravity = GravityCompat.END; layoutParams.setMargins(0, marginTop, 0, marginBottom); setLayoutParams(layoutParams); } else if (viewGroup instanceof RelativeLayout) { RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams(); int endRule = RelativeLayout.ALIGN_END; layoutParams.addRule(RelativeLayout.ALIGN_TOP, recyclerViewId); layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId); layoutParams.addRule(endRule, recyclerViewId); layoutParams.setMargins(0, marginTop, 0, marginBottom); setLayoutParams(layoutParams); } else { throw new IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout"); } updateViewHeights(); } public void setSectionIndexer(SectionIndexer sectionIndexer) { mSectionIndexer = sectionIndexer; } public void attachRecyclerView(RecyclerView recyclerView) { mRecyclerView = recyclerView; if (mRecyclerView != null) { mRecyclerView.addOnScrollListener(mScrollListener); post(() -> { // set initial positions for bubble and handle setViewPositions(getScrollProportion(mRecyclerView)); }); } } public void detachRecyclerView() { if (mRecyclerView != null) { mRecyclerView.removeOnScrollListener(mScrollListener); mRecyclerView = null; } } /** * Hide the scrollbar when not scrolling. * * @param fadeScrollbar True to hide the scrollbar, false to show */ public void setFadeScrollbar(boolean fadeScrollbar) { mFadeScrollbar = fadeScrollbar; mScrollbar.setVisibility(fadeScrollbar ? GONE : VISIBLE); } /** * Show the section bubble while scrolling. * * @param visible True to show the bubble, false to hide */ public void setBubbleVisible(boolean visible) { mShowBubble = visible; } /** * Display a scroll track while scrolling. * * @param visible True to show scroll track, false to hide */ public void setTrackVisible(boolean visible) { mTrackView.setVisibility(visible ? VISIBLE : GONE); } /** * Set the color of the scroll track. * * @param color The color for the scroll track */ public void setTrackColor(@ColorInt int color) { @ColorInt int trackColor = color; if (mTrackImage == null) { Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_track); if (drawable != null) { mTrackImage = DrawableCompat.wrap(drawable); } } DrawableCompat.setTint(mTrackImage, trackColor); mTrackView.setImageDrawable(mTrackImage); } /** * Set the color for the scroll handle. * * @param color The color for the scroll handle */ public void setHandleColor(@ColorInt int color) { mHandleColor = color; if (mHandleImage == null) { Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_handle); if (drawable != null) { mHandleImage = DrawableCompat.wrap(drawable); } } DrawableCompat.setTint(mHandleImage, mHandleColor); mHandleView.setImageDrawable(mHandleImage); } /** * Set the background color of the index bubble. * * @param color The background color for the index bubble */ public void setBubbleColor(@ColorInt int color) { mBubbleColor = color; if (mBubbleImage == null) { Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_bubble); if (drawable != null) { mBubbleImage = DrawableCompat.wrap(drawable); } } DrawableCompat.setTint(mBubbleImage, mBubbleColor); mBubbleView.setBackground(mBubbleImage); } /** * Set the text color of the index bubble. * * @param color The text color for the index bubble */ public void setBubbleTextColor(@ColorInt int color) { mBubbleView.setTextColor(color); } /** * Set the fast scroll state change listener. * * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events */ public void setFastScrollStateChangeListener(FastScrollStateChangeListener fastScrollStateChangeListener) { mFastScrollStateChangeListener = fastScrollStateChangeListener; } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); setVisibility(enabled ? VISIBLE : GONE); } @Override @SuppressLint("ClickableViewAccessibility") public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (event.getX() < mHandleView.getX() - ViewCompat.getPaddingStart(mHandleView)) { return false; } requestDisallowInterceptTouchEvent(true); setHandleSelected(true); getHandler().removeCallbacks(mScrollbarHider); cancelAnimation(mScrollbarAnimator); cancelAnimation(mBubbleAnimator); if (!isViewVisible(mScrollbar)) { showScrollbar(); } if (mShowBubble && mSectionIndexer != null) { showBubble(); } if (mFastScrollStateChangeListener != null) { mFastScrollStateChangeListener.onFastScrollStart(this); } case MotionEvent.ACTION_MOVE: final float y = event.getY(); setViewPositions(y); setRecyclerViewPosition(y); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: requestDisallowInterceptTouchEvent(false); setHandleSelected(false); if (mFadeScrollbar) { getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay); } hideBubble(); if (mFastScrollStateChangeListener != null) { mFastScrollStateChangeListener.onFastScrollStop(this); } return true; } return super.onTouchEvent(event); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mViewHeight = h; } private void setRecyclerViewPosition(float y) { if (mRecyclerView != null && mRecyclerView.getAdapter() != null) { int itemCount = mRecyclerView.getAdapter().getItemCount(); float proportion; if (mHandleView.getY() == 0) { proportion = 0f; } else if (mHandleView.getY() + mHandleHeight >= mViewHeight - sTrackSnapRange) { proportion = 1f; } else { proportion = y / (float) mViewHeight; } int scrolledItemCount = Math.round(proportion * itemCount); if (isLayoutReversed(mRecyclerView.getLayoutManager())) { scrolledItemCount = itemCount - scrolledItemCount; } int targetPos = getValueInRange(0, itemCount - 1, scrolledItemCount); mRecyclerView.getLayoutManager().scrollToPosition(targetPos); if (mShowBubble && mSectionIndexer != null) { mBubbleView.setText(mSectionIndexer.getSectionText(targetPos)); } } } private float getScrollProportion(RecyclerView recyclerView) { if (recyclerView == null) { return 0; } final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset(); final int verticalScrollRange = recyclerView.computeVerticalScrollRange(); final float rangeDiff = verticalScrollRange - mViewHeight; float proportion = (float) verticalScrollOffset / (rangeDiff > 0 ? rangeDiff : 1f); return mViewHeight * proportion; } @SuppressWarnings("SameParameterValue") private int getValueInRange(int min, int max, int value) { int minimum = Math.max(min, value); return Math.min(minimum, max); } private void setViewPositions(float y) { mBubbleHeight = mBubbleView.getHeight(); mHandleHeight = mHandleView.getHeight(); int bubbleY = getValueInRange(0, mViewHeight - mBubbleHeight - mHandleHeight / 2, (int) (y - mBubbleHeight)); int handleY = getValueInRange(0, mViewHeight - mHandleHeight, (int) (y - mHandleHeight / 2)); if (mShowBubble) { mBubbleView.setY(bubbleY); } mHandleView.setY(handleY); } private void updateViewHeights() { int measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); mBubbleView.measure(measureSpec, measureSpec); mBubbleHeight = mBubbleView.getMeasuredHeight(); mHandleView.measure(measureSpec, measureSpec); mHandleHeight = mHandleView.getMeasuredHeight(); } private boolean isLayoutReversed(@NonNull final RecyclerView.LayoutManager layoutManager) { if (layoutManager instanceof LinearLayoutManager) { return ((LinearLayoutManager) layoutManager).getReverseLayout(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { return ((StaggeredGridLayoutManager) layoutManager).getReverseLayout(); } return false; } private boolean isViewVisible(View view) { return view != null && view.getVisibility() == VISIBLE; } private void cancelAnimation(ViewPropertyAnimator animator) { if (animator != null) { animator.cancel(); } } private void showBubble() { if (!isViewVisible(mBubbleView)) { mBubbleView.setVisibility(VISIBLE); mBubbleAnimator = mBubbleView.animate().alpha(1f) .setDuration(sBubbleAnimDuration) .setListener(new AnimatorListenerAdapter() { // adapter required for new alpha value to stick }); } } private void hideBubble() { if (isViewVisible(mBubbleView)) { mBubbleAnimator = mBubbleView.animate().alpha(0f) .setDuration(sBubbleAnimDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mBubbleView.setVisibility(GONE); mBubbleAnimator = null; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); mBubbleView.setVisibility(GONE); mBubbleAnimator = null; } }); } } private void showScrollbar() { if (mRecyclerView.computeVerticalScrollRange() - mViewHeight > 0) { float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end); mScrollbar.setTranslationX(transX); mScrollbar.setVisibility(VISIBLE); mScrollbarAnimator = mScrollbar.animate().translationX(0f).alpha(1f) .setDuration(sScrollbarAnimDuration) .setListener(new AnimatorListenerAdapter() { // adapter required for new alpha value to stick }); } } private void hideScrollbar() { float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end); mScrollbarAnimator = mScrollbar.animate().translationX(transX).alpha(0f) .setDuration(sScrollbarAnimDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mScrollbar.setVisibility(GONE); mScrollbarAnimator = null; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); mScrollbar.setVisibility(GONE); mScrollbarAnimator = null; } }); } private void setHandleSelected(boolean selected) { mHandleView.setSelected(selected); DrawableCompat.setTint(mHandleImage, selected ? mBubbleColor : mHandleColor); } @SuppressWarnings("ConstantConditions") private void layout(Context context, AttributeSet attrs) { inflate(context, R.layout.view_fastscroller, this); setClipChildren(false); setOrientation(HORIZONTAL); mBubbleView = findViewById(R.id.fastscroll_bubble); mHandleView = findViewById(R.id.fastscroll_handle); mTrackView = findViewById(R.id.fastscroll_track); mScrollbar = findViewById(R.id.fastscroll_scrollbar); @ColorInt int bubbleColor = ColorUtils.adjustAlpha(ThemeStore.accentColor(context), 0.8f); @ColorInt int handleColor = ThemeStore.accentColor(context); @ColorInt int trackColor = context.getResources().getColor(R.color.transparent30); @ColorInt int textColor = ColorUtils.isColorLight(bubbleColor) ? Color.BLACK : Color.WHITE; boolean fadeScrollbar = true; boolean showBubble = false; boolean showTrack = true; if (attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, 0, 0); if (typedArray != null) { try { bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor); handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor); trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor); textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor); fadeScrollbar = typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar); showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble); showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack); } finally { typedArray.recycle(); } } } setTrackColor(trackColor); setHandleColor(handleColor); setBubbleColor(bubbleColor); setBubbleTextColor(textColor); setFadeScrollbar(fadeScrollbar); setBubbleVisible(showBubble); setTrackVisible(showTrack); } public interface SectionIndexer { String getSectionText(int position); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/sectioned/GridSpacingItemDecoration.java ================================================ package com.kunfei.bookshelf.widget.recycler.sectioned; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.View; import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; /** * Created by wsw on 2017/12/12. */ public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { private int space; private int color = -1; private Drawable mDivider; private Paint mPaint; private int type; public int getColor() { return color; } public void setColor(@ColorRes int color) { this.color = color; } public GridSpacingItemDecoration(int space) { this.space = space; } public GridSpacingItemDecoration(int space, int color) { this.space = space; this.color = color; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(color); mPaint.setStyle(Paint.Style.FILL); mPaint.setStrokeWidth(space * 2); } public GridSpacingItemDecoration(int space, int color, int type) { this.space = space; this.color = color; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(color); mPaint.setStyle(Paint.Style.FILL); mPaint.setStrokeWidth(space * 2); this.type = type; } public GridSpacingItemDecoration(int space, Drawable mDivider) { this.space = space; this.mDivider = mDivider; } @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { if (parent.getLayoutManager() != null) { if (parent.getLayoutManager() instanceof LinearLayoutManager && !(parent.getLayoutManager() instanceof GridLayoutManager)) { if (((LinearLayoutManager) parent.getLayoutManager()).getOrientation() == LinearLayoutManager.HORIZONTAL) { outRect.set(space, 0, space, 0); } else { outRect.set(0, space, 0, space); } } else { outRect.set(space, space, space, space); } } } @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDraw(c, parent, state); if (parent.getLayoutManager() != null) { if (parent.getLayoutManager() instanceof LinearLayoutManager && !(parent.getLayoutManager() instanceof GridLayoutManager)) { if (((LinearLayoutManager) parent.getLayoutManager()).getOrientation() == LinearLayoutManager.HORIZONTAL) { drawHorizontal(c, parent); } else { drawVertical(c, parent); } } else { if (type == 0) { drawGrideview(c, parent); } else { drawGrideview1(c, parent); } } } } //绘制纵向 item 分割线 private void drawVertical(Canvas canvas, RecyclerView parent) { final int top = parent.getPaddingTop(); final int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom(); final int childSize = parent.getChildCount(); for (int i = 0; i < childSize; i++) { final View child = parent.getChildAt(i); RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); final int left = child.getRight() + layoutParams.rightMargin; final int right = left + space; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } } } //绘制横向 item 分割线 private void drawHorizontal(Canvas canvas, RecyclerView parent) { int left = parent.getPaddingLeft(); int right = parent.getMeasuredWidth() - parent.getPaddingRight(); final int childSize = parent.getChildCount(); for (int i = 0; i < childSize; i++) { final View child = parent.getChildAt(i); RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); int top = child.getBottom() + layoutParams.bottomMargin; int bottom = top + space; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } } } //绘制grideview item 分割线 不是填充满的 private void drawGrideview(Canvas canvas, RecyclerView parent) { GridLayoutManager linearLayoutManager = (GridLayoutManager) parent.getLayoutManager(); int childSize = parent.getChildCount(); assert linearLayoutManager != null; int other = parent.getChildCount() / linearLayoutManager.getSpanCount() - 1; if (other < 1) { other = 1; } other = other * linearLayoutManager.getSpanCount(); if (parent.getChildCount() < linearLayoutManager.getSpanCount()) { other = parent.getChildCount(); } int top, bottom, left, right, spancount; spancount = linearLayoutManager.getSpanCount() - 1; for (int i = 0; i < childSize; i++) { final View child = parent.getChildAt(i); RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); if (i < other) { top = child.getBottom() + layoutParams.bottomMargin; bottom = top + space; left = (layoutParams.leftMargin + space) * (i + 1); right = child.getMeasuredWidth() * (i + 1) + left + space * i; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } } if (i != spancount) { top = (layoutParams.topMargin + space) * (i / linearLayoutManager.getSpanCount() + 1); bottom = (child.getMeasuredHeight() + space) * (i / linearLayoutManager.getSpanCount() + 1) + space; left = child.getRight() + layoutParams.rightMargin; right = left + space; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } } else { spancount += 4; } } } /***/ private void drawGrideview1(Canvas canvas, RecyclerView parent) { GridLayoutManager linearLayoutManager = (GridLayoutManager) parent.getLayoutManager(); int childSize = parent.getChildCount(); int top, bottom, left, right, spancount; spancount = linearLayoutManager.getSpanCount(); for (int i = 0; i < childSize; i++) { final View child = parent.getChildAt(i); //画横线 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); top = child.getBottom() + layoutParams.bottomMargin; bottom = top + space; left = layoutParams.leftMargin + child.getPaddingLeft() + space; right = child.getMeasuredWidth() * (i + 1) + left + space * i; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } //画竖线 top = (layoutParams.topMargin + space) * (i / linearLayoutManager.getSpanCount() + 1); bottom = (child.getMeasuredHeight() + space) * (i / linearLayoutManager.getSpanCount() + 1) + space; left = child.getRight() + layoutParams.rightMargin; right = left + space; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } //画上缺失的线框 if (i < spancount) { top = child.getTop() + layoutParams.topMargin; bottom = top + space; left = (layoutParams.leftMargin + space) * (i + 1); right = child.getMeasuredWidth() * (i + 1) + left + space * i; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } } if (i % spancount == 0) { top = (layoutParams.topMargin + space) * (i / linearLayoutManager.getSpanCount() + 1); bottom = (child.getMeasuredHeight() + space) * (i / linearLayoutManager.getSpanCount() + 1) + space; left = child.getLeft() + layoutParams.leftMargin; right = left + space; if (mDivider != null) { mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } if (mPaint != null) { canvas.drawRect(left, top, right, bottom, mPaint); } } } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/sectioned/SectionedRecyclerViewAdapter.java ================================================ /* * Copyright (C) 2015 Tomás Ruiz-López. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kunfei.bookshelf.widget.recycler.sectioned; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; /** * An extension to RecyclerView.Adapter to provide sections with headers and footers to a * RecyclerView. Each section can have an arbitrary number of items. * * @param Class extending RecyclerView.ViewHolder to hold and bind the header view * @param Class extending RecyclerView.ViewHolder to hold and bind the items view * @param Class extending RecyclerView.ViewHolder to hold and bind the footer view */ @SuppressWarnings("unused") public abstract class SectionedRecyclerViewAdapter extends RecyclerView.Adapter { protected static final int TYPE_SECTION_HEADER = -1; protected static final int TYPE_SECTION_FOOTER = -2; protected static final int TYPE_ITEM = -3; private int[] sectionForPosition = null; private int[] positionWithinSection = null; private boolean[] isHeader = null; private boolean[] isFooter = null; private int count = 0; public SectionedRecyclerViewAdapter() { super(); registerAdapterDataObserver(new SectionDataObserver()); } @Override public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); setupIndices(); } /** * Returns the sum of number of items for each section plus headers and footers if they * are provided. */ @Override public int getItemCount() { return count; } private void setupIndices() { count = countItems(); allocateAuxiliaryArrays(count); precomputeIndices(); } private int countItems() { int count = 0; int sections = getSectionCount(); for (int i = 0; i < sections; i++) { count += 1 + getItemCountForSection(i) + (hasFooterInSection(i) ? 1 : 0); } return count; } private void precomputeIndices() { int sections = getSectionCount(); int index = 0; for (int i = 0; i < sections; i++) { setPrecomputedItem(index, true, false, i, 0); index++; for (int j = 0; j < getItemCountForSection(i); j++) { setPrecomputedItem(index, false, false, i, j); index++; } if (hasFooterInSection(i)) { setPrecomputedItem(index, false, true, i, 0); index++; } } } private void allocateAuxiliaryArrays(int count) { sectionForPosition = new int[count]; positionWithinSection = new int[count]; isHeader = new boolean[count]; isFooter = new boolean[count]; } private void setPrecomputedItem(int index, boolean isHeader, boolean isFooter, int section, int position) { this.isHeader[index] = isHeader; this.isFooter[index] = isFooter; sectionForPosition[index] = section; positionWithinSection[index] = position; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { RecyclerView.ViewHolder viewHolder; if (isSectionHeaderViewType(viewType)) { viewHolder = onCreateSectionHeaderViewHolder(parent, viewType); } else if (isSectionFooterViewType(viewType)) { viewHolder = onCreateSectionFooterViewHolder(parent, viewType); } else { viewHolder = onCreateItemViewHolder(parent, viewType); } return viewHolder; } @SuppressWarnings("unchecked") @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { int section = sectionForPosition[position]; int index = positionWithinSection[position]; if (isSectionHeaderPosition(position)) { onBindSectionHeaderViewHolder((H) holder, section); } else if (isSectionFooterPosition(position)) { onBindSectionFooterViewHolder((F) holder, section); } else { onBindItemViewHolder((VH) holder, section, index); } } @Override public int getItemViewType(int position) { if (sectionForPosition == null) { setupIndices(); } int section = sectionForPosition[position]; int index = positionWithinSection[position]; if (isSectionHeaderPosition(position)) { return getSectionHeaderViewType(section); } else if (isSectionFooterPosition(position)) { return getSectionFooterViewType(section); } else { return getSectionItemViewType(section, index); } } protected int getSectionHeaderViewType(int section) { return TYPE_SECTION_HEADER; } protected int getSectionFooterViewType(int section) { return TYPE_SECTION_FOOTER; } protected int getSectionItemViewType(int section, int position) { return TYPE_ITEM; } /** * Returns true if the argument position corresponds to a header */ public boolean isSectionHeaderPosition(int position) { if (isHeader == null) { setupIndices(); } return isHeader[position]; } /** * Returns true if the argument position corresponds to a footer */ public boolean isSectionFooterPosition(int position) { if (isFooter == null) { setupIndices(); } return isFooter[position]; } protected boolean isSectionHeaderViewType(int viewType) { return viewType == TYPE_SECTION_HEADER; } protected boolean isSectionFooterViewType(int viewType) { return viewType == TYPE_SECTION_FOOTER; } /** * Returns the number of sections in the RecyclerView */ protected abstract int getSectionCount(); /** * Returns the number of items for a given section */ protected abstract int getItemCountForSection(int section); /** * Returns true if a given section should have a footer */ protected abstract boolean hasFooterInSection(int section); /** * Creates a ViewHolder of class H for a Header */ protected abstract H onCreateSectionHeaderViewHolder(ViewGroup parent, int viewType); /** * Creates a ViewHolder of class F for a Footer */ protected abstract F onCreateSectionFooterViewHolder(ViewGroup parent, int viewType); /** * Creates a ViewHolder of class VH for an Item */ protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType); /** * Binds data to the header view of a given section */ protected abstract void onBindSectionHeaderViewHolder(H holder, int section); /** * Binds data to the footer view of a given section */ protected abstract void onBindSectionFooterViewHolder(F holder, int section); /** * Binds data to the item view for a given position within a section */ protected abstract void onBindItemViewHolder(VH holder, int section, int position); class SectionDataObserver extends RecyclerView.AdapterDataObserver { @Override public void onChanged() { setupIndices(); } } public int getItemPosition(int position) { return positionWithinSection[position]; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/recycler/sectioned/SectionedSpanSizeLookup.java ================================================ /* * Copyright (C) 2015 Tomás Ruiz-López. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kunfei.bookshelf.widget.recycler.sectioned; import androidx.recyclerview.widget.GridLayoutManager; /** * A SpanSizeLookup to draw section headers or footer spanning the whole width of the RecyclerView * when using a GridLayoutManager */ public class SectionedSpanSizeLookup extends GridLayoutManager.SpanSizeLookup { protected SectionedRecyclerViewAdapter adapter = null; protected GridLayoutManager layoutManager = null; public SectionedSpanSizeLookup(SectionedRecyclerViewAdapter adapter, GridLayoutManager layoutManager) { this.adapter = adapter; this.layoutManager = layoutManager; } @Override public int getSpanSize(int position) { if (adapter.isSectionHeaderPosition(position) || adapter.isSectionFooterPosition(position)) { return layoutManager.getSpanCount(); } else { return 1; } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/seekbar/VerticalSeekBar.kt ================================================ package com.kunfei.bookshelf.widget.seekbar import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.KeyEvent import android.view.MotionEvent import android.widget.ProgressBar import androidx.appcompat.widget.AppCompatSeekBar import androidx.core.view.ViewCompat import com.kunfei.bookshelf.R import com.kunfei.bookshelf.utils.theme.ATH import com.kunfei.bookshelf.utils.theme.ThemeStore import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method class VerticalSeekBar : AppCompatSeekBar { private var mIsDragging: Boolean = false private var mThumb: Drawable? = null private var mMethodSetProgressFromUser: Method? = null private var mRotationAngle = ROTATION_ANGLE_CW_90 var rotationAngle: Int get() = mRotationAngle set(angle) { require(isValidRotationAngle(angle)) { "Invalid angle specified :$angle" } if (mRotationAngle == angle) { return } mRotationAngle = angle if (useViewRotation()) { val wrapper = wrapper wrapper?.applyViewRotation() } else { requestLayout() } } private val wrapper: VerticalSeekBarWrapper? get() { val parent = parent return if (parent is VerticalSeekBarWrapper) { parent } else { null } } constructor(context: Context) : super(context) { initialize(context, null, 0, 0) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(context, attrs, 0, 0) } constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( context, attrs, defStyle ) { initialize(context, attrs, defStyle, 0) } private fun initialize( context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) { ATH.setTint(this, ThemeStore.accentColor(context)) ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR) if (attrs != null) { val a = context.obtainStyledAttributes( attrs, R.styleable.VerticalSeekBar, defStyleAttr, defStyleRes ) val rotationAngle = a.getInteger(R.styleable.VerticalSeekBar_seekBarRotation, 0) if (isValidRotationAngle(rotationAngle)) { mRotationAngle = rotationAngle } a.recycle() } } override fun setThumb(thumb: Drawable) { mThumb = thumb super.setThumb(thumb) } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { return if (useViewRotation()) { onTouchEventUseViewRotation(event) } else { onTouchEventTraditionalRotation(event) } } private fun onTouchEventTraditionalRotation(event: MotionEvent): Boolean { if (!isEnabled) { return false } when (event.action) { MotionEvent.ACTION_DOWN -> { isPressed = true onStartTrackingTouch() trackTouchEvent(event) attemptClaimDrag(true) invalidate() } MotionEvent.ACTION_MOVE -> if (mIsDragging) { trackTouchEvent(event) } MotionEvent.ACTION_UP -> { if (mIsDragging) { trackTouchEvent(event) onStopTrackingTouch() isPressed = false } else { // Touch up when we never crossed the touch slop threshold // should // be interpreted as a tap-seek to that location. onStartTrackingTouch() trackTouchEvent(event) onStopTrackingTouch() attemptClaimDrag(false) } // ProgressBar doesn't know to repaint the thumb drawable // in its inactive state when the touch stops (because the // value has not apparently changed) invalidate() } MotionEvent.ACTION_CANCEL -> { if (mIsDragging) { onStopTrackingTouch() isPressed = false } invalidate() // see above explanation } } return true } private fun onTouchEventUseViewRotation(event: MotionEvent): Boolean { val handled = super.onTouchEvent(event) if (handled) { when (event.action) { MotionEvent.ACTION_DOWN -> attemptClaimDrag(true) MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> attemptClaimDrag(false) } } return handled } private fun trackTouchEvent(event: MotionEvent) { val paddingLeft = super.getPaddingLeft() val paddingRight = super.getPaddingRight() val height = height val available = height - paddingLeft - paddingRight val y = event.y.toInt() val scale: Float var value = 0f when (mRotationAngle) { ROTATION_ANGLE_CW_90 -> value = (y - paddingLeft).toFloat() ROTATION_ANGLE_CW_270 -> value = (height - paddingLeft - y).toFloat() } scale = if (value < 0 || available == 0) { 0.0f } else if (value > available) { 1.0f } else { value / available.toFloat() } val max = max val progress = scale * max setProgressFromUser(progress.toInt(), true) } /** * Tries to claim the user's drag motion, and requests disallowing any * ancestors from stealing events in the drag. */ private fun attemptClaimDrag(active: Boolean) { val parent = parent parent?.requestDisallowInterceptTouchEvent(active) } /** * This is called when the user has started touching this widget. */ private fun onStartTrackingTouch() { mIsDragging = true } /** * This is called when the user either releases his touch or the touch is * canceled. */ private fun onStopTrackingTouch() { mIsDragging = false } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { if (isEnabled) { val handled: Boolean var direction = 0 when (keyCode) { KeyEvent.KEYCODE_DPAD_DOWN -> { direction = if (mRotationAngle == ROTATION_ANGLE_CW_90) 1 else -1 handled = true } KeyEvent.KEYCODE_DPAD_UP -> { direction = if (mRotationAngle == ROTATION_ANGLE_CW_270) 1 else -1 handled = true } KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT -> // move view focus to previous/next view return false else -> handled = false } if (handled) { val keyProgressIncrement = keyProgressIncrement var progress = progress progress += direction * keyProgressIncrement if (progress in 0..max) { setProgressFromUser(progress, true) } return true } } return super.onKeyDown(keyCode, event) } @Synchronized override fun setProgress(progress: Int) { super.setProgress(progress) if (!useViewRotation()) { refreshThumb() } } @Synchronized private fun setProgressFromUser(progress: Int, fromUser: Boolean) { if (mMethodSetProgressFromUser == null) { try { val m: Method = ProgressBar::class.java.getDeclaredMethod( "setProgress", Int::class.javaPrimitiveType, Boolean::class.javaPrimitiveType ) m.isAccessible = true mMethodSetProgressFromUser = m } catch (e: NoSuchMethodException) { } } if (mMethodSetProgressFromUser != null) { try { mMethodSetProgressFromUser!!.invoke(this, progress, fromUser) } catch (e: IllegalArgumentException) { } catch (e: IllegalAccessException) { } catch (e: InvocationTargetException) { } } else { super.setProgress(progress) } refreshThumb() } @Synchronized override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { if (useViewRotation()) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) } else { super.onMeasure(heightMeasureSpec, widthMeasureSpec) val lp = layoutParams if (isInEditMode && lp != null && lp.height >= 0) { setMeasuredDimension(super.getMeasuredHeight(), lp.height) } else { setMeasuredDimension(super.getMeasuredHeight(), super.getMeasuredWidth()) } } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { if (useViewRotation()) { super.onSizeChanged(w, h, oldw, oldh) } else { super.onSizeChanged(h, w, oldh, oldw) } } @Synchronized override fun onDraw(canvas: Canvas) { if (!useViewRotation()) { when (mRotationAngle) { ROTATION_ANGLE_CW_90 -> { canvas.rotate(90f) canvas.translate(0f, (-super.getWidth()).toFloat()) } ROTATION_ANGLE_CW_270 -> { canvas.rotate(-90f) canvas.translate((-super.getHeight()).toFloat(), 0f) } } } super.onDraw(canvas) } // refresh thumb position private fun refreshThumb() { onSizeChanged(super.getWidth(), super.getHeight(), 0, 0) } /*package*/ internal fun useViewRotation(): Boolean { return !isInEditMode } companion object { const val ROTATION_ANGLE_CW_90 = 90 const val ROTATION_ANGLE_CW_270 = 270 private fun isValidRotationAngle(angle: Int): Boolean { return angle == ROTATION_ANGLE_CW_90 || angle == ROTATION_ANGLE_CW_270 } } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/seekbar/VerticalSeekBarWrapper.kt ================================================ package com.kunfei.bookshelf.widget.seekbar import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.view.ViewCompat import kotlin.math.max class VerticalSeekBarWrapper @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { private val childSeekBar: VerticalSeekBar? get() { val child = if (childCount > 0) getChildAt(0) else null return if (child is VerticalSeekBar) child else null } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { if (useViewRotation()) { onSizeChangedUseViewRotation(w, h, oldw, oldh) } else { onSizeChangedTraditionalRotation(w, h, oldw, oldh) } } @SuppressLint("RtlHardcoded") private fun onSizeChangedTraditionalRotation(w: Int, h: Int, oldw: Int, oldh: Int) { val seekBar = childSeekBar if (seekBar != null) { val hPadding = paddingLeft + paddingRight val vPadding = paddingTop + paddingBottom val lp = seekBar.layoutParams as LayoutParams lp.width = ViewGroup.LayoutParams.WRAP_CONTENT lp.height = max(0, h - vPadding) seekBar.layoutParams = lp seekBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) val seekBarMeasuredWidth = seekBar.measuredWidth seekBar.measure( MeasureSpec.makeMeasureSpec( max(0, w - hPadding), MeasureSpec.AT_MOST ), MeasureSpec.makeMeasureSpec( max(0, h - vPadding), MeasureSpec.EXACTLY ) ) lp.gravity = Gravity.TOP or Gravity.LEFT lp.leftMargin = (max(0, w - hPadding) - seekBarMeasuredWidth) / 2 seekBar.layoutParams = lp } super.onSizeChanged(w, h, oldw, oldh) } private fun onSizeChangedUseViewRotation(w: Int, h: Int, oldw: Int, oldh: Int) { val seekBar = childSeekBar if (seekBar != null) { val hPadding = paddingLeft + paddingRight val vPadding = paddingTop + paddingBottom seekBar.measure( MeasureSpec.makeMeasureSpec( max(0, h - vPadding), MeasureSpec.EXACTLY ), MeasureSpec.makeMeasureSpec( max(0, w - hPadding), MeasureSpec.AT_MOST ) ) } applyViewRotation(w, h) super.onSizeChanged(w, h, oldw, oldh) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val seekBar = childSeekBar val widthMode = MeasureSpec.getMode(widthMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) val widthSize = MeasureSpec.getSize(widthMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) if (seekBar != null && widthMode != MeasureSpec.EXACTLY) { val seekBarWidth: Int val seekBarHeight: Int val hPadding = paddingLeft + paddingRight val vPadding = paddingTop + paddingBottom val innerContentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(max(0, widthSize - hPadding), widthMode) val innerContentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(max(0, heightSize - vPadding), heightMode) if (useViewRotation()) { seekBar.measure(innerContentHeightMeasureSpec, innerContentWidthMeasureSpec) seekBarWidth = seekBar.measuredHeight seekBarHeight = seekBar.measuredWidth } else { seekBar.measure(innerContentWidthMeasureSpec, innerContentHeightMeasureSpec) seekBarWidth = seekBar.measuredWidth seekBarHeight = seekBar.measuredHeight } val measuredWidth = View.resolveSizeAndState(seekBarWidth + hPadding, widthMeasureSpec, 0) val measuredHeight = View.resolveSizeAndState(seekBarHeight + vPadding, heightMeasureSpec, 0) setMeasuredDimension(measuredWidth, measuredHeight) } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec) } } /*package*/ internal fun applyViewRotation() { applyViewRotation(width, height) } private fun applyViewRotation(w: Int, h: Int) { val seekBar = childSeekBar if (seekBar != null) { val isLTR = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR val rotationAngle = seekBar.rotationAngle val seekBarMeasuredWidth = seekBar.measuredWidth val seekBarMeasuredHeight = seekBar.measuredHeight val hPadding = paddingLeft + paddingRight val vPadding = paddingTop + paddingBottom val hOffset = (max(0, w - hPadding) - seekBarMeasuredHeight) * 0.5f val lp = seekBar.layoutParams lp.width = max(0, h - vPadding) lp.height = ViewGroup.LayoutParams.WRAP_CONTENT seekBar.layoutParams = lp seekBar.pivotX = (if (isLTR) 0 else max(0, h - vPadding)).toFloat() seekBar.pivotY = 0f when (rotationAngle) { VerticalSeekBar.ROTATION_ANGLE_CW_90 -> { seekBar.rotation = 90f if (isLTR) { seekBar.translationX = seekBarMeasuredHeight + hOffset seekBar.translationY = 0f } else { seekBar.translationX = -hOffset seekBar.translationY = seekBarMeasuredWidth.toFloat() } } VerticalSeekBar.ROTATION_ANGLE_CW_270 -> { seekBar.rotation = 270f if (isLTR) { seekBar.translationX = hOffset seekBar.translationY = seekBarMeasuredWidth.toFloat() } else { seekBar.translationX = -(seekBarMeasuredHeight + hOffset) seekBar.translationY = 0f } } } } } private fun useViewRotation(): Boolean { val seekBar = childSeekBar return seekBar?.useViewRotation() ?: false } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEAccentBgTextView.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.graphics.Color; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatTextView; import com.kunfei.bookshelf.utils.ColorUtils; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.utils.Selector; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class ATEAccentBgTextView extends AppCompatTextView { public ATEAccentBgTextView(Context context) { super(context); init(context, null); } public ATEAccentBgTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATEAccentBgTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { setBackground(Selector.shapeBuild() .setCornerRadius(ScreenUtils.dpToPx(3)) .setDefaultBgColor(ThemeStore.accentColor(context)) .setPressedBgColor(ColorUtils.darkenColor(ThemeStore.accentColor(context))) .create()); setTextColor(Color.WHITE); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEAccentStrokeTextView.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatTextView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.utils.Selector; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class ATEAccentStrokeTextView extends AppCompatTextView { public ATEAccentStrokeTextView(Context context) { super(context); init(context, null); } public ATEAccentStrokeTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATEAccentStrokeTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { setBackground(Selector.shapeBuild() .setCornerRadius(ScreenUtils.dpToPx(3)) .setStrokeWidth(ScreenUtils.dpToPx(1)) .setDisabledStrokeColor(context.getResources().getColor(R.color.md_grey_500)) .setDefaultStrokeColor(ThemeStore.accentColor(context)) .setPressedBgColor(context.getResources().getColor(R.color.transparent30)) .create()); setTextColor(Selector.colorBuild() .setDefaultColor(ThemeStore.accentColor(context)) .setDisabledColor(context.getResources().getColor(R.color.md_grey_500)) .create()); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEAutoCompleteTextView.java ================================================ package com.kunfei.bookshelf.widget.views; import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import androidx.appcompat.widget.AppCompatAutoCompleteTextView; import com.kunfei.bookshelf.utils.Selector; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class ATEAutoCompleteTextView extends AppCompatAutoCompleteTextView { public ATEAutoCompleteTextView(Context context) { super(context); init(context); } public ATEAutoCompleteTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public ATEAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setBackgroundTintList(Selector.colorBuild() .setFocusedColor(ThemeStore.accentColor(context)) .setDefaultColor(ThemeStore.textColorPrimary(context)) .create()); } } @Override public boolean enoughToFilter() { return true; } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { showDropDown(); } return super.onTouchEvent(event); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATECheckBox.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatCheckBox; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATECheckBox extends AppCompatCheckBox { public ATECheckBox(Context context) { super(context); init(context, null); } public ATECheckBox(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATECheckBox(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { ATH.setTint(this, ThemeStore.accentColor(context)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEEditText.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatEditText; /** * @author Aidan Follestad (afollestad) */ public class ATEEditText extends AppCompatEditText { public ATEEditText(Context context) { super(context); init(context, null); } public ATEEditText(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATEEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { //ATH.setTint(this, ThemeStore.accentColor(context)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEPrimaryTextView.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatTextView; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATEPrimaryTextView extends AppCompatTextView { public ATEPrimaryTextView(Context context) { super(context); init(context, null); } public ATEPrimaryTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATEPrimaryTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { setTextColor(ThemeStore.textColorPrimary(context)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEProgressBar.java ================================================ package com.kunfei.bookshelf.widget.views; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.util.AttributeSet; import android.widget.ProgressBar; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATEProgressBar extends ProgressBar { public ATEProgressBar(Context context) { super(context); init(context, null); } public ATEProgressBar(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATEProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ATEProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } private void init(Context context, AttributeSet attrs) { ATH.setTint(this, ThemeStore.accentColor(context)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATERadioButton.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatRadioButton; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATERadioButton extends AppCompatRadioButton { public ATERadioButton(Context context) { super(context); init(context, null); } public ATERadioButton(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATERadioButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { ATH.setTint(this, ThemeStore.accentColor(context)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATERadioNoButton.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.graphics.Color; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatRadioButton; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.utils.Selector; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATERadioNoButton extends AppCompatRadioButton { public ATERadioNoButton(Context context) { super(context); init(context, null); } public ATERadioNoButton(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATERadioNoButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { setBackground(Selector.shapeBuild() .setCornerRadius(ScreenUtils.dpToPx(3)) .setStrokeWidth(ScreenUtils.dpToPx(1)) .setCheckedBgColor(ThemeStore.accentColor(context)) .setCheckedStrokeColor(ThemeStore.accentColor(context)) .setDefaultStrokeColor(Color.WHITE) .create()); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATESecondaryTextView.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatTextView; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATESecondaryTextView extends AppCompatTextView { public ATESecondaryTextView(Context context) { super(context); init(context, null); } public ATESecondaryTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATESecondaryTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { setTextColor(ThemeStore.textColorSecondary(context)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATESeekBar.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatSeekBar; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATESeekBar extends AppCompatSeekBar { public ATESeekBar(Context context) { super(context); init(context, null); } public ATESeekBar(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATESeekBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { ATH.setTint(this, ThemeStore.accentColor(context)); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEStockSwitch.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.widget.Switch; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATEStockSwitch extends Switch { public ATEStockSwitch(Context context) { super(context); init(context, null); } public ATEStockSwitch(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATEStockSwitch(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { ATH.setTint(this, ThemeStore.accentColor(context)); } @Override public boolean isShown() { return getParent() != null && getVisibility() == View.VISIBLE; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATEStrokeTextView.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import androidx.appcompat.widget.AppCompatTextView; import com.kunfei.bookshelf.R; import com.kunfei.bookshelf.utils.ScreenUtils; import com.kunfei.bookshelf.utils.Selector; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class ATEStrokeTextView extends AppCompatTextView { public ATEStrokeTextView(Context context) { super(context); init(context, null); } public ATEStrokeTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATEStrokeTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ATEStrokeTextView); setBackground(Selector.shapeBuild() .setCornerRadius(a.getDimensionPixelSize(R.styleable.ATEStrokeTextView_cornerRadius, 1)) .setStrokeWidth(ScreenUtils.dpToPx(1)) .setDisabledStrokeColor(context.getResources().getColor(R.color.md_grey_500)) .setDefaultStrokeColor(ThemeStore.textColorSecondary(context)) .setSelectedStrokeColor(ThemeStore.accentColor(context)) .setPressedBgColor(context.getResources().getColor(R.color.transparent30)) .create()); setTextColor(Selector.colorBuild() .setDefaultColor(ThemeStore.textColorSecondary(context)) .setSelectedColor(ThemeStore.accentColor(context)) .setDisabledColor(context.getResources().getColor(R.color.md_grey_500)) .create()); } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATESwitch.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.widget.Switch; import com.kunfei.bookshelf.utils.theme.ATH; import com.kunfei.bookshelf.utils.theme.ThemeStore; /** * @author Aidan Follestad (afollestad) */ public class ATESwitch extends Switch { public ATESwitch(Context context) { super(context); init(context, null); } public ATESwitch(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ATESwitch(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { ATH.setTint(this, ThemeStore.accentColor(context)); } @Override public boolean isShown() { return getParent() != null && getVisibility() == View.VISIBLE; } } ================================================ FILE: app/src/main/java/com/kunfei/bookshelf/widget/views/ATETextInputLayout.java ================================================ package com.kunfei.bookshelf.widget.views; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import androidx.annotation.Nullable; import com.google.android.material.textfield.TextInputLayout; import com.kunfei.bookshelf.utils.Selector; import com.kunfei.bookshelf.utils.theme.ThemeStore; public class ATETextInputLayout extends TextInputLayout { public ATETextInputLayout(Context context) { super(context); init(context); } public ATETextInputLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } public ATETextInputLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { setHintTextColor(Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create()); } @Override public void draw(Canvas canvas) { super.draw(canvas); } } ================================================ FILE: app/src/main/res/anim/anim_none.xml ================================================ ================================================ FILE: app/src/main/res/anim/anim_readbook_bottom_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/anim_readbook_bottom_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/anim_readbook_top_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/anim_readbook_top_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_bottom_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_bottom_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_in_bottom_right.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_in_top_right.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_out_bottom_right.xml ================================================ ================================================ FILE: app/src/main/res/anim/moprogress_out_top_right.xml ================================================ ================================================ FILE: app/src/main/res/color/selector_menu_text.xml ================================================ ================================================ FILE: app/src/main/res/drawable/bg_chapter_item_divider.xml ================================================ ================================================ FILE: app/src/main/res/drawable/bg_edit.xml ================================================ ================================================ FILE: app/src/main/res/drawable/bg_ib_pre.xml ================================================ ================================================ FILE: app/src/main/res/drawable/bg_ib_pre_round.xml ================================================ ================================================ FILE: app/src/main/res/drawable/bg_textfield_search.xml ================================================ ================================================ FILE: app/src/main/res/drawable/fastscroll_bubble.xml ================================================ ================================================ FILE: app/src/main/res/drawable/fastscroll_handle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/fastscroll_track.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_about.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_online.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrange.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_back.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_drop_down.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_drop_up.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_author.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_auto_page.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_auto_page_stop.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_back_last.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_backup.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_label.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_book_has.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_book_last.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_book_source_manage.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_brightness.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bug_report_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_cancel.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_chapter_list.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_check.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_check_source.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_clear_all.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_copy.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_cursor_left.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_cursor_right.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_daytime.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_disclaimer.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_donate.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_download.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_download_line.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_edit.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_exchange.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_expand_less_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_expand_more_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_faq.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_find_replace.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_folder.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_format_line_spacing.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_groups.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_history.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_import.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_interface_setting.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_last_read.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launch.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_list.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_mail.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_more_vert.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_network_check.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_pause_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_pause_outline_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_play_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_play_outline_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_qq_group.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_read.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_read_aloud.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_refresh_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_refresh_white_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_remove.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_restore.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_save.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_scan.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_scoring.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_search.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_select_all.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_settings.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_share.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_skip_next.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_skip_previous.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_stop_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_swap_outline_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_theme.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_time_add_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_timer_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_toc.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_top_source.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_translate.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_tune.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_update.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_version.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_view_quilt.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_volume_up.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_web_outline.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_web_service_noti.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_web_service_phone.xml ================================================ ================================================ FILE: app/src/main/res/drawable/image_welcome.xml ================================================ ================================================ FILE: app/src/main/res/drawable/searchview_line.xml ================================================ ================================================ FILE: app/src/main/res/drawable/selector_common_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/selector_fillet_btn_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/selector_tv_black.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_card_view.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_fillet_btn.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_fillet_btn_press.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_pop_checkaddshelf_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_radius_1dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_space_divider.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_text_cursor.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v21/bg_ib_pre.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v21/bg_ib_pre_round.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_about.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_book_choice.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_book_cover_edit.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_book_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_book_info_edit.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_book_read.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_book_source.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_chapterlist.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_donate.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_import_book.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_qrcode_capture.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_read_style.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_recycler_vew.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_search_book.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_settings.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_source_debug.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_source_edit.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_source_login.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_update.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_web_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_welcome.xml ================================================ ================================================ FILE: app/src/main/res/layout/content_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_bookmark.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_change_source.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_download_choice.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_file_chooser.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_input.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_login.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_number_picker.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_page_key.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_replace_rule.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_txt_chpater_rule.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_book_find.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_book_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_bookmark_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_chapter_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_file_category.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_local_book.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_1line_text_and_del.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_book_source.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_bookshelf_grid.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_bookshelf_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_change_cover.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_change_source.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_chapter_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_download.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_file.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_file_filepicker.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_find1_group.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_find1_kind.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_find2_childer_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_find2_header_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_find_left.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_font.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_icon_preference.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_path_filepicker.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_read_bg.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_replace_rule.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_search_book.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_search_history.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_source_debug.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_source_edit.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_text.xml ================================================ ================================================ FILE: app/src/main/res/layout/mo_dialog_image_text.xml ================================================ ================================================ FILE: app/src/main/res/layout/mo_dialog_infor.xml ================================================ ================================================ FILE: app/src/main/res/layout/mo_dialog_loading.xml ================================================ ================================================ FILE: app/src/main/res/layout/mo_dialog_markdown.xml ================================================ ================================================ FILE: app/src/main/res/layout/mo_dialog_text_large.xml ================================================ ================================================ FILE: app/src/main/res/layout/mo_dialog_two.xml ================================================ ================================================ FILE: app/src/main/res/layout/navigation_header.xml ================================================ ================================================ FILE: app/src/main/res/layout/pop_media_player.xml ================================================ ================================================ FILE: app/src/main/res/layout/pop_more_setting.xml ================================================ ================================================ FILE: app/src/main/res/layout/pop_read_adjust.xml ================================================ ================================================ FILE: app/src/main/res/layout/pop_read_adjust_margin.xml ================================================ ================================================ FILE: app/src/main/res/layout/pop_read_interface.xml ================================================ ================================================ FILE: app/src/main/res/layout/pop_read_long_press.xml ================================================ ================================================ FILE: app/src/main/res/layout/pop_read_menu.xml ================================================ ================================================ FILE: app/src/main/res/layout/popup_keyboard_tool.xml ================================================ ================================================ FILE: app/src/main/res/layout/tab_view_icon_right.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_empty.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_fastscroller.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_file_picker.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_icon.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_loading.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_net_error.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_night_theme.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_number_buttom.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_recycler_font.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_refresh_error.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_refresh_load_more.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_refresh_no_data.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_refresh_recycler.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_book_download.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_book_info.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_book_read_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_book_search_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_book_source_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_book_source_edit.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_debug_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_main_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_main_drawer.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_qr_code_scan.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_read_style_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_replace_rule_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_search_view.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_source_login.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_txt_chapter_rule_activity.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_update_activity.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/book_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/book_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/arrays.xml ================================================ @string/layout_list @string/layout_grid3 @string/layout_grid4 @string/indent_0 @string/indent_1 @string/indent_2 @string/indent_3 @string/indent_4 @string/all_book @string/pursue_more_book @string/fattening_book @string/finish_book @string/local_book .txt .json .xml @string/jf_convert_o @string/jf_convert_j @string/jf_convert_f 自动 黑色 白色 跟随背景 默认 1分钟 2分钟 3分钟 常亮 0 60 120 180 -1 @string/screen_unspecified @string/screen_portrait @string/screen_landscape @string/screen_sensor @string/bookshelf_px_0 @string/bookshelf_px_1 @string/bookshelf_px_2 0 1 2 @string/sys_folder_picker @string/app_folder_picker @string/default_path icon1 icon2 @string/icon_main @string/icon_book ================================================ FILE: app/src/main/res/values/attrs.xml ================================================ ================================================ FILE: app/src/main/res/values/book_launcher_background.xml ================================================ #F9F9F9 ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #eb4333 #439b53 #00000000 @color/md_grey_100 #dedede #fcfcfc #00000000 #30000000 @color/md_grey_100 @color/md_grey_200 @color/md_grey_900 #d3321b #80C0C0C0 #80858585 #88000000 #737373 #adadad #de000000 #b2000000 #8a000000 #dfdfdf #383838 #efefef #23000000 #EEEEEE #aaaaaaaa #FFD4D4D4 #64000000 #f4f4f4 #99343434 #000000 #ffffff #1F000000 #1F000000 #43000000 #43000000 #43000000 #61000000 #8A000000 #8A000000 #8A000000 #DE000000 #FFFAFAFA #FFBDBDBD #E8E8E8 #1AFFFFFF #1F000000 #4DFFFFFF #4DFFFFFF #4DFFFFFF #4DFFFFFF #B3FFFFFF #B3FFFFFF #B3FFFFFF #FFFFFFFF #FFBDBDBD #FF424242 #202020 ================================================ FILE: app/src/main/res/values/colors_material_design.xml ================================================ @color/md_grey_50 #1F000000 #61000000 #8A000000 #8A000000 #DE000000 @color/md_grey_300 @color/md_grey_100 @color/md_white_1000 @color/md_white_1000 @color/md_grey_850 #1FFFFFFF #4DFFFFFF #B3FFFFFF #B3FFFFFF #FFFFFFFF @color/md_black_1000 @color/md_grey_900 @color/md_grey_800 @color/md_grey_800 #FFEBEE #FFCDD2 #EF9A9A #E57373 #EF5350 #F44336 #E53935 #D32F2F #C62828 #B71C1C #FF8A80 #FF5252 #FF1744 #D50000 #FCE4EC #F8BBD0 #F48FB1 #F06292 #EC407A #E91E63 #D81B60 #C2185B #AD1457 #880E4F #FF80AB #FF4081 #F50057 #C51162 #F3E5F5 #E1BEE7 #CE93D8 #BA68C8 #AB47BC #9C27B0 #8E24AA #7B1FA2 #6A1B9A #4A148C #EA80FC #E040FB #D500F9 #AA00FF #EDE7F6 #D1C4E9 #B39DDB #9575CD #7E57C2 #673AB7 #5E35B1 #512DA8 #4527A0 #311B92 #B388FF #7C4DFF #651FFF #6200EA #E8EAF6 #C5CAE9 #9FA8DA #7986CB #5C6BC0 #3F51B5 #3949AB #303F9F #283593 #1A237E #8C9EFF #536DFE #3D5AFE #304FFE #E3F2FD #BBDEFB #90CAF9 #64B5F6 #42A5F5 #2196F3 #1E88E5 #1976D2 #1565C0 #0D47A1 #82B1FF #448AFF #2979FF #2962FF #E1F5FE #B3E5FC #81D4FA #4FC3F7 #29B6F6 #03A9F4 #039BE5 #0288D1 #0277BD #01579B #80D8FF #40C4FF #00B0FF #0091EA #E0F7FA #B2EBF2 #80DEEA #4DD0E1 #26C6DA #00BCD4 #00ACC1 #0097A7 #00838F #006064 #84FFFF #18FFFF #00E5FF #00B8D4 #E0F2F1 #B2DFDB #80CBC4 #4DB6AC #26A69A #009688 #00897B #00796B #00695C #004D40 #A7FFEB #64FFDA #1DE9B6 #00BFA5 #E8F5E9 #C8E6C9 #A5D6A7 #81C784 #66BB6A #4CAF50 #43A047 #388E3C #2E7D32 #1B5E20 #B9F6CA #69F0AE #00E676 #00C853 #F1F8E9 #DCEDC8 #C5E1A5 #AED581 #9CCC65 #8BC34A #7CB342 #689F38 #558B2F #33691E #CCFF90 #B2FF59 #76FF03 #64DD17 #F9FBE7 #F0F4C3 #E6EE9C #DCE775 #D4E157 #CDDC39 #C0CA33 #AFB42B #9E9D24 #827717 #F4FF81 #EEFF41 #C6FF00 #AEEA00 #FFFDE7 #FFF9C4 #FFF59D #FFF176 #FFEE58 #FFEB3B #FDD835 #FBC02D #F9A825 #F57F17 #FFFF8D #FFFF00 #FFEA00 #FFD600 #FFF8E1 #FFECB3 #FFE082 #FFD54F #FFCA28 #FFC107 #FFB300 #FFA000 #FF8F00 #FF6F00 #FFE57F #FFD740 #FFC400 #FFAB00 #FFF3E0 #FFE0B2 #FFCC80 #FFB74D #FFA726 #FF9800 #FB8C00 #F57C00 #EF6C00 #E65100 #FFD180 #FFAB40 #FF9100 #FF6D00 #FBE9E7 #FFCCBC #FFAB91 #FF8A65 #FF7043 #FF5722 #F4511E #E64A19 #D84315 #BF360C #FF9E80 #FF6E40 #FF3D00 #DD2C00 #EFEBE9 #D7CCC8 #BCAAA4 #A1887F #8D6E63 #795548 #6D4C41 #5D4037 #4E342E #3E2723 #FAFAFA #F5F5F5 #EEEEEE #E0E0E0 #BDBDBD #9E9E9E #757575 #616161 #424242 #303030 #212121 #ECEFF1 #CFD8DC #B0BEC5 #90A4AE #78909C #607D8B #546E7A #455A64 #37474F #263238 #000000 #FFFFFF ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 16dp 16dp 24dp 0.8dp 10dp 18sp 4dp 44dp 88dp 48sp 16dp 40dp 8dp 0dp 2dp 8dp 8dp 8dp 8dp ================================================ FILE: app/src/main/res/values/ic_launcher_background.xml ================================================ #4f4f4f ================================================ FILE: app/src/main/res/values/ids.xml ================================================ ================================================ FILE: app/src/main/res/values/pref_key_value.xml ================================================ auto_refresh list_screen_direction full_screen threads_num user_agent bookshelf_px read_type expandGroupFind defaultToRead autoDownload downloadPath checkUpdate search_result_filter_grade ic_launcher_round book_launcher_round https://gedoor.github.io/MyBookshelf/sourcerule.html https://github.com/gedoor/MyBookshelf https://gedoor.github.io/MyBookshelf/disclaimer.html https://gedoor.github.io/MyBookshelf/ https://github.com/gedoor/MyBookshelf/releases/latest https://api.github.com/repos/gedoor/MyBookshelf/releases/latest 🔒%s 🔓%s ================================================ FILE: app/src/main/res/values/strings.xml ================================================ 阅读 书架 最后阅读 让阅读成为一种习惯。 更新日志 阅读·搜索 书架还空着,先去添加吧! 搜索 下载任务 书架布局 书城 添加本地 书源管理 设置 主题设置 关于 捐赠 退出 还未保存,是否继续编辑 阅读样式设置 打开侧边栏 关闭侧边栏 提示 取消 确定 去设置 无法跳转至设置界面 本地 搜索 没有网络 网络连接超时 数据解析失败 来源: %s 最近: %s 书名 最新: %s 是否将《%s》放入书架? 共%s个Text文件 加载中… 获取数据失败! 重试 web服务 web编辑书源 http://%s:%d 正在启动服务 正在启动服务\n具体信息查看通知栏 服务已启动 离线下载 离线下载 下载选择的章节到本地 换源 \u3000\u3000这是一款开源的阅读软件,你可以fork我们的代码自己编译APK。欢迎提交代码帮助改善应用。\n\u3000\u3000公众号[开源阅读]! Version %s 自动刷新 打开软件时自动更新书籍 自动下载最新章节 更新书籍时自动下载最新章节 备份 恢复 备份请给与存储权限 恢复请给与存储权限 确认 取消 确认备份吗? 新备份会替换原有备份。\n备份文件夹YueDu 确认恢复吗? 恢复书架会覆盖现有书架。 备份成功 备份失败 正在恢复 恢复成功 恢复失败 屏幕方向 跟随传感器 横向 竖向 跟随系统 免责声明 共%d章 界面 亮度 目录 下一章 上一章 隐藏状态栏 阅读界面隐藏状态栏 阅读行数调整 阅读行数减一行,如阅读界面显示不全可启用 朗读 正在朗读 点击打开阅读界面 返回 重新加载 开始 停止 暂停 继续 定时 朗读暂停 正在朗读(还剩%d分钟) "正在朗读(还剩%d小时%d分)" %d分钟 "%d小时%d分" "计时已取消" 阅读界面隐藏虚拟按键 隐藏导航栏 导航栏颜色 GitHub 评分 发送邮件 无法打开 分享失败 无章节 添加网址 添加书籍网址 背景 作者 站点暂时不支持解析,请反馈 朗读停止 清除缓存 保存 编辑书源 禁用书源 新建书源 添加书籍 扫描 拷贝书源 拷贝书源无发现 粘贴书源 书源规则说明 检查更新 扫描二维码 扫描本地图片 规则说明 分享 软件分享 跟随系统 添加 导入书源 本地导入 网络导入 替换净化 替换规则编辑 替换规则 替换为 封面 音量键翻页 点击翻页 点击总是翻下一页 翻页动画 屏幕超时 返回 菜单 调节 滚动条 清除缓存会删除所有已保存章节,是否确认删除? 书源共享 替换规则名称 全选 夜间模式 启动页 开始下载 取消下载 暂无任务 导入选择书籍 更新和搜索线程数,如感觉卡顿请减小线程数,量力而行 切换图标 删除书籍 开始阅读 加载数据中… 加载失败,点击重试 内容简介 打开外部书籍 来源 本地导入 网络导入 书架排序 检查更新间隔 按阅读时间排序 按更新时间排序 手动排序 阅读方式 删除所选 是否确认删除? 默认字体 发现 发现管理 没有内容,去书源里自定义吧! 删除所有 搜索历史 清除 正文显示标题 书源同步 无最新章节信息 显示时间和电量 长按选择文本 显示分隔线 深色状态栏图标 内容 拷贝内容 一键缓存 这是一段测试文字\n\u3000\u3000只是让你看看效果的 文字颜色和背景(长按自定义) 沉浸式状态栏 还剩%d章未下载 长按输入颜色值 加载中… 追更区 养肥区 书签 添加书签 删除 加载超时 关注:%s 已拷贝 整理书架 这将会删除所有书籍,请谨慎操作。 搜索书源 搜索(共%d个书源) 目录(%d) 加粗 字体 文字 软件主页 边距: 上边距 下边距 左边距 右边距 校验书源 校验所选 进度 %d/%d 请安装并选择中文TTS! TTS初始化失败! 简繁转换 关闭 简转繁 繁转简 翻页模式 %1$d 项 存储卡 加入书架 加入书架(%1$d) 成功添加%1$d本书 请将字体文件放到SD根目录Fonts文件夹下重新选择 默认字体 选择字体 字号 行距 段距 编辑 删除 置顶 自动展开发现 默认展开第一组发现 当前线程数 %s 朗读语速 自动翻页 停止自动翻页 自动翻页时每分钟阅读字数(CPM) 书籍信息 默认打开书架 自动跳转最近阅读 替换范围,选填书名或者源名 分组 备份路径 内容缓存路径 系统文件选择器 新版本 下载更新 朗读时音量键翻页 Tip边距跟随边距调整 禁止更新 允许更新 反转选择 搜索书名、作者 书名、作者、URL 常见问题 显示所有发现 关闭则只显示勾选源的发现 更新目录 txt目录正则 设置编码 倒序-顺序 排序 智能排序 手动排序 拼音排序 滚动到顶部 滚动到底部 已读: %s 追更 养肥 完结 所有书籍 追更书籍 养肥书籍 完结书籍 本地书籍 状态栏颜色透明 导航栏变色 导航栏根据夜间模式变化 放入书架 继续阅读 封面地址 覆盖 仿真 滑动 滚动 无动画 此书源使用了高级功能,复制支付宝红包搜索码领取红包或关注微信公众号[开源阅读]开启。 后台更新换源最新章节 开启则会在软件打开1分钟后开始更新 书架ToolBar自动隐藏 滚动书架时ToolBar自动隐藏与显示 登录 登录%s 成功 当前源没有配置登陆地址 书源名称(bookSourceName) 书源URL(bookSourceUrl) 书源分组(bookSourceGroup) 登录URL(loginUrl) 作者规则(ruleBookAuthor) 正文规则(ruleBookContent) 正文替换规则(ruleBookContentReplace) 书名规则(ruleBookName) 目录列表规则(ruleChapterList) 章节名称规则(ruleChapterName) 目录URL规则(ruleChapterUrl) 目录下一页规则(ruleChapterUrlNext) 章节URL规则(ruleContentUrl) 封面规则(ruleCoverUrl) 简介规则(ruleIntroduce) 搜索作者规则(ruleSearchAuthor) 发现作者规则(ruleFindAuthor) 搜索封面规则(ruleSearchCoverUrl) 发现封面规则(ruleFindCoverUrl) 搜索分类规则(ruleSearchKind) 发现分类规则(ruleFindKind) 搜索最新章节规则(ruleSearchLastChapter) 发现最新章节规则(ruleFindLastChapter) 搜索列表规则(ruleSearchList) 发现列表规则(ruleFindList) 搜索书名规则(ruleSearchName) 发现书名规则(ruleFindName) 搜索书籍URL规则(ruleSearchNoteUrl) 发现书籍URL规则(ruleFindNoteUrl) 搜索简介规则(ruleSearchIntroduce) 发现简介规则(ruleFindIntroduce) 搜索地址(ruleSearchUrl) 发现规则(ruleFindUrl) 正文下一页URL规则(ruleContentUrlNext) 书籍详情URL正则(ruleBookUrlPattern) 书籍详情预处理规则(ruleBookInfoInit) 分类规则(ruleBookKind) 最新章节规则(ruleBookLastChapter) HttpUserAgent 调试书源 二维码导入 扫描二维码 选中时点击可弹出菜单 主题 默认主题 恢复主题为默认配色 加入QQ群 文件读取失败 获取背景图片需存储权限 输入书源网址 删除文件 删除文件成功 确定删除文件吗? 手机目录 智能导入 发现 切换显示样式 导入本地书籍需存储权限 点击可切换到白天模式 点击可切换到夜间模式 本软件需要存储权限来存储备份书籍信息 再按一次退出程序 导入本地书籍需存储权限 网络连接不可用 是否删除全部书籍? 是否同时删除已下载的书籍目录? 扫描二维码需相机权限 朗读正在运行,不能自动翻页 输入编码 TXT目录规则 打开外部书籍需获取存储权限 书籍信息获取失败 内容获取失败 目录获取失败 访问网站失败:%s 未获取到书名 输入替换规则网址 搜索列表获取成功%d 书源名称和URL不能为空 图库 领支付宝红包 没有获取到更新地址 正在打开首页,成功自动返回主界面 登录成功后请点击右上角图标进行首页访问测试 章节: 使用正则表达式 缩进 无缩进 一字符缩进 二字符缩进 三字符缩进 四字符缩进 选择文件夹 选择SD卡 没有发现,可以在书源里添加。 恢复默认 自定义缓存路径需要存储权限 黑色 文章内容为空 正在换源请等待… 目录列表为空 加载失败\n%s 正文边距 Tip边距 字距 默认路径 系统文件夹选择器 自带选择器\n(Android10以上因权限限制可能无法使用) Android10以上因权限限制可能无法读写文件 E-Ink 模式 去除动画,优化电纸书使用体验 Web服务 web端口 当前端口 %s 二维码分享 字符串分享 wifi分享 请给于存储权限 上一个 下一个 音乐 音频 启用 全部书源 输入不能为空 清空发现缓存 编辑发现 切换软件显示在桌面的图标 显示勾选 扩展到刘海 列表视图 网格视图三列 网格视图四列 封面换源 选择本地图片 刷新封面 自定义翻页按键 自定义翻页按键 上一页按键 下一页按键 搜索结果过滤等级 调高过滤等级可以避免看到书名/作者无关的搜索结果。 等级1-9:搜索关键字需要出现到书名/作者中,但是允许有8-0个字(空格不计)没有出现(避免误输入的影响)。默认等级0,不过滤。推荐等级7,允许2个错別字。 \n当前过滤等级 %s 自动重分段落 分享书籍 扫二维码 复制网址 标记所选发现源 广告话术 编辑广告话术规则 添加广告话术规则 广告话术功能使用了自动优化的算法,不需要使用正则表达式,只需要多次标记不想看的文字,就可以获得较好的广告替换效果。同一个网站的广告话术自动保存在一个规则内,避免了“替换净化”列表里有太多多的规则。 登录UI(loginUi) 登录检查Js(loginCheckJs) VIP标识(ruleVip) 购买标识(rulePay) 购买 ================================================ FILE: app/src/main/res/values/strings_me.xml ================================================ 默认书源 默认书源导入 没有可用的默认书源 单倍 双倍 三倍 默认 自定义间距 行间距 段间距 字间距 上边距 下边距 左边距 右边距 上Tip 下Tip 左Tip 右Tip 55dp 45dp 25dp 13sp ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/values-en/strings.xml ================================================ 登录UI(loginUi) 登录检查Js(loginCheckJs) VIP标识(ruleVip) 购买标识(rulePay) 购买 ================================================ FILE: app/src/main/res/values-night/colors.xml ================================================ @color/md_grey_800 #353535 #282828 #69000000 @color/md_grey_800 @color/md_grey_900 @color/md_grey_100 #30ffffff #363636 #80696969 #80686868 #88111111 #66666666 #737373 #565656 #ffffffff #e5ffffff #b3ffffff #b7b7b7 #303030 #222222 ================================================ FILE: app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: app/src/main/res/values-v27/styles.xml ================================================ ================================================ FILE: app/src/main/res/values-v28/styles.xml ================================================ ================================================ FILE: app/src/main/res/values-zh-rCN/strings.xml ================================================ 登录UI(loginUi) 登录检查Js(loginCheckJs) VIP标识(ruleVip) 购买标识(rulePay) 购买 ================================================ FILE: app/src/main/res/values-zh-rTW/strings.xml ================================================ 閱讀 書架 最後閱讀 讓閱讀成為一種習慣。 更新日誌 閱讀·搜尋 書架還空著,先去添加吧! 搜尋 下載任務 書架佈局 書庫 添加本機 書源管理 設定 主題設定 關於 捐贈 退出 還未保存,是否繼續編輯 閱讀樣式設定 打開側邊欄 關閉側邊欄 提示 取消 確定 去設定 無法跳轉至設定畫面 本機 搜尋 沒有網路 網路連線逾時 數據解析失敗 來源: %s 最近: %s 書名 最新: %s 是否將《%s》放入書架? 共%s個Text文件 加載中… 獲取數據失敗! 重試 web服務 web編輯書源 http://%s:%d 離線下載 離線下載 下載選擇的章節到本地 換源 \u3000\u3000這是一款開源的閱讀應用,你可以fork我們的代碼自己編譯APK。 Version %s 自動刷新 打開應用時自動更新書籍 自動下載最新章節 更新書籍時自動下載最新章節 備份 復原 備份請授予儲存權限 復原請授予儲存權限 確認 取消 確認備份嗎? 新備份會替換原有備份。 確認復原嗎? 復原書架會覆蓋現有書架。 備份成功 備份失敗 正在復原 復原成功 復原失敗 熒幕方向 跟隨傳感器 橫向 直向 跟隨系統 免責聲明 共%d章 界面 亮度 目錄 下一章 上一章 隱藏狀態欄 閱讀畫面隱藏狀態欄 閱讀行數調整 閱讀行數減一行,如閱讀畫面顯示不全可啟用 朗讀 正在朗讀 點擊打開閱讀畫面 返回 重新加載 開始 停止 暫停 繼續 定時 朗讀暫停 正在朗讀(還剩%d分鐘) "正在朗讀(還剩%d小時%d分)" %d分鐘 "%d小時%d分" "計時已取消" 閱讀畫面隱藏虛擬按鍵 隱藏導航欄 導航欄顏色 GitHub上 評分 發送郵件 無法打開 分享失敗 無章節 添加網址 添加書籍網址 背景 作者 站點暫時不支持解析,請反饋 朗讀停止 清除快取 保存 編輯書源 禁用書源 新建書源 添加書籍 掃描 複製書源 複製書源無發現 粘貼書源 書源規則說明 檢查更新 掃描二維碼 掃描本機相片 規則說明 分享 應用程式分享 跟隨系統 添加 匯入書源 本地匯入 網路匯入 替換淨化 替換規則編輯 替換規則 替換為 封面 音量鍵翻頁 點擊翻頁 點擊總是翻下一頁 翻頁動畫 熒幕逾時 返回 選單 調節 滾動條 清除快取會刪除所有已保存章節,是否確認刪除? 書源共享 替換規則名稱 全選 夜間模式 啟動頁 開始下載 取消下載 暫無任務 匯入選擇書籍 更新和搜尋線程數,如感覺卡頓請減小線程數,量力而行 切換圖示 刪除書籍 開始閱讀 加載數據中… 加載失敗,點擊重試 內容簡介 打開外部書籍 來源 本機匯入 網路匯入 書架排序 檢查更新間隔 按閱讀時間排序 按更新時間排序 手動排序 閱讀方式 刪除所選 是否確認刪除? 預設字體 發現 發現管理 沒有內容,去書源里自訂吧! 刪除所有 搜尋歷史 清除 正文顯示標題 書源同步 無最新章節資訊 顯示時間和電量 長按選取文本 顯示分隔線 深色狀態欄圖標 內容 複製內容 一鍵緩存 這是一段測試文字\n\u3000\u3000只是讓你看看效果的 文字顏色和背景(長按自訂) 沉浸式狀態欄 還剩%d章未下載 長按輸入顏色值 加載中… 追更區 養肥區 書籤 添加書籤 刪除 加載逾時 關注:%s 已複製 整理書架 這將會刪除所有書籍,請謹慎操作。 搜尋書源 搜尋(共%d個書源) 目錄(%d) 加粗 字體 文字 應用主頁 邊距: 上邊距 下邊距 左邊距 右邊距 校驗書源 校驗所選 進度 %d/%d 請安裝並選擇中文TTS! TTS初始化失敗! 簡繁轉換 關閉 簡轉繁 繁轉簡 翻頁模式 %1$d 項 SD記憶卡 加入書架 加入書架(%1$d) 成功添加%1$d本書 請將字體檔案放到內部存儲空間目錄Fonts資料夾下重新選擇 預設字體 選擇字體 字號 行距 段距 編輯 刪除 置頂 自動展開發現 默認展開第一組發現 當前線程數 %s 朗讀語速 自動翻頁 停止自動翻頁 自動翻頁時每分鐘閱讀字數(CPM) 書籍資訊 默認打開書架 自動跳轉最近閱讀 替換範圍,選填書名或者源名 分組 备份路徑 內容緩存路徑 系統檔案選擇器 新版本 下載更新 朗讀時音量鍵翻頁 Tip邊距跟隨邊距調整 禁止更新 允許更新 反轉選擇 搜尋書名、作者 書名、作者、URL 常見問題 顯示所有發現 關閉則只顯示勾選源的發現 更新目錄 txt目錄正則 設定編碼 倒序-順序 排序 智慧排序 手動排序 拼音排序 滾動到頂部 滾動到底部 已讀: %s 追更 養肥 完結 所有書籍 追更書籍 養肥書籍 完結書籍 本地書籍 狀態欄顏色透明 導航欄變色 導航欄根據夜間模式變化 放入書架 繼續閱讀 封面地址 覆蓋 仿真 平移 上下平移 無動畫 此書源使用了進階功能,複製支付寶紅包搜索碼領取紅包或關注WeChat官方賬號[開源閱讀軟件]開啟。 後台更新換源最新章節 開啟則會在應用打開1分鐘後開始更新 書架ToolBar自動隱藏 滾動書架時ToolBar自動隱藏與顯示 登錄 登錄%s 成功 當前源沒有配置登陸地址 書源名稱(bookSourceName) 書源URL(bookSourceUrl) 書源類別(bookSourceGroup) 登錄URL(loginUrl) 作者規則(ruleBookAuthor) 正文規則(ruleBookContent) 正文替换規則(ruleBookContentReplace) 書名規則(ruleBookName) 目錄列表規則(ruleChapterList) 章節名稱規則(ruleChapterName) 目錄URL規則(ruleChapterUrl) 目錄下一頁規則(ruleChapterUrlNext) 章節URL規則(ruleContentUrl) 封面規則(ruleCoverUrl) 簡介規則(ruleIntroduce) 搜尋作者規則(ruleSearchAuthor) 發現作者規則(ruleFindAuthor) 搜尋封面規則(ruleSearchCoverUrl) 發現封面規則(ruleFindCoverUrl) 搜尋類別規則(ruleSearchKind) 發現類別規則(ruleFindKind) 搜尋最新章節規則(ruleSearchLastChapter) 發現最新章節規則(ruleFindLastChapter) 搜尋列表規則(ruleSearchList) 發現列表規則(ruleFindList) 搜尋書名規則(ruleSearchName) 發現書名規則(ruleFindName) 搜尋書籍URL規則(ruleSearchNoteUrl) 發現書籍URL規則(ruleFindNoteUrl) 搜尋簡介規則(ruleSearchIntroduce) 發現簡介規則(ruleFindIntroduce) 搜尋地址(ruleSearchUrl) 發現規則(ruleFindUrl) 正文下一頁URL規則(ruleContentUrlNext) 書籍詳情URL正則(ruleBookUrlPattern) 書籍詳情預處理規則(ruleBookInfoInit) 類別規則(ruleBookKind) 最新章節規則(ruleBookLastChapter) HttpUserAgent 調試書源 二維碼匯入 掃描二維碼 選中時點擊可彈出選單 主題 預設主題 復原主題為預設配色 加入QQ群 檔案存取失敗 獲取背景圖片需儲存權限 輸入書源網址 刪除檔案 刪除檔案成功 確定刪除檔案嗎? 手機目錄 智慧匯入 發現 切換顯示樣式 匯入本機書籍需儲存權限 點擊可切換到白天模式 點擊可切換到夜間模式 本應用需要儲存權限來儲存備份書籍資訊 再按一次退出應用 匯入本機書籍需儲存權限 網路連線不可用 是否刪除全部書籍? 是否同時刪除已下載的書籍目錄? 掃描二維碼需相機權限 朗讀正在運行,不能自動翻頁 輸入編碼 TXT目錄規則 打開外部書籍需獲取儲存權限 書籍資訊獲取失敗 內容獲取失敗 目錄獲取失敗 訪問網站失敗:%s 未獲取到書名 輸入替換規則網址 搜尋列表獲取成功%d 書源名稱和URL不能為空 相簿 領支付寶紅包 沒有獲取到更新地址 正在打開首頁,成功自動返回主畫面 登錄成功後請點擊右上角圖示進行首頁訪問測試 章節: 使用正則表達式 縮進 無縮進 一字符縮進 二字符縮進 三字符縮進 四字符縮進 選擇SD記憶卡 沒有發現,可以在書源里添加。 恢復預設 自訂暫存資料路徑需要儲存權限 黑色 文章內容為空 正在換源請等待… 目錄列表為空 加載失敗\n%s 正文邊距 Tip邊距 字距 E-Ink 模式 去除動畫,優化電紙書使用體驗 Web服務 web端口 當前端口 %s 二維碼分享 字符串分享 wifi分享 請授予儲存權限 上一個 下一個 音樂 音訊 切換應用顯示在桌面的圖示 顯示勾選 擴展到劉海 列表視圖 網格視圖三列 網格視圖四列 全部書源 搜索結果過濾等級 調高過濾等級可以避免看到書名/作者無關的搜索結果。 等級1-9:搜索關鍵字需要出現到書名/作者中,但是允許有8-0個字(空格不計)沒有出現(避免誤輸入的影響)。默認等級0,不過濾。推薦等級7,允許2個錯別字。 \n當前過濾等級 %s 登录UI(loginUi) 登录检查Js(loginCheckJs) VIP标识(ruleVip) 购买标识(rulePay) 购买 ================================================ FILE: app/src/main/res/xml/file_paths.xml ================================================ ================================================ FILE: app/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: app/src/main/res/xml/pref_settings.xml ================================================ ================================================ FILE: app/src/main/res/xml/pref_settings_theme.xml ================================================ ================================================ FILE: app/src/main/res/xml/pref_settings_web_dav.xml ================================================ ================================================ FILE: app/src/main/res/xml/shortcuts.xml ================================================ ================================================ FILE: app/src/test/java/com/kunfei/bookshelf/ExampleUnitTest.java ================================================ package com.kunfei.bookshelf; import org.junit.Test; import static org.junit.Assert.*; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: basemvplib/.gitignore ================================================ /build ================================================ FILE: basemvplib/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion 30 buildToolsVersion '30.0.3' defaultConfig { minSdkVersion 19 targetSdkVersion 30 } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } android { } } compileOptions { targetCompatibility 1.8 sourceCompatibility 1.8 } } dependencies { testImplementation 'junit:junit:4.13.2' api fileTree(dir: 'libs', include: ['*.jar']) //support api 'androidx.core:core:1.6.0' api 'androidx.appcompat:appcompat:1.3.0' } ================================================ FILE: basemvplib/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in D:\CodeTool\Android\Android_SDK/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -optimizationpasses 5 -dontskipnonpubliclibraryclassmembers -dontusemixedcaseclassnames -classobfuscationdictionary obfuscationClassNames.txt -dontskipnonpubliclibraryclasses -verbose ##################OKGO######################## #okgo -dontwarn com.lzy.okgo.** -keep class com.lzy.okgo.**{*;} #okrx -dontwarn com.lzy.okrx.** -keep class com.lzy.okrx.**{*;} #okserver -dontwarn com.lzy.okserver.** -keep class com.lzy.okserver.**{*;} ================================================ FILE: basemvplib/src/main/AndroidManifest.xml ================================================ ================================================ FILE: basemvplib/src/main/java/com/kunfei/basemvplib/AppActivityManager.java ================================================ package com.kunfei.basemvplib; import android.app.Activity; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Activity管理器,管理项目中Activity的状态 */ public class AppActivityManager { private static List> activities; private AppActivityManager() { activities = new ArrayList<>(); } private static volatile AppActivityManager instance; public static AppActivityManager getInstance() { if (null == instance) { synchronized (AppActivityManager.class) { if (null == instance) { instance = new AppActivityManager(); } } } return instance; } public List> getActivities() { return activities; } /** * 添加Activity */ public void add(Activity activity) { activities.add(new WeakReference(activity)); } /** * 移除Activity */ public void remove(Activity activity) { for (WeakReference temp : activities) { if (null != temp.get() && temp.get() == activity) { activities.remove(temp); break; } } } /** * 移除Activity */ public void remove(Class activityClass) { for (Iterator> iterator = activities.iterator(); iterator.hasNext(); ) { WeakReference item = iterator.next(); if (null != item && null != item.get() && item.get().getClass() == activityClass) { iterator.remove(); } } } /** * 关闭指定 activity */ public void finishActivity(BaseActivity... activities) { for (BaseActivity activity : activities) { if (null != activity) { activity.finish(); } } } /** * 关闭指定 activity(class) */ public void finishActivity(Class... activityClasses) { ArrayList> waitFinish = new ArrayList<>(); for (WeakReference temp : activities) { for (Class activityClass : activityClasses) { if (null != temp.get() && temp.get().getClass() == activityClass) { waitFinish.add(temp); break; } } } for (WeakReference activityWeakReference : waitFinish) { if (null != activityWeakReference.get()) { activityWeakReference.get().finish(); } } } /** * 判断指定Activity是否存在 */ public Boolean isExist(Class activityClass) { boolean result = false; for (WeakReference item : activities) { if (null != item && null != item.get() && item.get().getClass() == activityClass) { result = true; break; } } return result; } } ================================================ FILE: basemvplib/src/main/java/com/kunfei/basemvplib/BaseActivity.java ================================================ package com.kunfei.basemvplib; import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.graphics.PorterDuff; import android.os.Build; import android.os.Bundle; import android.view.View; import android.widget.Toast; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import com.monke.basemvplib.R; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; public abstract class BaseActivity extends AppCompatActivity implements IView { public final static String START_SHEAR_ELE = "start_with_share_ele"; public static final int SUCCESS = 1; public static final int ERROR = -1; protected Bundle savedInstanceState; protected T mPresenter; protected boolean isRecreate; private Boolean startShareAnim = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.savedInstanceState = savedInstanceState; if(getIntent()!=null){ isRecreate = getIntent().getBooleanExtra("isRecreate", false); startShareAnim = getIntent().getBooleanExtra(START_SHEAR_ELE, false); } AppActivityManager.getInstance().add(this); initSDK(); onCreateActivity(); mPresenter = initInjector(); attachView(); initData(); bindView(); bindEvent(); firstRequest(); } /** * 首次逻辑操作 */ protected void firstRequest() { } /** * 事件触发绑定 */ protected void bindEvent() { } /** * 控件绑定 */ protected void bindView() { } /** * P层绑定V层 */ private void attachView() { if (null != mPresenter) { mPresenter.attachView(this); } } /** * P层解绑V层 */ private void detachView() { if (null != mPresenter) { mPresenter.detachView(); } } /** * SDK初始化 */ protected void initSDK() { } /** * P层绑定 若无则返回null; */ protected abstract T initInjector(); /** * 布局载入 setContentView() */ protected abstract void onCreateActivity(); /** * 数据初始化 */ protected abstract void initData(); @Override protected void onResume() { super.onResume(); } @Override protected void onDestroy() { super.onDestroy(); detachView(); AppActivityManager.getInstance().remove(this); } @Override public void recreate() { getIntent().putExtra("isRecreate", true); super.recreate(); } /////////Toast////////////////// public void toast(String msg) { toast(msg, Toast.LENGTH_SHORT, 0); } public void toast(String msg, int state) { toast(msg, Toast.LENGTH_LONG, state); } public void toast(int strId) { toast(strId, 0); } public void toast(int strId, int state) { toast(getString(strId), Toast.LENGTH_LONG, state); } public void toast(String msg, int length, int state) { Toast toast = Toast.makeText(this, msg, length); try { if (state == SUCCESS) { toast.getView().getBackground().setColorFilter(getResources().getColor(R.color.success), PorterDuff.Mode.SRC_IN); } else if (state == ERROR) { toast.getView().getBackground().setColorFilter(getResources().getColor(R.color.error), PorterDuff.Mode.SRC_IN); } } catch (Exception ignored) { } toast.show(); } public Context getContext(){ return this; } public Boolean getStart_share_ele() { return startShareAnim; } ////////////////////////////////启动Activity转场动画///////////////////////////////////////////// protected void startActivityForResultByAnim(Intent intent, int requestCode, int animIn, int animExit) { startActivityForResult(intent, requestCode); overridePendingTransition(animIn, animExit); } protected void startActivityByAnim(Intent intent, int animIn, int animExit) { startActivity(intent); overridePendingTransition(animIn, animExit); } protected void startActivityByAnim(Intent intent, @NonNull View view, @NonNull String transitionName, int animIn, int animExit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.putExtra(START_SHEAR_ELE, true); ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, view, transitionName); startActivity(intent, options.toBundle()); } else { startActivityByAnim(intent, animIn, animExit); } } } ================================================ FILE: basemvplib/src/main/java/com/kunfei/basemvplib/BaseFragment.java ================================================ package com.kunfei.basemvplib; import static com.kunfei.basemvplib.BaseActivity.START_SHEAR_ELE; import android.app.ActivityOptions; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; public abstract class BaseFragment extends Fragment implements IView { protected View view; protected Bundle savedInstanceState; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { this.savedInstanceState = savedInstanceState; initSDK(); view = createView(inflater, container); initData(); bindView(); bindEvent(); firstRequest(); return view; } /** * 事件触发绑定 */ protected void bindEvent() { } /** * 控件绑定 */ protected void bindView() { } /** * 数据初始化 */ protected void initData() { } /** * 首次逻辑操作 */ protected void firstRequest() { } /** * 加载布局 */ protected abstract View createView(LayoutInflater inflater, ViewGroup container); /** * 第三方SDK初始化 */ protected void initSDK() { } protected void startActivityByAnim(Intent intent, int animIn, int animExit) { startActivity(intent); requireActivity().overridePendingTransition(animIn, animExit); } protected void startActivityByAnim(Intent intent, @NonNull View view, @NonNull String transitionName, int animIn, int animExit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.putExtra(START_SHEAR_ELE, true); ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(getActivity(), view, transitionName); startActivity(intent, options.toBundle()); } else { startActivityByAnim(intent, animIn, animExit); } } } ================================================ FILE: basemvplib/src/main/java/com/kunfei/basemvplib/BasePresenterImpl.java ================================================ package com.kunfei.basemvplib; import com.kunfei.basemvplib.impl.IPresenter; import com.kunfei.basemvplib.impl.IView; import androidx.annotation.NonNull; public abstract class BasePresenterImpl implements IPresenter { protected T mView; @Override public void attachView(@NonNull IView iView) { mView = (T) iView; } } ================================================ FILE: basemvplib/src/main/java/com/kunfei/basemvplib/BitIntentDataManager.java ================================================ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.basemvplib; import java.util.HashMap; import java.util.Map; public class BitIntentDataManager { private static Map bigData; private static BitIntentDataManager instance = null; private BitIntentDataManager() { bigData = new HashMap<>(); } public static BitIntentDataManager getInstance() { if (instance == null) { synchronized (BitIntentDataManager.class) { if (instance == null) { instance = new BitIntentDataManager(); } } } return instance; } public Object getData(String key) { Object object = bigData.get(key); bigData.remove(key); return object; } public void putData(String key, Object data) { bigData.put(key, data); } } ================================================ FILE: basemvplib/src/main/java/com/kunfei/basemvplib/impl/IPresenter.java ================================================ package com.kunfei.basemvplib.impl; import androidx.annotation.NonNull; public interface IPresenter { /** * 注入View,使之能够与View相互响应 */ void attachView(@NonNull IView iView); /** * 释放资源,如果使用了网络请求 可以在此执行IModel.cancelRequest() */ void detachView(); } ================================================ FILE: basemvplib/src/main/java/com/kunfei/basemvplib/impl/IView.java ================================================ package com.kunfei.basemvplib.impl; import android.content.Context; public interface IView { Context getContext(); void toast(String msg); void toast(int id); } ================================================ FILE: basemvplib/src/main/res/values/colors.xml ================================================ #eb4333 #439b53 ================================================ FILE: basemvplib/src/main/res/values/strings.xml ================================================ BaseMvpLib ================================================ FILE: basemvplib/src/test/java/com/kunfei/basemvplib/ExampleUnitTest.java ================================================ package com.kunfei.basemvplib; import org.junit.Test; import static org.junit.Assert.*; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: build.gradle ================================================ ext { support_library_version = '28.0.0' } buildscript { ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } maven { url 'https://plugins.gradle.org/m2/' } } dependencies { classpath 'com.android.tools.build:gradle:7.1.2' classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath 'net.ricecode:string-similarity:1.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.4' } } allprojects { repositories { google() mavenCentral() maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } maven { url 'https://plugins.gradle.org/m2/' } maven { url 'https://jitpack.io' } } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Sat Jun 05 22:59:28 CST 2021 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradle.properties ================================================ ## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx1024m -XX:MaxPermSize=256m org.gradle.jvmargs=-Xms1024m -Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #Fri Mar 30 10:51:47 CST 2018 android.useAndroidX=true android.enableJetifier=true org.gradle.parallel=true org.gradle.damon=true org.gradle.configureondemand=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: mail ================================================ gekunfei@live.com ================================================ FILE: settings.gradle ================================================ include ':app', ':basemvplib' ================================================ FILE: tool/书源整理工具/BookSourceMgr.dpr ================================================ program BookSourceMgr; uses Forms, uFrmMain in 'uFrmMain.pas' {Form1}, Themes, Styles, uFrmWait in 'uFrmWait.pas' {Form2}, uBookSourceBean in 'uBookSourceBean.pas', uFrmEditSource in 'uFrmEditSource.pas' {frmEditSource}, uFrmReplaceGroup in 'uFrmReplaceGroup.pas' {frmReplaceGroup}; {$R *.res} begin Application.Initialize; Application.MainFormOnTaskbar := True; Application.Title := 'ĶԴ'; Application.CreateForm(TForm1, Form1); Application.Run; end. ================================================ FILE: tool/书源整理工具/BookSourceMgr.dproj ================================================  {b1648034-9a9f-40d4-a355-b3335a5a9446} Debug DCC32 bin\BookSourceMgr.exe BookSourceMgr.dpr VCL 18.3 True Debug Win32 3 Application true true Base true true Base true true Base true true Cfg_1 true true true Cfg_1 true true true Base true true Cfg_2 true true true Cfg_2 true true BookSourceMgr Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Data;Datasnap;Web;Soap;Winapi;$(DCC_Namespace) 2052 CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments= D:\Documents\Desktop\SynEdit\Source;D:\Documents\Desktop\YxdJson\source;D:\Documents\Desktop\YxdIocp\YxdIocp\source\IOCP;D:\Documents\Desktop\YxdWorker\YxdWorker\source;E:\Projects\Delphi\YxdIocp\source\IOCP;E:\Projects\Delphi\YxdJson\source;E:\Projects\Delphi\YxdWorker\source;\\Mac\Home\Desktop\Project\SynEdit\SynEdit\Source;$(DCC_UnitSearchPath) System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) Debug true CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) 1033 $(BDS)\bin\default_app.manifest doc\Main.ico true $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png true $(BDS)\bin\default_app.manifest BookSourceMgr_Icon1.ico true $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) Debug true CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= 1033 7.0 0 False 0 RELEASE;$(DCC_Define) true true CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) true 1033 true true 7.0 DEBUG;$(DCC_Define) .\bin .\dcu .\dcu .\dcu D:\Documents\Desktop\YxdJson\source;D:\Documents\Desktop\YxdIocp\YxdIocp\source\IOCP;D:\Documents\Desktop\YxdWorker\YxdWorker\source;$(DCC_ResourcePath) D:\Documents\Desktop\YxdJson\source;D:\Documents\Desktop\YxdIocp\YxdIocp\source\IOCP;D:\Documents\Desktop\YxdWorker\YxdWorker\source;$(DCC_ObjPath) D:\Documents\Desktop\YxdJson\source;D:\Documents\Desktop\YxdIocp\YxdIocp\source\IOCP;D:\Documents\Desktop\YxdWorker\YxdWorker\source;$(DCC_IncludePath) true true CompanyName=YangYxd;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=阅读书源管理工具;ProgramID=com.embarcadero.$(MSBuildProjectName) true 1033 true true true CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= true 1033 doc\Main.ico Delphi.Personality.12 False True False False False 1 0 0 0 False False False False False 2052 936 1.0.0.0 1.0.0.0 Embarcadero Git Integration Microsoft Office 2000 Sample Automation Server Wrapper Components Microsoft Office XP Sample Automation Server Wrapper Components BookSourceMgr.dpr True True BookSourceMgr.exe true 1 Contents\MacOS 1 Contents\MacOS 0 classes 1 library\lib\armeabi-v7a 1 library\lib\armeabi 1 library\lib\mips 1 library\lib\armeabi-v7a 1 res\drawable 1 res\values 1 res\drawable 1 res\drawable-xxhdpi 1 res\drawable-ldpi 1 res\drawable-mdpi 1 res\drawable-hdpi 1 res\drawable-xhdpi 1 res\drawable-small 1 res\drawable-normal 1 res\drawable-large 1 res\drawable-xlarge 1 1 Contents\MacOS 1 0 Contents\MacOS 1 .framework 0 1 .dylib 1 .dylib 1 .dylib Contents\MacOS 1 .dylib 0 .dll;.bpl 1 .dylib 1 .dylib 1 .dylib Contents\MacOS 1 .dylib 0 .bpl 0 0 0 0 Contents\Resources\StartUp\ 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 1 ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 1 1 1 ..\ 1 ..\ 1 1 1 1 1 1 1 ..\ 1 Contents 1 Contents\Resources 1 library\lib\armeabi-v7a 1 1 1 1 1 Contents\MacOS 1 0 1 1 Assets 1 Assets 1 Assets 1 Assets 1 12 MainSource
    Form1
    Form2
    dfm
    frmEditSource
    dfm
    frmReplaceGroup
    dfm
    Cfg_2 Base Base Cfg_1 Base
    ================================================ FILE: tool/书源整理工具/ReadMe.txt ================================================ õ SynEdit https://github.com/SynEdit/SynEdit ================================================ FILE: tool/书源整理工具/uBookSourceBean.pas ================================================ unit uBookSourceBean; interface uses YxdJson, Classes, SysUtils, Math; type TBookSourceItem = class(JSONObject) private function GetIndexValue(const Index: Integer): string; procedure SetIndexValue(const Index: Integer; const Value: string); function GetEnable: Boolean; function GetSerialNumber: Integer; function GetWeight: Integer; procedure SetEnable(const Value: Boolean); procedure SetSerialNumber(const Value: Integer); procedure SetWeight(const Value: Integer); public procedure AddGroup(const Name: string); procedure RemoveGroup(const Name: string); procedure ReplaceGroup(const Name, NewName: string); function GetGroupList(): TArray; property bookSourceGroup: string index 0 read GetIndexValue write SetIndexValue; // Դ property bookSourceName: string index 1 read GetIndexValue write SetIndexValue; // Դ property bookSourceUrl: string index 2 read GetIndexValue write SetIndexValue; // ԴURL property httpUserAgent: string index 3 read GetIndexValue write SetIndexValue; // HttpUserAgent property loginUrl: string index 4 read GetIndexValue write SetIndexValue; // ¼URL property ruleBookAuthor: string index 5 read GetIndexValue write SetIndexValue; // ߹ property ruleBookContent: string index 6 read GetIndexValue write SetIndexValue; // Ĺ property ruleBookKind: string index 7 read GetIndexValue write SetIndexValue; // property ruleBookLastChapter: string index 8 read GetIndexValue write SetIndexValue; // ½ڹ property ruleBookName: string index 9 read GetIndexValue write SetIndexValue; // property ruleBookUrlPattern: string index 10 read GetIndexValue write SetIndexValue; // 鼮URL property ruleChapterList: string index 11 read GetIndexValue write SetIndexValue; // Ŀ¼б property ruleChapterName: string index 12 read GetIndexValue write SetIndexValue; // ½ƹ property ruleChapterUrl: string index 13 read GetIndexValue write SetIndexValue; // Ŀ¼URL property ruleChapterUrlNext: string index 14 read GetIndexValue write SetIndexValue; // Ŀ¼һҳUrl property ruleContentUrl: string index 15 read GetIndexValue write SetIndexValue; // ½URL property ruleContentUrlNext: string index 16 read GetIndexValue write SetIndexValue; // һҳURL property ruleCoverUrl: string index 17 read GetIndexValue write SetIndexValue; // property ruleFindUrl: string index 18 read GetIndexValue write SetIndexValue; // ֹ property ruleIntroduce: string index 19 read GetIndexValue write SetIndexValue; // property ruleSearchAuthor: string index 20 read GetIndexValue write SetIndexValue; // ߹ property ruleSearchCoverUrl: string index 21 read GetIndexValue write SetIndexValue; // property ruleSearchKind: string index 22 read GetIndexValue write SetIndexValue; // property ruleSearchLastChapter: string index 23 read GetIndexValue write SetIndexValue; // ½ڹ property ruleSearchList: string index 24 read GetIndexValue write SetIndexValue; // б property ruleSearchName: string index 25 read GetIndexValue write SetIndexValue; // property ruleSearchNoteUrl: string index 26 read GetIndexValue write SetIndexValue; // 鼮URL property ruleSearchUrl: string index 27 read GetIndexValue write SetIndexValue; // ַ property enable: Boolean read GetEnable write SetEnable; property serialNumber: Integer read GetSerialNumber write SetSerialNumber; property weight: Integer read GetWeight write SetWeight; end; implementation { TBookSourceItem } const SKeyArray: array [0..27] of string = ( 'bookSourceGroup', 'bookSourceName', 'bookSourceUrl', 'httpUserAgent', 'loginUrl', 'ruleBookAuthor', 'ruleBookContent', 'ruleBookKind', 'ruleBookLastChapter', 'ruleBookName', 'ruleBookUrlPattern', 'ruleChapterList', 'ruleChapterName', 'ruleChapterUrl', 'ruleChapterUrlNext', 'ruleContentUrl', 'ruleContentUrlNext', 'ruleCoverUrl', 'ruleFindUrl', 'ruleIntroduce', 'ruleSearchAuthor', 'ruleSearchCoverUrl', 'ruleSearchKind', 'ruleSearchLastChapter', 'ruleSearchList', 'ruleSearchName', 'ruleSearchNoteUrl', 'ruleSearchUrl' ); SEnabled = 'enable'; SSerialNumber = 'serialNumber'; SWeight = 'weight'; procedure TBookSourceItem.AddGroup(const Name: string); var S: string; List: TArray; I: Integer; begin S := Trim(bookSourceGroup); if S = '' then bookSourceGroup := Name else begin List := GetGroupList(); for I := Low(List) to High(List) do begin if Trim(List[I]) = Name then Exit; end; bookSourceGroup := bookSourceGroup + '; ' + Name; end; end; function TBookSourceItem.GetEnable: Boolean; begin Result := Self.B[SEnabled]; end; function TBookSourceItem.GetGroupList: TArray; var S: string; begin S := Trim(bookSourceGroup); Result := S.Split([',', ';', ':', '', '']); end; function TBookSourceItem.GetIndexValue(const Index: Integer): string; begin Result := Self.S[SKeyArray[Index]]; end; function TBookSourceItem.GetSerialNumber: Integer; begin Result := Self.I[SSerialNumber]; end; function TBookSourceItem.GetWeight: Integer; begin Result := SElf.I[SWeight]; end; procedure TBookSourceItem.RemoveGroup(const Name: string); var S: string; List: TArray; I, J: Integer; SB: TStringBuilder; begin S := Trim(bookSourceGroup); if S <> '' then begin J := 0; List := GetGroupList(); SB := TStringBuilder.Create(Length(bookSourceGroup) * 2); for I := Low(List) to High(List) do begin if Trim(List[I]) <> Name then begin if J > 0 then SB.Append('; '); SB.Append(Trim(List[I])); Inc(J); end; end; bookSourceGroup := SB.ToString; end; end; procedure TBookSourceItem.ReplaceGroup(const Name, NewName: string); var S: string; List: TArray; I, J: Integer; SB: TStringBuilder; begin S := Trim(bookSourceGroup); if S <> '' then begin J := 0; List := GetGroupList(); SB := TStringBuilder.Create(Length(bookSourceGroup) * 2); for I := Low(List) to High(List) do begin if Trim(List[I]) <> Name then begin if J > 0 then SB.Append('; '); SB.Append(Trim(List[I])); Inc(J); end else if NewName <> '' then begin if J > 0 then SB.Append('; '); SB.Append(Trim(NewName)); Inc(J); end; end; bookSourceGroup := SB.ToString; end; end; procedure TBookSourceItem.SetEnable(const Value: Boolean); begin Self.B[SEnabled] := Value; end; procedure TBookSourceItem.SetIndexValue(const Index: Integer; const Value: string); begin Self.S[SKeyArray[Index]] := Value; end; procedure TBookSourceItem.SetSerialNumber(const Value: Integer); begin Self.I[SSerialNumber] := Value; end; procedure TBookSourceItem.SetWeight(const Value: Integer); begin Self.I[SWeight] := Value; end; end. ================================================ FILE: tool/书源整理工具/uFrmEditSource.dfm ================================================ object frmEditSource: TfrmEditSource Left = 0 Top = 0 BorderIcons = [biSystemMenu] Caption = #32534#36753#20070#28304 ClientHeight = 743 ClientWidth = 1198 Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = 'Courier New' Font.Style = [] OldCreateOrder = False Position = poScreenCenter OnClose = FormClose OnCreate = FormCreate OnMouseWheel = FormMouseWheel OnResize = FormResize OnShow = FormShow PixelsPerInch = 96 TextHeight = 15 object ScrollBox1: TScrollBox Left = 0 Top = 0 Width = 1198 Height = 700 HorzScrollBar.Smooth = True HorzScrollBar.Style = ssFlat VertScrollBar.Smooth = True VertScrollBar.Style = ssFlat VertScrollBar.Tracking = True Align = alClient BevelInner = bvNone BevelOuter = bvNone BorderStyle = bsNone Color = clWindow Padding.Bottom = 8 ParentColor = False TabOrder = 0 DesignSize = ( 1181 700) object Label1: TLabel Left = 603 Top = 13 Width = 184 Height = 12 Caption = #20998#32452#21517#31216#65306'(bookSourceGroup)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label2: TLabel Left = 8 Top = 679 Width = 156 Height = 12 Caption = #21457#29616#35268#21017#65306'(ruleFindUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label3: TLabel Left = 8 Top = 13 Width = 177 Height = 12 Caption = #20070#28304#21517#31216#65306'(bookSourceName)' Font.Charset = DEFAULT_CHARSET Font.Color = clRed Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label4: TLabel Left = 8 Top = 40 Width = 165 Height = 12 Caption = #20070#28304'URL'#65306'(bookSourceUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clRed Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label5: TLabel Left = 8 Top = 66 Width = 114 Height = 12 Caption = #30331#24405'URL'#65306'(loginUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clTeal Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label6: TLabel Left = 8 Top = 91 Width = 170 Height = 12 Caption = #25628#32034#22320#22336#65306'(ruleSearchUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label7: TLabel Left = 8 Top = 117 Width = 229 Height = 12 Caption = #25628#32034#32467#26524#21015#34920#35268#21017#65306'(ruleSearchList)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label8: TLabel Left = 8 Top = 142 Width = 229 Height = 12 Caption = #25628#32034#32467#26524#20070#21517#35268#21017#65306'(ruleSearchName)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label9: TLabel Left = 8 Top = 168 Width = 243 Height = 12 Caption = #25628#32034#32467#26524#20316#32773#35268#21017#65306'(ruleSearchAuthor)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label10: TLabel Left = 8 Top = 193 Width = 229 Height = 12 Caption = #25628#32034#32467#26524#20998#31867#35268#21017#65306'(ruleSearchKind)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label11: TLabel Left = 8 Top = 219 Width = 270 Height = 12 Caption = #25628#32034#32467#26524#26368#26032#31456#33410#35268#21017#65306'(ruleSearchLastChapter)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label12: TLabel Left = 8 Top = 244 Width = 257 Height = 12 Caption = #25628#32034#32467#26524#23553#38754#35268#21017#65306'(ruleSearchCoverUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label13: TLabel Left = 8 Top = 270 Width = 271 Height = 12 Caption = #25628#32034#32467#26524#20070#31821'URL'#35268#21017#65306'(ruleSearchNoteUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clGreen Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label14: TLabel Left = 8 Top = 295 Width = 222 Height = 12 Caption = #20070#31821#35814#24773'URL'#27491#21017#65306'(ruleBookUrlPattern)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label15: TLabel Left = 8 Top = 321 Width = 144 Height = 12 Caption = #20070#21517#35268#21017#65306'(ruleBookName)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label16: TLabel Left = 8 Top = 346 Width = 156 Height = 12 Caption = #20316#32773#35268#21017#65306'(ruleBookAuthor)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label17: TLabel Left = 8 Top = 372 Width = 144 Height = 12 Caption = #23553#38754#35268#21017#65306'(ruleCoverUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label18: TLabel Left = 8 Top = 397 Width = 144 Height = 12 Caption = #20998#31867#35268#21017#65306'(ruleBookKind)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label19: TLabel Left = 8 Top = 423 Width = 210 Height = 12 Caption = #26368#26032#31456#33410#35268#21017#65306'(ruleBookLastChapter)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label20: TLabel Left = 8 Top = 448 Width = 170 Height = 12 Caption = #31616#20171#35268#21017#65306'(ruleIntroduce)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label21: TLabel Left = 8 Top = 474 Width = 198 Height = 12 Caption = #30446#24405'URL'#35268#21017#65306'(ruleChapterUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label22: TLabel Left = 8 Top = 499 Width = 210 Height = 12 Caption = #30446#24405#21015#34920#35268#21017#65306'(ruleChapterList)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label23: TLabel Left = 8 Top = 525 Width = 234 Height = 12 Caption = #30446#24405#19979#19968#39029'URL'#35268#21017#65306'(ruleChapterUrlNext)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label24: TLabel Left = 8 Top = 550 Width = 210 Height = 12 Caption = #31456#33410#21517#31216#35268#21017#65306'(ruleChapterName)' Font.Charset = DEFAULT_CHARSET Font.Color = clNavy Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label25: TLabel Left = 8 Top = 576 Width = 224 Height = 12 Caption = #27491#25991#31456#33410'URL'#35268#21017#65306'(ruleContentUrl)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label26: TLabel Left = 8 Top = 601 Width = 234 Height = 12 Caption = #27491#25991#19979#19968#39029'URL'#35268#21017#65306'(ruleContentUrlNext)' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label27: TLabel Left = 8 Top = 627 Width = 184 Height = 12 Caption = #27491#25991#35268#21017#65306'(ruleBookContent)' Font.Charset = DEFAULT_CHARSET Font.Color = clNavy Font.Height = -12 Font.Name = #23435#20307 Font.Style = [fsBold] ParentFont = False end object Label28: TLabel Left = 9 Top = 653 Width = 162 Height = 12 Caption = #27983#35272#22120#27169#25311#65306'(HttpUserAgent)' Font.Charset = DEFAULT_CHARSET Font.Color = clTeal Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object ComboBox1: TComboBox Left = 793 Top = 8 Width = 383 Height = 23 Hint = 'bookSourceGroup' Anchors = [akLeft, akTop, akRight] DropDownCount = 20 TabOrder = 1 end object Memo1: TMemo Left = 296 Top = 674 Width = 881 Height = 200 Hint = 'ruleFindUrl' Margins.Bottom = 8 Anchors = [akLeft, akTop, akRight] ScrollBars = ssVertical TabOrder = 27 end object Edit1: TEdit Left = 295 Top = 8 Width = 290 Height = 23 Hint = 'bookSourceName' TabOrder = 0 TextHint = #24517#22635 end object Edit2: TEdit Left = 295 Top = 35 Width = 881 Height = 23 Hint = 'bookSourceUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 2 TextHint = #24517#22635 end object Edit3: TEdit Left = 295 Top = 61 Width = 881 Height = 23 Hint = 'loginUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 3 end object Edit4: TEdit Left = 295 Top = 86 Width = 881 Height = 23 Hint = 'ruleSearchUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 4 end object Edit5: TEdit Left = 295 Top = 112 Width = 881 Height = 23 Hint = 'ruleSearchList' Anchors = [akLeft, akTop, akRight] TabOrder = 5 end object Edit6: TEdit Left = 295 Top = 137 Width = 881 Height = 23 Hint = 'ruleSearchName' Anchors = [akLeft, akTop, akRight] TabOrder = 6 end object Edit7: TEdit Left = 295 Top = 163 Width = 881 Height = 23 Hint = 'ruleSearchAuthor' Anchors = [akLeft, akTop, akRight] TabOrder = 7 end object Edit8: TEdit Left = 295 Top = 188 Width = 881 Height = 23 Hint = 'ruleSearchKind' Anchors = [akLeft, akTop, akRight] TabOrder = 8 end object Edit9: TEdit Left = 295 Top = 214 Width = 881 Height = 23 Hint = 'ruleSearchLastChapter' Anchors = [akLeft, akTop, akRight] TabOrder = 9 end object Edit10: TEdit Left = 295 Top = 239 Width = 881 Height = 23 Hint = 'ruleSearchCoverUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 10 end object Edit11: TEdit Left = 295 Top = 265 Width = 881 Height = 23 Hint = 'ruleSearchNoteUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 11 end object Edit12: TEdit Left = 295 Top = 290 Width = 881 Height = 23 Hint = 'ruleBookUrlPattern' Anchors = [akLeft, akTop, akRight] TabOrder = 12 end object Edit13: TEdit Left = 295 Top = 316 Width = 881 Height = 23 Hint = 'ruleBookName' Anchors = [akLeft, akTop, akRight] TabOrder = 13 end object Edit14: TEdit Left = 295 Top = 341 Width = 881 Height = 23 Hint = 'ruleBookAuthor' Anchors = [akLeft, akTop, akRight] TabOrder = 14 end object Edit15: TEdit Left = 295 Top = 367 Width = 881 Height = 23 Hint = 'ruleCoverUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 15 end object Edit16: TEdit Left = 295 Top = 392 Width = 881 Height = 23 Hint = 'ruleBookKind' Anchors = [akLeft, akTop, akRight] TabOrder = 16 end object Edit17: TEdit Left = 295 Top = 418 Width = 881 Height = 23 Hint = 'ruleBookLastChapter' Anchors = [akLeft, akTop, akRight] TabOrder = 17 end object Edit18: TEdit Left = 295 Top = 443 Width = 881 Height = 23 Hint = 'ruleIntroduce' Anchors = [akLeft, akTop, akRight] TabOrder = 18 end object Edit19: TEdit Left = 295 Top = 469 Width = 881 Height = 23 Hint = 'ruleChapterUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 19 end object Edit20: TEdit Left = 295 Top = 494 Width = 881 Height = 23 Hint = 'ruleChapterList' Anchors = [akLeft, akTop, akRight] TabOrder = 20 end object Edit21: TEdit Left = 295 Top = 520 Width = 881 Height = 23 Hint = 'ruleChapterUrlNext' Anchors = [akLeft, akTop, akRight] TabOrder = 21 end object Edit22: TEdit Left = 295 Top = 545 Width = 881 Height = 23 Hint = 'ruleChapterName' Anchors = [akLeft, akTop, akRight] TabOrder = 22 end object Edit23: TEdit Left = 295 Top = 571 Width = 881 Height = 23 Hint = 'ruleContentUrl' Anchors = [akLeft, akTop, akRight] TabOrder = 23 end object Edit24: TEdit Left = 295 Top = 596 Width = 881 Height = 23 Hint = 'ruleContentUrlNext' Anchors = [akLeft, akTop, akRight] TabOrder = 24 end object Edit25: TEdit Left = 295 Top = 622 Width = 881 Height = 23 Hint = 'ruleBookContent' Anchors = [akLeft, akTop, akRight] TabOrder = 25 end object Edit26: TEdit Left = 296 Top = 648 Width = 881 Height = 23 Hint = 'httpUserAgent' Anchors = [akLeft, akTop, akRight] TabOrder = 26 end end object Panel1: TPanel Left = 0 Top = 700 Width = 1198 Height = 43 Align = alBottom BevelOuter = bvNone Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False ShowCaption = False TabOrder = 1 DesignSize = ( 1198 43) object Label29: TLabel Left = 140 Top = 19 Width = 36 Height = 12 Caption = #26435#37325#65306 Font.Charset = DEFAULT_CHARSET Font.Color = clTeal Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Label30: TLabel Left = 291 Top = 19 Width = 48 Height = 12 Caption = #24207#21015#21495#65306 Font.Charset = DEFAULT_CHARSET Font.Color = clTeal Font.Height = -12 Font.Name = #23435#20307 Font.Style = [] ParentFont = False end object Button1: TButton Left = 1077 Top = 10 Width = 107 Height = 25 Anchors = [akTop, akRight] Caption = #30830#23450'(&O)' Default = True TabOrder = 0 OnClick = Button1Click end object Button2: TButton Left = 949 Top = 10 Width = 107 Height = 25 Anchors = [akTop, akRight] Cancel = True Caption = #21462#28040'(&C)' ModalResult = 2 TabOrder = 1 OnClick = Button2Click end object CheckBox1: TCheckBox Left = 12 Top = 16 Width = 113 Height = 17 Caption = #21551#29992#20070#28304 TabOrder = 2 end object Edit27: TEdit Left = 178 Top = 14 Width = 87 Height = 20 Hint = 'weight' NumbersOnly = True ParentShowHint = False ShowHint = True TabOrder = 3 Text = '0' end object Edit28: TEdit Left = 338 Top = 14 Width = 87 Height = 20 Hint = 'weight' NumbersOnly = True ParentShowHint = False ShowHint = True TabOrder = 4 Text = '0' end object Button3: TButton Left = 793 Top = 10 Width = 107 Height = 25 Anchors = [akTop, akRight] Caption = #27979#35797#20070#28304'(&T)' ModalResult = 2 TabOrder = 5 OnClick = Button3Click end end end ================================================ FILE: tool/书源整理工具/uFrmEditSource.pas ================================================ unit uFrmEditSource; interface uses uBookSourceBean, YxdJson, Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls; type TNotifyEventA = reference to procedure (Item: TBookSourceItem); TfrmEditSource = class(TForm) ScrollBox1: TScrollBox; ComboBox1: TComboBox; Label1: TLabel; Label2: TLabel; Memo1: TMemo; Edit1: TEdit; Label3: TLabel; Label4: TLabel; Edit2: TEdit; Label5: TLabel; Edit3: TEdit; Label6: TLabel; Edit4: TEdit; Label7: TLabel; Edit5: TEdit; Label8: TLabel; Edit6: TEdit; Label9: TLabel; Edit7: TEdit; Label10: TLabel; Edit8: TEdit; Label11: TLabel; Edit9: TEdit; Label12: TLabel; Edit10: TEdit; Label13: TLabel; Edit11: TEdit; Label14: TLabel; Edit12: TEdit; Label15: TLabel; Edit13: TEdit; Label16: TLabel; Edit14: TEdit; Label17: TLabel; Edit15: TEdit; Label18: TLabel; Edit16: TEdit; Label19: TLabel; Edit17: TEdit; Label20: TLabel; Edit18: TEdit; Label21: TLabel; Edit19: TEdit; Label22: TLabel; Edit20: TEdit; Label23: TLabel; Edit21: TEdit; Label24: TLabel; Edit22: TEdit; Label25: TLabel; Edit23: TEdit; Label26: TLabel; Edit24: TEdit; Label27: TLabel; Edit25: TEdit; Edit26: TEdit; Label28: TLabel; Panel1: TPanel; Button1: TButton; Button2: TButton; CheckBox1: TCheckBox; Label29: TLabel; Edit27: TEdit; Label30: TLabel; Edit28: TEdit; Button3: TButton; procedure FormShow(Sender: TObject); procedure Button1Click(Sender: TObject); procedure FormResize(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Button2Click(Sender: TObject); procedure FormCreate(Sender: TObject); procedure Button3Click(Sender: TObject); procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); private { Private declarations } FDisableChange: Boolean; public { Public declarations } Data: TBookSourceItem; CallBack: TNotifyEventA; procedure ApplayEdit(Data: TBookSourceItem); end; var frmEditSource: TfrmEditSource; procedure ShowEditSource(Item: TBookSourceItem; CallBack: TNotifyEventA = nil); implementation {$R *.dfm} uses uFrmMain, Math; var LastW, LastH: Integer; procedure ShowEditSource(Item: TBookSourceItem; CallBack: TNotifyEventA); var F: TfrmEditSource; begin F := TfrmEditSource.Create(Application); F.Data := Item; F.CallBack := CallBack; F.Show; end; procedure TfrmEditSource.ApplayEdit(Data: TBookSourceItem); var I: Integer; Item: TControl; begin Data.enable := CheckBox1.Checked; Data.weight := StrToIntDef(Edit27.Text, Data.weight); Data.serialNumber := StrToIntDef(Edit28.Text, Data.serialNumber); for I := 0 to ScrollBox1.ControlCount - 1 do begin Item := ScrollBox1.Controls[I]; if not Item.Visible then Continue; if Item.Hint = '' then Continue; if Item is TEdit then Data.S[Item.Hint] := TEdit(Item).Text else if Item is TComboBox then Data.S[Item.Hint] := TComboBox(Item).Text else if Item is TMemo then Data.S[Item.Hint] := TMemo(Item).Text; end; end; procedure TfrmEditSource.Button1Click(Sender: TObject); begin FDisableChange := True; try ApplayEdit(Data); finally FDisableChange := False; end; if Assigned(CallBack) then CallBack(Data); Close; end; procedure TfrmEditSource.Button2Click(Sender: TObject); begin Close; end; procedure TfrmEditSource.Button3Click(Sender: TObject); var Item: TBookSourceItem; Msg: TStrings; begin Msg := TStringList.Create; Item := TBookSourceItem(JSONObject.Create); try Item.Parse(Data.ToString()); ApplayEdit(Item); if Form1.CheckBookSourceItem(Item, True, Msg) then MessageBox(Handle, PChar(Msg.Text), 'ϲ, ͨ!', 64) else MessageBox(Handle, PChar(Msg.Text), 'Դ쳣', 48) finally FreeAndNil(Item); FreeAndNil(Msg); end; end; procedure TfrmEditSource.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caFree; end; procedure TfrmEditSource.FormCreate(Sender: TObject); begin if LastW <= 0 then LastW := Self.Width; if LastH <= 0 then LastH := Self.Height; Self.SetBounds(Left, Top, LastW, LastH); end; procedure TfrmEditSource.FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); begin if WheelDelta < 0 then ScrollBox1.Perform(WM_VSCROLL,SB_LINEDOWN,0) else ScrollBox1.Perform(WM_VSCROLL,SB_LINEUP,0); end; procedure TfrmEditSource.FormResize(Sender: TObject); begin LastW := Width; LastH := Height; end; procedure TfrmEditSource.FormShow(Sender: TObject); var I: Integer; Item: TControl; begin ComboBox1.Items := Form1.bookGroupList.Items; if Assigned(Data) then begin FDisableChange := True; try CheckBox1.Checked := Data.enable; Edit27.Text := IntToStr(Data.weight); Edit28.Text := IntToStr(Data.serialNumber); for I := 0 to ScrollBox1.ControlCount - 1 do begin Item := ScrollBox1.Controls[I]; if not Item.Visible then Continue; if Item.Hint = '' then Continue; if Item is TEdit then TEdit(Item).Text := Data.S[Item.Hint] else if Item is TComboBox then TComboBox(Item).Text := Data.S[Item.Hint] else if Item is TMemo then TMemo(Item).Text := Data.S[Item.Hint]; end; finally FDisableChange := False; end; end; end; end. ================================================ FILE: tool/书源整理工具/uFrmMain.dfm ================================================ object Form1: TForm1 Left = 0 Top = 0 Caption = #38405#35835#20070#28304#25972#29702#24037#20855 ClientHeight = 548 ClientWidth = 1145 Color = 15921906 DoubleBuffered = True Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'Tahoma' Font.Pitch = fpFixed Font.Style = [] Menu = MainMenu1 OldCreateOrder = False OnCreate = FormCreate OnDestroy = FormDestroy OnShow = FormShow PixelsPerInch = 96 TextHeight = 13 object Splitter1: TSplitter AlignWithMargins = True Left = 313 Top = 37 Width = 4 Height = 480 Margins.Left = 0 Margins.Top = 37 Margins.Right = 0 Margins.Bottom = 0 Color = clSilver ParentColor = False ExplicitLeft = 257 ExplicitTop = 0 ExplicitHeight = 697 end object Panel1: TPanel Left = 0 Top = 0 Width = 313 Height = 517 Align = alLeft BevelOuter = bvNone Padding.Left = 4 Padding.Top = 4 Padding.Bottom = 4 TabOrder = 1 object SrcList: TListBox AlignWithMargins = True Left = 4 Top = 38 Width = 309 Height = 475 Hint = #23558#20070#28304#25991#20214#25302#20837#27492#22788 Margins.Left = 0 Margins.Top = 2 Margins.Right = 0 Margins.Bottom = 0 Style = lbVirtual Align = alClient BorderStyle = bsNone DoubleBuffered = True Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = #23435#20307 Font.Pitch = fpFixed Font.Style = [] ItemHeight = 18 MultiSelect = True ParentDoubleBuffered = False ParentFont = False ParentShowHint = False PopupMenu = PopupMenu1 ShowHint = True TabOrder = 0 OnClick = SrcListClick OnData = SrcListData OnDblClick = SrcListDblClick OnKeyDown = SrcListKeyDown end object Panel4: TPanel Left = 4 Top = 4 Width = 309 Height = 32 Align = alTop BevelOuter = bvNone DoubleBuffered = True ParentDoubleBuffered = False TabOrder = 1 DesignSize = ( 309 32) object lbCount: TLabel Left = 0 Top = 7 Width = 60 Height = 13 Caption = #20070#28304#21015#34920#65306 Transparent = False end object bookGroupList: TComboBox Left = 80 Top = 3 Width = 223 Height = 21 Anchors = [akLeft, akTop, akRight] DropDownCount = 24 Font.Charset = GB2312_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'Tahoma' Font.Pitch = fpFixed Font.Style = [] ParentFont = False TabOrder = 0 OnChange = bookGroupListChange OnClick = bookGroupListChange end end object StaticText1: TStaticText AlignWithMargins = True Left = 7 Top = 66 Width = 303 Height = 444 Margins.Top = 30 Align = alClient Alignment = taCenter Caption = #40736#26631#25302#20837#20070#28304#25991#20214#21040#27492#22788 Color = clWindow Font.Charset = DEFAULT_CHARSET Font.Color = 6736896 Font.Height = -16 Font.Name = #24494#36719#38597#40657 Font.Pitch = fpFixed Font.Style = [] ParentColor = False ParentFont = False PopupMenu = PopupMenu1 TabOrder = 2 Transparent = False ExplicitLeft = 4 ExplicitTop = 36 ExplicitWidth = 309 ExplicitHeight = 477 end end object Panel2: TPanel Left = 317 Top = 0 Width = 828 Height = 517 Align = alClient BevelOuter = bvNone ParentBackground = False ParentColor = True TabOrder = 0 object Splitter2: TSplitter Left = 0 Top = 372 Width = 828 Height = 4 Cursor = crVSplit Align = alBottom Color = clSilver ParentColor = False ExplicitTop = 373 end object Panel3: TPanel Left = 0 Top = 0 Width = 828 Height = 36 Align = alTop BevelOuter = bvNone TabOrder = 1 object Label1: TLabel Left = 144 Top = 10 Width = 64 Height = 13 Caption = #24037#20316#32447#31243#25968':' Transparent = False end object Button1: TButton Left = 6 Top = 6 Width = 128 Height = 24 Caption = #24320#22987#22788#29702'(&B)' TabOrder = 0 OnClick = Button1Click end object Edit1: TEdit Left = 212 Top = 7 Width = 35 Height = 21 Alignment = taCenter NumbersOnly = True TabOrder = 1 Text = '60' end object CheckBox1: TCheckBox Left = 264 Top = 9 Width = 97 Height = 17 Caption = #21435#38500#37325#22797 Checked = True State = cbChecked TabOrder = 2 end object CheckBox3: TCheckBox Left = 344 Top = 9 Width = 97 Height = 17 Hint = #21435#37325#26102#19981#26816#27979#20070#28304#21517#31216#21644#20998#32452 Caption = #30495#23454#21435#37325 Checked = True State = cbChecked TabOrder = 4 end object CheckBox2: TCheckBox Left = 429 Top = 9 Width = 97 Height = 17 Caption = #26657#39564#20070#28304 TabOrder = 3 end end object EditData: TSynMemo Left = 0 Top = 36 Width = 828 Height = 336 Align = alClient Ctl3D = True ParentCtl3D = False Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -13 Font.Name = 'Courier New' Font.Style = [] PopupMenu = PopupMenu2 TabOrder = 0 CodeFolding.GutterShapeSize = 11 CodeFolding.CollapsedLineColor = clGrayText CodeFolding.FolderBarLinesColor = clGrayText CodeFolding.IndentGuidesColor = clGray CodeFolding.IndentGuides = True CodeFolding.ShowCollapsedLine = False CodeFolding.ShowHintMark = True UseCodeFolding = False BorderStyle = bsNone Gutter.AutoSize = True Gutter.BorderStyle = gbsNone Gutter.Color = cl3DLight Gutter.BorderColor = clWindowFrame Gutter.Font.Charset = DEFAULT_CHARSET Gutter.Font.Color = clWindowText Gutter.Font.Height = -11 Gutter.Font.Name = 'Courier New' Gutter.Font.Style = [] Gutter.ShowLineNumbers = True Highlighter = SynJSONSyn1 WordWrap = True OnChange = EditDataChange FontSmoothing = fsmNone ExplicitHeight = 481 end object Panel5: TPanel Left = 0 Top = 376 Width = 828 Height = 141 Align = alBottom BevelOuter = bvNone TabOrder = 2 ExplicitLeft = 2 DesignSize = ( 828 141) object Label2: TLabel Left = 6 Top = 2 Width = 36 Height = 13 Caption = #26085#24535#65306 Transparent = False end object SpeedButton1: TSpeedButton Left = 783 Top = 0 Width = 43 Height = 20 Anchors = [akTop, akRight] Caption = #28165#31354 Flat = True Font.Charset = DEFAULT_CHARSET Font.Color = clTeal Font.Height = -11 Font.Name = 'Tahoma' Font.Pitch = fpFixed Font.Style = [] ParentFont = False OnClick = SpeedButton1Click end object edtLog: TSynMemo AlignWithMargins = True Left = 0 Top = 20 Width = 828 Height = 121 Margins.Left = 0 Margins.Top = 20 Margins.Right = 0 Margins.Bottom = 0 Align = alClient Ctl3D = False ParentCtl3D = False Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = 'Courier New' Font.Style = [] TabOrder = 0 CodeFolding.GutterShapeSize = 11 CodeFolding.CollapsedLineColor = clGrayText CodeFolding.FolderBarLinesColor = clGrayText CodeFolding.IndentGuidesColor = clGray CodeFolding.IndentGuides = False CodeFolding.ShowCollapsedLine = False CodeFolding.ShowHintMark = True UseCodeFolding = False BookMarkOptions.EnableKeys = False BookMarkOptions.GlyphsVisible = False BorderStyle = bsNone Gutter.AutoSize = True Gutter.BorderStyle = gbsNone Gutter.Color = cl3DLight Gutter.BorderColor = clWindowFrame Gutter.Font.Charset = DEFAULT_CHARSET Gutter.Font.Color = clWindowText Gutter.Font.Height = -11 Gutter.Font.Name = 'Courier New' Gutter.Font.Style = [] Gutter.ShowLineNumbers = True Gutter.Visible = False Gutter.Width = 0 Options = [eoScrollPastEol, eoShowScrollHint, eoSmartTabDelete, eoSmartTabs, eoTabsToSpaces] ReadOnly = True RightEdge = 0 WordWrap = True OnChange = EditDataChange FontSmoothing = fsmClearType ExplicitTop = 36 ExplicitHeight = 481 end end end object StatusBar1: TStatusBar Left = 0 Top = 523 Width = 1145 Height = 25 Panels = < item Width = 500 end item Width = 50 end> end object ProgressBar1: TProgressBar Left = 0 Top = 517 Width = 1145 Height = 6 Align = alBottom Position = 100 TabOrder = 3 Visible = False end object PopupMenu1: TPopupMenu Left = 136 Top = 192 object C3: TMenuItem Caption = #22797#21046#26032#22686'(&A)' OnClick = C3Click end object N7: TMenuItem Caption = #26032#24314#20070#28304'(&N)...' OnClick = N7Click end object E1: TMenuItem Caption = #32534#36753#20070#28304'(&E)...' OnClick = E1Click end object N5: TMenuItem Caption = '-' end object S2: TMenuItem Caption = #25490#24207' - '#20070#28304#21517#31216'(&S)' OnClick = S2Click end object G1: TMenuItem Caption = #25490#24207' - '#20998#32452'(&G)' OnClick = G1Click end object N9: TMenuItem Caption = '-' end object H2: TMenuItem Caption = #20998#32452#21517#31216#26367#25442'(&H)...' OnClick = H2Click end object N6: TMenuItem Caption = '-' end object D1: TMenuItem Caption = #21024#38500#36873#20013#39033'(&D)' OnClick = D1Click end object C1: TMenuItem Caption = #28165#31354'(&C)' OnClick = C1Click end end object SynJSONSyn1: TSynJSONSyn Options.AutoDetectEnabled = False Options.AutoDetectLineLimit = 0 Options.Visible = False Left = 552 Top = 352 end object PopupMenu2: TPopupMenu OnPopup = PopupMenu2Popup Left = 632 Top = 352 object S1: TMenuItem Caption = #20445#23384#20462#25913'(&S)' ShortCut = 16467 OnClick = S1Click end object T1: TMenuItem Caption = #27979#35797#20070#28304'(&T)' OnClick = T1Click end object N3: TMenuItem Caption = '-' end object R1: TMenuItem Caption = #25764#28040'(&R)' OnClick = R1Click end object Z1: TMenuItem Caption = #37325#20570'(&Z)' OnClick = Z1Click end object N1: TMenuItem Caption = '-' end object C2: TMenuItem Caption = #22797#21046'(&C)' OnClick = C2Click end object X1: TMenuItem Caption = #21098#20999'(&X)' OnClick = X1Click end object P1: TMenuItem Caption = #31896#36148'(&P)' OnClick = P1Click end object N2: TMenuItem Caption = '-' end object A1: TMenuItem Caption = #20840#36873'(&A)' OnClick = A1Click end object N4: TMenuItem Caption = '-' end object W1: TMenuItem Caption = #33258#21160#25442#34892'(&W)' OnClick = W1Click end end object Timer1: TTimer Interval = 100 OnTimer = Timer1Timer Left = 464 Top = 440 end object MainMenu1: TMainMenu Left = 448 Top = 136 object F1: TMenuItem Caption = #25991#20214'(&F)' object O1: TMenuItem Caption = #25171#24320#20070#28304#25991#20214'(&O)...' OnClick = O1Click end object A2: TMenuItem Caption = #28155#21152#20070#28304#25991#20214'(&A)...' OnClick = A2Click end object N12: TMenuItem Caption = '-' end object N11: TMenuItem Caption = #26032#24314#20070#28304'(&N)...' OnClick = N7Click end object N10: TMenuItem Caption = '-' end object E2: TMenuItem Caption = #23548#20986#20070#28304#25991#20214'(&E)...' OnClick = E2Click end end object E3: TMenuItem Caption = #32534#36753'(&E)' object H3: TMenuItem Caption = #20998#32452#21517#31216#26367#25442'(&H)...' OnClick = H2Click end end object H1: TMenuItem Caption = #24110#21161'(&H)' object I1: TMenuItem Caption = #20851#20110'(&I)' OnClick = I1Click end object N8: TMenuItem Caption = '-' end object W2: TMenuItem Caption = #20316#32773#21338#23458'(&W)' OnClick = W2Click end object R2: TMenuItem Caption = #23567#35828#38405#35835#22120'(&R)' OnClick = R2Click end end end object SaveDialog1: TSaveDialog Filter = #20070#28304#25991#20214'(*.json)|*.json|'#25152#26377#25991#20214'(*.*)|*.*' Options = [ofHideReadOnly, ofPathMustExist, ofNoReadOnlyReturn, ofEnableSizing] Title = #23548#20986#20070#28304 Left = 472 Top = 352 end object OpenDialog1: TOpenDialog Filter = #20070#28304#25991#20214'(*.json)|*.json|'#25152#26377#25991#20214'(*.*)|*.*' Options = [ofHideReadOnly, ofPathMustExist, ofFileMustExist, ofEnableSizing] Left = 544 Top = 280 end end ================================================ FILE: tool/书源整理工具/uFrmMain.pas ================================================ unit uFrmMain; interface uses iocp.Http.Client, iocp.Utils.Str, YxdJson, YxdStr, YxdHash, YxdWorker, ShellAPI, Math, StrUtils, uBookSourceBean, Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Menus, SynEdit, SynMemo, ComCtrls, SyncObjs, SynEditHighlighter, SynHighlighterJSON, Vcl.Buttons; type PProcessState = ^TProcessState; TProcessState = record STime: Int64; NeedFree: Boolean; Min: Integer; Max: Integer; Value: Integer; end; type TForm1 = class(TForm) Panel1: TPanel; SrcList: TListBox; Splitter1: TSplitter; Panel2: TPanel; PopupMenu1: TPopupMenu; C1: TMenuItem; Panel3: TPanel; EditData: TSynMemo; Button1: TButton; Panel4: TPanel; lbCount: TLabel; bookGroupList: TComboBox; StatusBar1: TStatusBar; ProgressBar1: TProgressBar; SynJSONSyn1: TSynJSONSyn; PopupMenu2: TPopupMenu; S1: TMenuItem; N1: TMenuItem; C2: TMenuItem; X1: TMenuItem; P1: TMenuItem; A1: TMenuItem; N2: TMenuItem; N3: TMenuItem; R1: TMenuItem; Z1: TMenuItem; N4: TMenuItem; W1: TMenuItem; Label1: TLabel; Edit1: TEdit; CheckBox1: TCheckBox; CheckBox2: TCheckBox; D1: TMenuItem; N6: TMenuItem; C3: TMenuItem; N5: TMenuItem; S2: TMenuItem; G1: TMenuItem; N7: TMenuItem; E1: TMenuItem; Timer1: TTimer; CheckBox3: TCheckBox; MainMenu1: TMainMenu; F1: TMenuItem; H1: TMenuItem; E2: TMenuItem; I1: TMenuItem; SaveDialog1: TSaveDialog; W2: TMenuItem; N8: TMenuItem; R2: TMenuItem; N9: TMenuItem; H2: TMenuItem; E3: TMenuItem; H3: TMenuItem; N10: TMenuItem; N11: TMenuItem; T1: TMenuItem; Panel5: TPanel; Splitter2: TSplitter; Label2: TLabel; edtLog: TSynMemo; SpeedButton1: TSpeedButton; StaticText1: TStaticText; O1: TMenuItem; N12: TMenuItem; OpenDialog1: TOpenDialog; A2: TMenuItem; procedure FormCreate(Sender: TObject); procedure C1Click(Sender: TObject); procedure FormShow(Sender: TObject); procedure SrcListKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure FormDestroy(Sender: TObject); procedure SrcListData(Control: TWinControl; Index: Integer; var Data: string); procedure PopupMenu2Popup(Sender: TObject); procedure R1Click(Sender: TObject); procedure Z1Click(Sender: TObject); procedure C2Click(Sender: TObject); procedure X1Click(Sender: TObject); procedure P1Click(Sender: TObject); procedure A1Click(Sender: TObject); procedure S1Click(Sender: TObject); procedure SrcListClick(Sender: TObject); procedure EditDataChange(Sender: TObject); procedure W1Click(Sender: TObject); procedure D1Click(Sender: TObject); procedure C3Click(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure bookGroupListChange(Sender: TObject); procedure N7Click(Sender: TObject); procedure E1Click(Sender: TObject); procedure SrcListDblClick(Sender: TObject); procedure I1Click(Sender: TObject); procedure E2Click(Sender: TObject); procedure W2Click(Sender: TObject); procedure R2Click(Sender: TObject); procedure S2Click(Sender: TObject); procedure G1Click(Sender: TObject); procedure H2Click(Sender: TObject); procedure T1Click(Sender: TObject); procedure SpeedButton1Click(Sender: TObject); procedure O1Click(Sender: TObject); procedure A2Click(Sender: TObject); private { Private declarations } OldListWndProc, OldTextWndProc: TWndMethod; FBookSrcData: JSONArray; FBookGroups: TStringHash; FIsChange, FChanging: Boolean; FCurIndex: Integer; FWaitStop: Integer; FTaskRef: Integer; FTaskStartTime: Int64; FCheckLastTime: Int64; FFilterList: TList; FCurCheckIndex: Integer; FCheckCount: Integer; FWaitCheckBookSourceSingId: Integer; FLocker: TCriticalSection; FStateMsg: string; protected procedure SrcListWndProc(var Message: TMessage); procedure SrcListTextWndProc(var Message: TMessage); procedure AddSrcFiles(ADrop: Integer); procedure WMDropFiles(var Msg: TWMDropFiles); message WM_DROPFILES; public { Public declarations } function CheckItem(Item: TBookSourceItem): Boolean; function CheckSaveState(): Boolean; function CheckBookSourceItem(Item: TBookSourceItem; RaiseErr: Boolean = False; OutLog: TStrings = nil): Boolean; overload; function CheckBookSourceItem(Item: TBookSourceItem; Http: THttpClient; Header: THttpHeaders; RaiseErr: Boolean = False; OutLog: TStrings = nil): Boolean; overload; procedure AddSrcFile(const FileName: string); procedure UpdateBookGroup(Item: TBookSourceItem); procedure RemoveRepeat(AJob: PJob); procedure WaitCheckBookSource(AJob: PJob); procedure DoCheckBookSourceItem(AJob: PJob); procedure TaskFinish(AJob: PJob); procedure DoNotifyDataChange(AJob: PJob); procedure DoUpdateProcess(AJob: PJob); procedure Log(const Msg: string); procedure LogD(const Msg: string); procedure DispLog(); procedure NotifyListChange(Flag: Integer = 0); procedure RemoveSelected(); procedure EditSource(Item: TBookSourceItem); end; var Form1: TForm1; implementation {$R *.dfm} uses uFrmWait, uFrmEditSource, uFrmReplaceGroup; procedure CutOrCopyFiles(FileList: AnsiString; bCopy: Boolean); type PDropFiles = ^TDropFiles; TDropFiles = record pfiles: DWORD; pt: TPoint; fNC: BOOL; fwide: BOOL; end; const DROPEFFECT_COPY = 1; DROPEFFECT_MOVE = 2; var hGblFileList: hGlobal; pFileListDate: Pbyte; HandleDropEffect: UINT; hGblDropEffect: hGlobal; pdwDropEffect: PDWORD; iLen: Integer; begin iLen := Length(FileList) + 2; FileList := FileList + #0#0; hGblFileList := GlobalAlloc(GMEM_ZEROINIT or GMEM_MOVEABLE or GMEM_SHARE, SizeOf(TDropFiles) + iLen); pFileListDate := GlobalLock(hGblFileList); PDropFiles(pFileListDate)^.pfiles := SizeOf(TDropFiles); PDropFiles(pFileListDate)^.pt.Y := 0; PDropFiles(pFileListDate)^.pt.X := 0; PDropFiles(pFileListDate)^.fNC := False; PDropFiles(pFileListDate)^.fwide := False; Inc(pFileListDate, SizeOf(TDropFiles)); CopyMemory(pFileListDate, @FileList[1], iLen); GlobalUnlock(hGblFileList); HandleDropEffect := RegisterClipboardFormat('Preferred DropEffect '); hGblDropEffect := GlobalAlloc(GMEM_ZEROINIT or GMEM_MOVEABLE or GMEM_SHARE, SizeOf(DWORD)); pdwDropEffect := GlobalLock(hGblDropEffect); if (bCopy) then pdwDropEffect^ := DROPEFFECT_COPY else pdwDropEffect^ := DROPEFFECT_MOVE; GlobalUnlock(hGblDropEffect); if OpenClipboard(0) then begin EmptyClipboard(); SetClipboardData(HandleDropEffect, hGblDropEffect); SetClipboardData(CF_HDROP, hGblFileList); CloseClipboard(); end; end; // ļļ #0 ָ procedure CopyFileClipbrd(const FName: string); begin CutOrCopyFiles(AnsiString(FName), True); end; procedure TForm1.AddSrcFiles(ADrop: Integer); var i: Integer; p: array[0..1023] of Char; begin for i := 0 to DragQueryFile(ADrop, $FFFFFFFF, nil, 0) - 1 do begin DragQueryFile(ADrop, i, p, 1024); AddSrcFile(StrPas(p)); end; end; procedure TForm1.bookGroupListChange(Sender: TObject); begin if FChanging then Exit; FChanging := True; try EditData.Text := ''; FIsChange := False; FCurIndex := -1; NotifyListChange(1); finally FChanging := False; end; end; procedure TForm1.Button1Click(Sender: TObject); begin if Button1.Tag = 0 then begin Button1.Tag := 1; Button1.Caption := 'ֹͣ(&S)'; FTaskRef := 0; FWaitStop := 0; FCheckLastTime := GetTimestamp; FTaskStartTime := GetTimestamp; Workers.MaxWorkers := Max(1, StrToIntDef(Edit1.Text, 8)); edtLog.Lines.Clear; Log('ڳʼ...'); Timer1.Enabled := True; ProgressBar1.Min := 0; ProgressBar1.Max := 100; ProgressBar1.Position := 0; if CheckBox1.Checked then begin Inc(FTaskRef); Log('ȥظ...'); Workers.Post(RemoveRePeat, Pointer(Integer(CheckBox3.Checked))); end; if CheckBox2.Checked then begin Inc(FTaskRef); if not CheckBox1.Checked then Workers.SendSignal(FWaitCheckBookSourceSingId); end; ShowWaitDlg(); end else begin AtomicIncrement(FTaskRef); if AtomicDecrement(FTaskRef) <= 0 then begin Button1.Tag := 0; Button1.Caption := 'ʼ(&B)'; Timer1.Enabled := False; FTaskStartTime := 0; ProgressBar1.Visible := False; HideWaitDlg; Log(''); end else begin Button1.Tag := 2; Button1.Caption := 'ֹͣ...'; AtomicIncrement(FWaitStop); end; end; //Application.ProcessMessages; end; procedure TForm1.A1Click(Sender: TObject); begin EditData.SelectAll; end; procedure TForm1.A2Click(Sender: TObject); begin OpenDialog1.Title := 'Դļ'; if OpenDialog1.Execute(Handle) then AddSrcFile(OpenDialog1.FileName); end; procedure TForm1.AddSrcFile(const FileName: string); var Data: JSONArray; Item: TBookSourceItem; I: Integer; begin Data := JSONArray.Create; try Data.LoadFromFile(FileName); I := Data.Count; while I > 0 do begin try Item := TBookSourceItem(Data.O[0]); if Assigned(Item) and (Item.bookSourceUrl <> '') then begin UpdateBookGroup(Item); FBookSrcData.Add(Data.O[0]); end; except end; Dec(I); end; finally Data.Free; NotifyListChange; end; end; procedure TForm1.C1Click(Sender: TObject); begin FBookSrcData.Clear; NotifyListChange; end; procedure TForm1.C2Click(Sender: TObject); begin EditData.CopyToClipboard; end; procedure TForm1.C3Click(Sender: TObject); var S: string; Item, NewItem: TBookSourceItem; begin if SrcList.ItemIndex < 0 then Exit; if not CheckSaveState then Exit; Item := TBookSourceItem(FFilterList[SrcList.ItemIndex]); S := InputBox('Դ', 'Դ', Item.bookSourceName); NewItem := TBookSourceItem(FBookSrcData.AddChildObject); NewItem.Parse(Item.ToString); NewItem.bookSourceName := S; NotifyListChange; if FFilterList.Count > 0 then SrcList.ItemIndex := FFilterList.Count - 1; end; function TForm1.CheckBookSourceItem(Item: TBookSourceItem; RaiseErr: Boolean; OutLog: TStrings): Boolean; var Http: THttpClient; Header: THttpHeaders; begin Result := False; try if not Assigned(Item) then Exit; Http := THttpClient.Create(nil); if Assigned(OutLog) then begin Http.ConnectionTimeOut := 6000; Http.RecvTimeOut := 30000; end else begin Http.ConnectionTimeOut := 30000; Http.RecvTimeOut := 30000; end; Header := THttpHeaders.Create; Result := CheckBookSourceItem(Item, Http, Header, RaiseErr, OutLog); finally FreeAndNil(Http); FreeAndNil(Header); end; end; function TForm1.CheckBookSourceItem(Item: TBookSourceItem; Http: THttpClient; Header: THttpHeaders; RaiseErr: Boolean; OutLog: TStrings): Boolean; function CheckURL(const URL, Title: string; RaiseErr: Boolean = False; Try404: Boolean = False): Boolean; var Resp: THttpResult; Msg: string; begin Result := (URL <> '') and (URL <> '-') and (Pos('http', LowerCase(URL)) = 1); if Result then begin try Resp := Http.Get(UrlEncodeEx(URL), nil, Header); if (Resp.StatusCode = 200) or (Try404 and (Resp.StatusCode = 404)) then begin Result := True; if Assigned(OutLog) then OutLog.Add(Title + 'ӳɹ.'); end else begin Result := False; Msg := Format('%sʧ(StatusCode: %d, %s).', [Title, Resp.StatusCode, URL]); if Assigned(OutLog) then OutLog.Add(Msg); if RaiseErr then raise Exception.Create(Msg); end; except Result := False; Msg := Format('%sԳ(%s).', [Title, Exception(ExceptObject).Message]); if Assigned(OutLog) then OutLog.Add(Msg); if RaiseErr then raise Exception.Create(Msg); end; end else OutLog.Add('Ч' + Title + '.'); end; // ⷢб function CheckFindURL(const Text, Title: string; RaiseErr: Boolean): Boolean; var List: TStrings; I, J, L: Integer; Msg, Item, SubTitle, AURL: string; begin if Text = '' then begin Result := True; Exit; end; try J := 1; while (J > 0) and (J <= Length(Text)) do begin I := PosEx('&&', Text, J); L := 2; if I <= 0 then begin I := PosEx(#$A, Text, J); L := 1; end; if I > 0 then begin Item := MidStr(Text, J, I - J); J := I + L; end else begin Item := Trim(RightStr(Text, Length(Text) - J + 1)); J := Length(Text) + 1; end; if (Item = #$A) or (Item = #13) then Continue; Item := StringReplace(Item, #13, '', [rfReplaceAll]); Item := StringReplace(Item, #10, '', [rfReplaceAll]); Item := StringReplace(Item, '\n', '', [rfReplaceAll, rfIgnoreCase]); Item := Trim(Item); I := Pos('::', Item); if (Item = '') or (I < 1) then begin if Assigned(OutLog) then OutLog.Add('бʽ'); Continue; end else begin SubTitle := Trim(LeftStr(Item, I - 1)); AURL := Trim(RightStr(Item, Length(Item) - I - 1)); CheckURL(AURL, 'б' + SubTitle + ''); end; end; except Result := False; Msg := Format('%sԳ(%s).', [Title, Exception(ExceptObject).Message]); if Assigned(OutLog) then OutLog.Add(Msg); if RaiseErr then raise Exception.Create(Msg); end; end; var Resp: THttpResult; URL: string; T: Int64; begin Result := False; if not Assigned(Item) then Exit; if Item.bookSourceUrl <> '' then begin T := GetTimestamp; Header.Clear; if Item.httpUserAgent <> '' then Header.Add('User-Agent', Item.httpUserAgent); // ԴURL Result := CheckURL(Trim(Item.bookSourceUrl), 'ԴURL', RaiseErr, True); if Result and Assigned(OutLog) then begin // URL CheckURL(Trim(Item.ruleSearchUrl), 'ַ'); // ⷢб CheckFindURL(Trim(Item.ruleFindUrl), '', RaiseErr); end; if Assigned(OutLog) then OutLog.Add(Format('ʱ %d ms.', [GetTimestamp - T])); end else begin if Assigned(OutLog) then OutLog.Add('ԴURLδ.'); raise Exception.Create('ԴURLЧ'); end; end; function TForm1.CheckItem(Item: TBookSourceItem): Boolean; begin Result := Assigned(Item) and (Item.bookSourceUrl <> ''); end; function TForm1.CheckSaveState: Boolean; var LR: Integer; begin if FIsChange and (FCurIndex >= 0) and (FCurIndex < SrcList.Count) then begin LR := MessageBox(Handle, 'ԴѾ޸ģǷ񱣴棿', 'ʾ', 64 + MB_YESNOCANCEL); if LR = IDCANCEL then begin Result := False; Exit; end; if LR = IDYES then S1Click(S1); end; Result := True; end; procedure TForm1.D1Click(Sender: TObject); begin RemoveSelected; end; procedure TForm1.DispLog; begin if FTaskStartTime > 0 then begin if ProgressBar1.Visible then StatusBar1.Panels[1].Text := Format('%s (%d/%d, %d%%) (ʱ: %dms)', [FStateMsg, ProgressBar1.Position, ProgressBar1.Max, Round(ProgressBar1.Position / ProgressBar1.Max * 100), GetTimestamp - FTaskStartTime]) else StatusBar1.Panels[1].Text := Format('%s (ʱ: %dms)', [FStateMsg, GetTimestamp - FTaskStartTime]) end else StatusBar1.Panels[1].Text := FStateMsg; end; procedure TForm1.DoCheckBookSourceItem(AJob: PJob); var Item: TBookSourceItem; State: PProcessState; V: Integer; IsOK: Boolean; Http: THttpClient; Header: THttpHeaders; begin V := 0; try Http := THttpClient.Create(nil); Http.ConnectionTimeOut := 30000; Http.RecvTimeOut := 30000; Header := THttpHeaders.Create; while (not AJob.IsTerminated) and (FWaitStop = 0) do begin V := AtomicIncrement(FCurCheckIndex) - 1; FLocker.Enter; if (GetTimestamp - FCheckLastTime) > 100 then begin FCheckLastTime := GetTimestamp; New(State); State.Min := 0; State.Max := FCheckCount; State.Value := V; Workers.Post(DoUpdateProcess, State, True); Sleep(10); end; FLocker.Leave; if V < FCheckCount then begin Item := TBookSourceItem(FBookSrcData.O[V]); if not Assigned(Item) then Exit; try IsOK := CheckBookSourceItem(Item, Http, Header); except IsOK := False; end; if IsOK then Item.RemoveGroup('ʧЧ') else Item.AddGroup('ʧЧ'); end else Break; end; finally if (V >= FCheckCount) or (FWaitStop > 0) then begin Sleep(100); Workers.Post(TaskFinish, nil, True); end; FreeAndNil(Http); FreeAndNil(Header); end; end; procedure TForm1.DoNotifyDataChange(AJob: PJob); begin NotifyListChange; end; procedure TForm1.DoUpdateProcess(AJob: PJob); var V: PProcessState; begin if not Assigned(Self) then Exit; V := AJob.Data; if V = nil then ProgressBar1.Visible := False else begin ProgressBar1.Min := V.Min; ProgressBar1.Max := V.Max; ProgressBar1.Position := V.Value; ProgressBar1.Visible := Button1.Tag <> 0; if V.NeedFree then Dispose(V); end; end; procedure TForm1.E1Click(Sender: TObject); begin if SrcList.ItemIndex < 0 then Exit; EditSource(TBookSourceItem(FFilterList[SrcList.ItemIndex])); end; procedure TForm1.E2Click(Sender: TObject); var FName: JSONString; begin if SaveDialog1.Execute(Handle) then begin FName := SaveDialog1.FileName; if ExtractFileExt(FName) = '' then FName := FName + '.json'; FBookSrcData.SaveToFile(FName, 4, YxdStr.TTextEncoding.teUTF8, False); end; end; procedure TForm1.EditDataChange(Sender: TObject); begin FIsChange := True; end; procedure TForm1.EditSource(Item: TBookSourceItem); begin ShowEditSource(Item, procedure (Item: TBookSourceItem) begin if FBookSrcData.IndexOfObject(Item) < 0 then FBookSrcData.Add(JSONObject(Item)); NotifyListChange; if (FCurIndex >= 0) and (FCurIndex < FFilterList.Count) then begin if TObject(FFilterList[FCurIndex]) = Item then begin EditData.Text := TBookSourceItem(FFilterList[FCurIndex]).ToString(4); FIsChange := False; end; end; end ); end; procedure TForm1.FormCreate(Sender: TObject); begin JsonNameAfterSpace := True; JsonCaseSensitive := False; FBookSrcData := JSONArray.Create; FBookGroups := TStringHash.Create(997); FFilterList := TList.Create; FLocker := TCriticalSection.Create; FWaitCheckBookSourceSingId := Workers.RegisterSignal('WaitCheckBookSource'); Workers.PostWait(WaitCheckBookSource, FWaitCheckBookSourceSingId); end; procedure TForm1.FormDestroy(Sender: TObject); begin FreeAndNil(FBookSrcData); FreeAndNil(FBookGroups); FreeAndNil(FFilterList); FreeAndNil(FLocker); end; procedure TForm1.FormShow(Sender: TObject); begin DragAcceptFiles(SrcList.Handle, True); DragAcceptFiles(StaticText1.Handle, True); OldListWndProc := SrcList.WindowProc; OldTextWndProc := StaticText1.WindowProc; SrcList.WindowProc := SrcListWndProc; StaticText1.WindowProc := SrcListTextWndProc; NotifyListChange; end; procedure TForm1.G1Click(Sender: TObject); begin FBookSrcData.Sort( function (A, B: Pointer): Integer var Item1: PJSONValue absolute A; Item2: PJSONValue absolute B; S1, S2: string; begin if (Item1.FType = Item2.FType) and (Item1.FType = jdtObject) and (Item1.AsJsonObject <> nil) and (Item2.AsJsonObject <> nil) then begin S1 := TBookSourceItem(Item1.AsJsonObject).bookSourceGroup; S2 := TBookSourceItem(Item2.AsJsonObject).bookSourceGroup; Result := CompareStr(S1, S2); end else Result := 0; end ); NotifyListChange(1); end; procedure TForm1.H2Click(Sender: TObject); var FindStr, NewStr: string; I, Flag: Integer; Item: TBookSourceItem; begin if ShowReplaceGroup(Self, FindStr, NewStr, Flag) then begin if (FindStr <> '') and (Flag = 0) then Exit; for I := 0 to FBookSrcData.Count - 1 do begin Item := TBookSourceItem(FBookSrcData.O[I]); if not Assigned(Item) then Continue; if Flag = 0 then begin Item.bookSourceGroup := StringReplace(Trim(Item.bookSourceGroup), FindStr, NewStr, [rfReplaceAll, rfIgnoreCase]); end else begin if NewStr = '' then Item.RemoveGroup(FindStr) else Item.ReplaceGroup(FindStr, NewStr); end; end; NotifyListChange(); end; end; procedure TForm1.I1Click(Sender: TObject); var Msg: string; begin Msg := Application.Title + sLineBreak + 'YangYxd Ȩ 2019'; MessageBox(Handle, PChar(Msg), '', 64); end; procedure TForm1.Log(const Msg: string); begin LogD(Msg); FStateMsg := Msg; DispLog(); end; procedure TForm1.LogD(const Msg: string); begin edtLog.Lines.Add(Format('[%s] %s', [FormatDateTime('hh:mm:ss.zzz', Now), Msg])); end; procedure TForm1.N7Click(Sender: TObject); var Item: TBookSourceItem; begin Item := TBookSourceItem(JSONObject.Create); EditSource(Item); end; procedure TForm1.NotifyListChange(Flag: Integer); var I, J: Integer; Key: string; Item: TBookSourceItem; begin J := FCurIndex; if Flag = 0 then begin for I := 0 to FBookSrcData.Count - 1 do UpdateBookGroup(TBookSourceItem(FBookSrcData.O[I])); bookGroupList.Items.Clear; FBookGroups.GetKeyList(bookGroupList.Items); end; FFilterList.Clear; Key := LowerCase(bookGroupList.Text); if Key <> '' then begin for I := 0 to FBookSrcData.Count - 1 do begin Item := TBookSourceItem(FBookSrcData.O[I]); if (Pos(Key, Item.bookSourceGroup) > 0) or (Pos(Key, Item.bookSourceName) > 0) then FFilterList.Add(Item); end; end else begin for I := 0 to FBookSrcData.Count - 1 do FFilterList.Add(FBookSrcData.O[I]); end; SrcList.Count := FFilterList.Count; StaticText1.Visible := SrcList.Count = 0; if (J < SrcList.Count) and (J >= 0) then begin SrcList.ClearSelection; SrcList.ItemIndex := J; SrcList.Selected[J] := True; end; SrcList.ShowHint := SrcList.Count = 0; StatusBar1.Panels[0].Text := Format('Դ%d, ǰ: %d', [FBookSrcData.Count, FFilterList.Count]); end; procedure TForm1.O1Click(Sender: TObject); begin OpenDialog1.Title := 'Դļ'; if OpenDialog1.Execute(Handle) then begin FBookSrcData.Clear; AddSrcFile(OpenDialog1.FileName); end; end; procedure TForm1.P1Click(Sender: TObject); begin EditData.PasteFromClipboard; end; procedure TForm1.PopupMenu2Popup(Sender: TObject); begin S1.Enabled := SrcList.ItemIndex >= 0; P1.Enabled := EditData.CanPaste; X1.Enabled := EditData.SelLength > 0; C2.Enabled := X1.Enabled; R1.Enabled := EditData.CanUndo; Z1.Enabled := EditData.CanRedo; W1.Checked := EditData.WordWrap; end; procedure TForm1.R1Click(Sender: TObject); begin EditData.Undo; end; procedure TForm1.R2Click(Sender: TObject); begin ShellExecute(0, 'OPEN', PChar('https://github.com/yangyxd/MyBookshelf'), nil, nil, SW_SHOWMAXIMIZED) end; procedure TForm1.RemoveRepeat(AJob: PJob); var CheckName: Boolean; function Equals(A, B: TBookSourceItem): Boolean; begin Result := (LowerCase(A.bookSourceUrl) = LowerCase(B.bookSourceUrl)) and (LowerCase(A.loginUrl) = LowerCase(B.loginUrl)) and (LowerCase(A.ruleBookContent) = LowerCase(B.ruleBookContent)) and (LowerCase(A.httpUserAgent) = LowerCase(B.httpUserAgent)) and (LowerCase(A.ruleBookKind) = LowerCase(B.ruleBookKind)) and (LowerCase(A.ruleBookLastChapter) = LowerCase(B.ruleBookLastChapter)) and (LowerCase(A.ruleBookName) = LowerCase(B.ruleBookName)) and (LowerCase(A.ruleBookUrlPattern) = LowerCase(B.ruleBookUrlPattern)) and (LowerCase(A.ruleChapterList) = LowerCase(B.ruleChapterList)) and (LowerCase(A.ruleChapterName) = LowerCase(B.ruleChapterName)) and (LowerCase(A.ruleChapterUrl) = LowerCase(B.ruleChapterUrl)) and (LowerCase(A.ruleChapterUrlNext) = LowerCase(B.ruleChapterUrlNext)) and (LowerCase(A.ruleContentUrl) = LowerCase(B.ruleContentUrl)) and (LowerCase(A.ruleContentUrlNext) = LowerCase(B.ruleContentUrlNext)) and (LowerCase(A.ruleCoverUrl) = LowerCase(B.ruleCoverUrl)) and (LowerCase(A.ruleFindUrl) = LowerCase(B.ruleFindUrl)) and (LowerCase(A.ruleIntroduce) = LowerCase(B.ruleIntroduce)) and (LowerCase(A.ruleSearchAuthor) = LowerCase(B.ruleSearchAuthor)) and (LowerCase(A.ruleSearchCoverUrl) = LowerCase(B.ruleSearchCoverUrl)) and (LowerCase(A.ruleSearchKind) = LowerCase(B.ruleSearchKind)) and (LowerCase(A.ruleSearchLastChapter) = LowerCase(B.ruleSearchLastChapter)) and (LowerCase(A.ruleSearchList) = LowerCase(B.ruleSearchList)) and (LowerCase(A.ruleSearchName) = LowerCase(B.ruleSearchName)) and (LowerCase(A.ruleSearchNoteUrl) = LowerCase(B.ruleSearchNoteUrl)) and (LowerCase(A.ruleSearchUrl) = LowerCase(B.ruleSearchUrl)); if not CheckName then Result := Result and (LowerCase(A.bookSourceName) = LowerCase(B.bookSourceName)) and (LowerCase(A.bookSourceGroup) = LowerCase(B.bookSourceGroup)); end; var I, J, LastCount, ST: Integer; Item: TBookSourceItem; T: TProcessState; State: PProcessState; begin I := 0; LastCount := FBookSrcData.Count; CheckName := Boolean(Integer(AJob.Data)); T.STime := GetTimestamp; T.Min := 0; T.Value := 0; ST := 1000; try while I < FBookSrcData.Count do begin Item := TBookSourceItem(FBookSrcData.O[I]); Inc(I); for J := FBookSrcData.Count - 1 downto I do begin if Equals(Item, TBookSourceItem(FBookSrcData.O[J])) then FBookSrcData.Remove(J); end; if AJob.IsTerminated or (FWaitStop > 0) then Break; if GetTimestamp - T.STime > ST then begin ST := 200; T.Value := I; T.Max := FBookSrcData.Count; New(State); State^ := T; State.NeedFree := True; Workers.Post(DoUpdateProcess, State, True); end; end; finally if LastCount <> FBookSrcData.Count then Workers.Post(DoNotifyDataChange, nil, True); Sleep(100); Workers.Post(TaskFinish, nil, True); end; end; procedure TForm1.RemoveSelected; var I, V: Integer; begin for I := SrcList.Count - 1 downto 0 do begin if SrcList.Selected[I] then begin V := FBookSrcData.IndexOfObject(JSONObject(FFilterList[I])); if V >= 0 then FBookSrcData.Remove(V); end; end; NotifyListChange; end; procedure TForm1.S1Click(Sender: TObject); var S: string; Item: TBookSourceItem; begin if (FCurIndex < 0) or (FCurIndex >= SrcList.Count) then Exit; Item := TBookSourceItem(FFilterList[FCurIndex]); if not Assigned(Item) then Exit; try FIsChange := False; S := Item.ToString(); Item.Parse(EditData.Text); if not CheckItem(Item) then Item.Parse(S); finally NotifyListChange; end; end; procedure TForm1.S2Click(Sender: TObject); begin FBookSrcData.Sort( function (A, B: Pointer): Integer var Item1: PJSONValue absolute A; Item2: PJSONValue absolute B; S1, S2: string; begin if (Item1.FType = Item2.FType) and (Item1.FType = jdtObject) and (Item1.AsJsonObject <> nil) and (Item2.AsJsonObject <> nil) then begin S1 := TBookSourceItem(Item1.AsJsonObject).bookSourceName; S2 := TBookSourceItem(Item2.AsJsonObject).bookSourceName; Result := CompareStr(S1, S2); end else Result := 0; end ); NotifyListChange(1); end; procedure TForm1.SpeedButton1Click(Sender: TObject); begin edtLog.Lines.Clear; end; procedure TForm1.SrcListClick(Sender: TObject); begin if SrcList.ItemIndex < 0 then Exit; if not CheckSaveState then Exit; FCurIndex := SrcList.ItemIndex; EditData.Text := TBookSourceItem(FFilterList[FCurIndex]).ToString(4); FIsChange := False; end; procedure TForm1.SrcListData(Control: TWinControl; Index: Integer; var Data: string); var Item: TBookSourceItem; begin if Index < FBookSrcData.Count then begin Item := TBookSourceItem(FFilterList[index]); Data := Format('%s%s', [Item.bookSourceGroup, Item.bookSourceName]); end else Data := ''; end; procedure TForm1.SrcListDblClick(Sender: TObject); begin E1Click(E1); end; procedure TForm1.SrcListKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure PasteItems(); var pGlobal : Thandle; begin OpenClipboard(Handle); try pGlobal := GetClipboardData(CF_HDROP); //ȡļ if pGlobal > 0 then AddSrcFiles(pGlobal); finally CloseClipboard; end; end; begin if Key = VK_DELETE then begin RemoveSelected(); end else if Key = Ord('V') then begin // ճ PasteItems(); end; end; procedure TForm1.SrcListTextWndProc(var Message: TMessage); begin if Message.Msg = WM_DROPFILES then WMDropFiles(TWMDropFiles(Message)) else OldTextWndProc(Message); end; procedure TForm1.SrcListWndProc(var Message: TMessage); begin if Message.Msg = WM_DROPFILES then WMDropFiles(TWMDropFiles(Message)) else OldListWndProc(Message); end; procedure TForm1.T1Click(Sender: TObject); var Item: TBookSourceItem; Msg: TStrings; begin Item := TBookSourceItem(JSONObject.Create); try Item.Parse(EditData.Text); if CheckBookSourceItem(Item, True, edtLog.Lines) then LogD('ϲ, ͨ!') else LogD('Դ쳣!'); finally FreeAndNil(Item); end; end; procedure TForm1.TaskFinish(AJob: PJob); var I: Integer; begin I := AtomicDecrement(FTaskRef); if (I <= 0) or (FWaitStop > 0) then begin if (I = 0) and Assigned(Self) and (not (csDestroying in ComponentState)) then begin NotifyListChange(); Button1Click(Button1); end; end else if not (csDestroying in ComponentState) then begin Log('УԴ...'); Workers.SendSignal(FWaitCheckBookSourceSingId); end; end; procedure TForm1.Timer1Timer(Sender: TObject); begin DispLog; end; procedure TForm1.UpdateBookGroup(Item: TBookSourceItem); var J: Integer; ARef: Number; ABookGroup: TArray; AGroup: string; begin ABookGroup := Item.GetGroupList; for J := 0 to High(ABookGroup) do begin ARef := 0; AGroup := Trim(ABookGroup[J]); FBookGroups.TryGetValue(AGroup, ARef); Inc(ARef); FBookGroups.AddOrUpdate(AGroup, ARef); end; end; procedure TForm1.W1Click(Sender: TObject); begin EditData.WordWrap := not W1.Checked; end; procedure TForm1.W2Click(Sender: TObject); begin ShellExecute(0, 'OPEN', PChar('http://www.cnblogs.com/yangyxd/'), nil, nil, SW_SHOWMAXIMIZED) end; procedure TForm1.WaitCheckBookSource(AJob: PJob); var I, J: Integer; begin if FBookSrcData.Count > 0 then begin FCheckCount := FBookSrcData.Count; FCurCheckIndex := 0; J := Min(FBookSrcData.Count, Workers.MaxWorkers - 1); for I := 0 to J - 1 do begin if AJob.IsTerminated then Break; Workers.Post(DoCheckBookSourceItem, nil); end; end else Workers.Post(TaskFinish, nil, True); end; procedure TForm1.WMDropFiles(var Msg: TWMDropFiles); begin AddSrcFiles(Msg.Drop); end; procedure TForm1.X1Click(Sender: TObject); begin EditData.CopyToClipboard; EditData.SelText := ''; end; procedure TForm1.Z1Click(Sender: TObject); begin EditData.Redo; end; end. ================================================ FILE: tool/书源整理工具/uFrmReplaceGroup.dfm ================================================ object frmReplaceGroup: TfrmReplaceGroup Left = 0 Top = 0 BorderStyle = bsDialog Caption = #26367#25442' - '#20070#28304#20998#32452 ClientHeight = 166 ClientWidth = 345 Color = clWindow Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'Tahoma' Font.Style = [] OldCreateOrder = False Position = poMainFormCenter DesignSize = ( 345 166) PixelsPerInch = 96 TextHeight = 13 object Label1: TLabel Left = 24 Top = 16 Width = 60 Height = 13 Caption = #26597#25214#20869#23481#65306 end object Label2: TLabel Left = 24 Top = 63 Width = 60 Height = 13 Caption = #26367#25442#20869#23481#65306 end object Edit1: TEdit Left = 24 Top = 35 Width = 289 Height = 21 TabOrder = 0 end object Edit2: TEdit Left = 24 Top = 82 Width = 289 Height = 21 TabOrder = 1 end object CheckBox1: TCheckBox Left = 24 Top = 128 Width = 97 Height = 17 Caption = #20840#23383#21305#37197 Checked = True State = cbChecked TabOrder = 2 end object Button1: TButton Left = 239 Top = 125 Width = 75 Height = 25 Anchors = [akRight, akBottom] Caption = #30830#23450'(&O)' Default = True TabOrder = 3 OnClick = Button1Click ExplicitLeft = 234 ExplicitTop = 176 end object Button2: TButton Left = 151 Top = 125 Width = 75 Height = 25 Anchors = [akRight, akBottom] Cancel = True Caption = #21462#28040'(&C)' ModalResult = 2 TabOrder = 4 ExplicitLeft = 146 ExplicitTop = 176 end end ================================================ FILE: tool/书源整理工具/uFrmReplaceGroup.pas ================================================ unit uFrmReplaceGroup; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TfrmReplaceGroup = class(TForm) Label1: TLabel; Edit1: TEdit; Label2: TLabel; Edit2: TEdit; CheckBox1: TCheckBox; Button1: TButton; Button2: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var frmReplaceGroup: TfrmReplaceGroup; function ShowReplaceGroup(Sender: TComponent; var FindStr, NewStr: string; var Flag: Integer): Boolean; implementation {$R *.dfm} function ShowReplaceGroup(Sender: TComponent; var FindStr, NewStr: string; var Flag: Integer): Boolean; var F: TfrmReplaceGroup; begin F := TfrmReplaceGroup.Create(Sender); try Result := F.ShowModal = mrOk; if Result then begin FindStr := Trim(F.Edit1.Text); NewStr := Trim(F.Edit2.Text); Flag := Ord(F.CheckBox1.Checked); end; finally F.Free; end; end; procedure TfrmReplaceGroup.Button1Click(Sender: TObject); begin // if Trim(Edit1.Text) = '' then begin // ShowMessage('Ҫҵ'); // Exit; // end; ModalResult := mrOk; end; end. ================================================ FILE: tool/书源整理工具/uFrmWait.dfm ================================================ object Form2: TForm2 Left = 0 Top = 0 BorderStyle = bsNone BorderWidth = 1 Caption = 'Form2' ClientHeight = 173 ClientWidth = 251 Color = clSilver Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'Tahoma' Font.Style = [] OldCreateOrder = False Position = poOwnerFormCenter OnClose = FormClose PixelsPerInch = 96 TextHeight = 13 object Panel1: TPanel Left = 0 Top = 0 Width = 251 Height = 173 Align = alClient BevelOuter = bvNone Color = clWindow ParentBackground = False TabOrder = 0 object Label1: TLabel Left = 73 Top = 88 Width = 103 Height = 13 Alignment = taCenter Caption = #27491#22312#22788#29702', '#35831#31561#24453'...' end object ActivityIndicator1: TActivityIndicator AlignWithMargins = True Left = 112 Top = 32 Animate = True end object Button1: TButton Left = 71 Top = 120 Width = 106 Height = 25 Cancel = True Caption = #21462#28040'(&C)' TabOrder = 1 OnClick = Button1Click end end object Timer1: TTimer Enabled = False Interval = 100 OnTimer = Timer1Timer Left = 184 Top = 56 end end ================================================ FILE: tool/书源整理工具/uFrmWait.pas ================================================ unit uFrmWait; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.WinXCtrls, Vcl.ExtCtrls; type TForm2 = class(TForm) Panel1: TPanel; ActivityIndicator1: TActivityIndicator; Label1: TLabel; Button1: TButton; Timer1: TTimer; procedure Button1Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Timer1Timer(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form2: TForm2; procedure ShowWaitDlg(); procedure HideWaitDlg(); implementation {$R *.dfm} uses uFrmMain; var fWait: TForm2; procedure ShowWaitDlg(); begin if Assigned(fWait) then Exit; fWait := TForm2.Create(Application); fWait.ShowModal; end; procedure HideWaitDlg(); begin if Assigned(fWait) then fWait.Timer1.Enabled := True; end; procedure TForm2.Button1Click(Sender: TObject); begin if Form1.Button1.Tag <> 0 then begin Timer1.Enabled := True; Form1.Button1Click(Button1); end; end; procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction); begin fWait := nil; Action := caFree; end; procedure TForm2.Timer1Timer(Sender: TObject); begin if Form1.Button1.Tag = 0 then begin ModalResult := mrCancel; end; end; end.