Repository: jordyamc/UKIKU Branch: master Commit: f0f8acc010cd Files: 1147 Total size: 82.1 MB Directory structure: gitextract_do_1h07p/ ├── .github/ │ ├── FUNDING.yml │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app/ │ ├── .gitattributes │ ├── .gitignore │ ├── amazon/ │ │ └── app-amazon.apk │ ├── build.gradle │ ├── fabric.properties │ ├── proguard-android.txt │ ├── proguard-rules.pro │ ├── release/ │ │ └── app-release.apk │ ├── src/ │ │ ├── amazon/ │ │ │ ├── AndroidManifest.xml │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ ├── ic_launcher_foregound.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── menu/ │ │ │ │ └── activity_main_drawer_drawer.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values/ │ │ │ │ ├── bool.xml │ │ │ │ ├── google_maps_api.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ └── xml/ │ │ │ └── preferences.xml │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── knf/ │ │ │ └── kuma/ │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── debug/ │ │ │ └── res/ │ │ │ └── xml/ │ │ │ └── preferences.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── assets/ │ │ │ │ └── changelog.xml │ │ │ ├── java/ │ │ │ │ └── knf/ │ │ │ │ └── kuma/ │ │ │ │ ├── App.kt │ │ │ │ ├── AppInfoActivity.kt │ │ │ │ ├── AppInfoActivityMaterial.kt │ │ │ │ ├── AppInfoFragment.kt │ │ │ │ ├── BottomFragment.kt │ │ │ │ ├── Diagnostic.kt │ │ │ │ ├── DiagnosticMaterial.kt │ │ │ │ ├── Main.kt │ │ │ │ ├── MainMaterial.kt │ │ │ │ ├── SplashActivity.kt │ │ │ │ ├── achievements/ │ │ │ │ │ ├── AchievementActivity.kt │ │ │ │ │ ├── AchievementActivityMaterial.kt │ │ │ │ │ ├── AchievementAdapter.kt │ │ │ │ │ ├── AchievementFragment.kt │ │ │ │ │ ├── AchievementManager.kt │ │ │ │ │ ├── AchievementsFragmentsPagerAdapter.kt │ │ │ │ │ ├── AchievementsPagerAdapter.kt │ │ │ │ │ └── LevelCalculator.kt │ │ │ │ ├── ads/ │ │ │ │ │ ├── AdAnimeObject.kt │ │ │ │ │ ├── AdCallback.kt │ │ │ │ │ ├── AdCardItemHolder.kt │ │ │ │ │ ├── AdFavoriteObject.kt │ │ │ │ │ ├── AdRecentObject.kt │ │ │ │ │ ├── AdsUtils.kt │ │ │ │ │ ├── AdsUtilsBrains.kt │ │ │ │ │ ├── AdsUtilsLovin.kt │ │ │ │ │ ├── AdsUtilsMob.kt │ │ │ │ │ ├── NativeManager.kt │ │ │ │ │ └── SubscriptionReceiver.kt │ │ │ │ ├── animeinfo/ │ │ │ │ │ ├── ActivityAnime.kt │ │ │ │ │ ├── ActivityAnimeMaterial.kt │ │ │ │ │ ├── AnimeBroadcast.kt │ │ │ │ │ ├── AnimeChaptersAdapter.kt │ │ │ │ │ ├── AnimeChaptersAdapterMaterial.kt │ │ │ │ │ ├── AnimeInfo.kt │ │ │ │ │ ├── AnimePagerAdapter.kt │ │ │ │ │ ├── AnimePagerAdapterMaterial.kt │ │ │ │ │ ├── AnimeRelatedAdapter.kt │ │ │ │ │ ├── AnimeRelatedAdapterMaterial.kt │ │ │ │ │ ├── AnimeTagsAdapter.kt │ │ │ │ │ ├── AnimeTagsAdapterMaterial.kt │ │ │ │ │ ├── AnimeViewModel.kt │ │ │ │ │ ├── BottomActionsDialog.kt │ │ │ │ │ ├── ChapterObjWrap.kt │ │ │ │ │ ├── CommentariesDialog.kt │ │ │ │ │ ├── DownloadedObserver.kt │ │ │ │ │ ├── fragments/ │ │ │ │ │ │ ├── ChaptersFragment.kt │ │ │ │ │ │ ├── ChaptersFragmentMaterial.kt │ │ │ │ │ │ ├── DetailsFragment.kt │ │ │ │ │ │ └── DetailsFragmentMaterial.kt │ │ │ │ │ ├── img/ │ │ │ │ │ │ ├── ActivityImgFull.kt │ │ │ │ │ │ ├── ImgFullFragment.kt │ │ │ │ │ │ └── ImgPagerAdapter.kt │ │ │ │ │ ├── ktx/ │ │ │ │ │ │ └── Extensions.kt │ │ │ │ │ └── viewholders/ │ │ │ │ │ ├── AnimeActivityHolder.kt │ │ │ │ │ ├── AnimeActivityMaterialHolder.kt │ │ │ │ │ ├── AnimeChaptersHolder.kt │ │ │ │ │ ├── AnimeChaptersMaterialHolder.kt │ │ │ │ │ ├── AnimeDetailsHolder.kt │ │ │ │ │ └── AnimeDetailsMaterialHolder.kt │ │ │ │ ├── backup/ │ │ │ │ │ ├── BackUpActivity.kt │ │ │ │ │ ├── Backups.kt │ │ │ │ │ ├── MigrationActivity.kt │ │ │ │ │ ├── firestore/ │ │ │ │ │ │ ├── FirestoreManager.kt │ │ │ │ │ │ ├── QueueManager.kt │ │ │ │ │ │ └── data/ │ │ │ │ │ │ ├── AchievementsData.kt │ │ │ │ │ │ ├── EAData.kt │ │ │ │ │ │ ├── FavsData.kt │ │ │ │ │ │ ├── GenresData.kt │ │ │ │ │ │ ├── HistoryData.kt │ │ │ │ │ │ ├── QueueData.kt │ │ │ │ │ │ ├── SeeingData.kt │ │ │ │ │ │ ├── SeenData.kt │ │ │ │ │ │ └── TopData.kt │ │ │ │ │ ├── framework/ │ │ │ │ │ │ ├── BackupService.kt │ │ │ │ │ │ ├── DropBoxService.kt │ │ │ │ │ │ └── LocalService.kt │ │ │ │ │ ├── objects/ │ │ │ │ │ │ ├── AnimeChapters.kt │ │ │ │ │ │ ├── BackupObject.kt │ │ │ │ │ │ ├── FavList.kt │ │ │ │ │ │ └── SeenList.kt │ │ │ │ │ └── screens/ │ │ │ │ │ ├── MigrateDirectoryFragment.kt │ │ │ │ │ ├── MigrateSuccessFragment.kt │ │ │ │ │ └── MigrateVersionFragment.kt │ │ │ │ ├── cast/ │ │ │ │ │ ├── CastCustom.kt │ │ │ │ │ ├── CastMedia.kt │ │ │ │ │ ├── CastNotificationHelper.kt │ │ │ │ │ └── ProxyCache.kt │ │ │ │ ├── changelog/ │ │ │ │ │ ├── ChangeAdapter.kt │ │ │ │ │ ├── ChangeAdapterMaterial.kt │ │ │ │ │ ├── ChangelogActivity.kt │ │ │ │ │ ├── ChangelogActivityMaterial.kt │ │ │ │ │ ├── ReleaseAdapter.kt │ │ │ │ │ ├── ReleaseAdapterMaterial.kt │ │ │ │ │ └── objects/ │ │ │ │ │ ├── Change.kt │ │ │ │ │ ├── Changelog.kt │ │ │ │ │ └── Release.kt │ │ │ │ ├── commons/ │ │ │ │ │ ├── AllSSLOkHttpClient.kt │ │ │ │ │ ├── BypassUtil.kt │ │ │ │ │ ├── CastUtil.kt │ │ │ │ │ ├── ChannelTools.kt │ │ │ │ │ ├── CipherExt.kt │ │ │ │ │ ├── DesignUtils.kt │ │ │ │ │ ├── EAHelper.kt │ │ │ │ │ ├── EAMapActivity.kt │ │ │ │ │ ├── Economy.kt │ │ │ │ │ ├── Encryption.java │ │ │ │ │ ├── ExtensionUtils.kt │ │ │ │ │ ├── FileUtil.kt │ │ │ │ │ ├── FileWrapper.kt │ │ │ │ │ ├── Network.kt │ │ │ │ │ ├── NoSSLOkHttpClient.kt │ │ │ │ │ ├── PatternUtil.kt │ │ │ │ │ ├── PicassoSingle.kt │ │ │ │ │ ├── PrefsUtil.kt │ │ │ │ │ ├── SSLSkipper.kt │ │ │ │ │ ├── SelfServer.kt │ │ │ │ │ ├── SharedPreferenceLiveData.kt │ │ │ │ │ └── ThumbsDownloader.kt │ │ │ │ ├── custom/ │ │ │ │ │ ├── AchievementUnlocked.java │ │ │ │ │ ├── AppWebView.kt │ │ │ │ │ ├── BackgroundExecutor.kt │ │ │ │ │ ├── BannerContainerView.kt │ │ │ │ │ ├── CenterLayoutManager.kt │ │ │ │ │ ├── ConnectionState.kt │ │ │ │ │ ├── ExpandableTV.kt │ │ │ │ │ ├── ExpandableTextView.kt │ │ │ │ │ ├── FSGridRecyclerView.kt │ │ │ │ │ ├── FSRecyclerView.kt │ │ │ │ │ ├── FixedGridLayoutManager.kt │ │ │ │ │ ├── GenericActivity.kt │ │ │ │ │ ├── GridRecyclerView.kt │ │ │ │ │ ├── HiddenOverlay.kt │ │ │ │ │ ├── HomeList.kt │ │ │ │ │ ├── ListPreferenceDialogFragmentCompat.kt │ │ │ │ │ ├── MainExecutor.kt │ │ │ │ │ ├── PreferenceFragmentCompat.java │ │ │ │ │ ├── SSLManager.kt │ │ │ │ │ ├── SeenAnimeOverlay.kt │ │ │ │ │ ├── SingleFragmentActivity.kt │ │ │ │ │ ├── SingleFragmentMaterialActivity.kt │ │ │ │ │ ├── StateView.kt │ │ │ │ │ ├── StateViewMaterial.kt │ │ │ │ │ ├── SyncItemView.kt │ │ │ │ │ ├── SyncStaticItemView.kt │ │ │ │ │ ├── ThemedControlsActivity.kt │ │ │ │ │ ├── TlsOnlySocketFactory.java │ │ │ │ │ ├── VariantGridLayoutManager.kt │ │ │ │ │ ├── VariantLinearLayoutManager.kt │ │ │ │ │ ├── WrapWebView.kt │ │ │ │ │ ├── exceptions/ │ │ │ │ │ │ └── EJNFException.kt │ │ │ │ │ └── snackbar/ │ │ │ │ │ ├── SnackProgressBar.kt │ │ │ │ │ ├── SnackProgressBarCore.kt │ │ │ │ │ ├── SnackProgressBarLayout.kt │ │ │ │ │ └── SnackProgressBarManager.kt │ │ │ │ ├── database/ │ │ │ │ │ ├── BaseConverter.kt │ │ │ │ │ ├── CacheDB.kt │ │ │ │ │ ├── CacheDBWrap.java │ │ │ │ │ ├── EADB.kt │ │ │ │ │ └── dao/ │ │ │ │ │ ├── AchievementsDAO.kt │ │ │ │ │ ├── AnimeDAO.kt │ │ │ │ │ ├── ChaptersDAO.kt │ │ │ │ │ ├── DownloadsDAO.kt │ │ │ │ │ ├── EaDAO.kt │ │ │ │ │ ├── ExplorerDAO.kt │ │ │ │ │ ├── FavsDAO.kt │ │ │ │ │ ├── GenresDAO.kt │ │ │ │ │ ├── NotificationDAO.kt │ │ │ │ │ ├── PlayerStateDAO.kt │ │ │ │ │ ├── QueueDAO.kt │ │ │ │ │ ├── RecentModelsDAO.kt │ │ │ │ │ ├── RecentsDAO.kt │ │ │ │ │ ├── RecordsDAO.kt │ │ │ │ │ ├── SeeingDAO.kt │ │ │ │ │ └── SeenDAO.kt │ │ │ │ ├── directory/ │ │ │ │ │ ├── DirManager.kt │ │ │ │ │ ├── DirObject.kt │ │ │ │ │ ├── DirObjectCompact.kt │ │ │ │ │ ├── DirPagerAdapter.kt │ │ │ │ │ ├── DirPagerAdapterMaterial.kt │ │ │ │ │ ├── DirPagerAdapterOnline.kt │ │ │ │ │ ├── DirectoryDataSource.kt │ │ │ │ │ ├── DirectoryFragment.kt │ │ │ │ │ ├── DirectoryFragmentMaterial.kt │ │ │ │ │ ├── DirectoryPageAdapter.kt │ │ │ │ │ ├── DirectoryPageAdapterMaterial.kt │ │ │ │ │ ├── DirectoryPageAdapterOnline.kt │ │ │ │ │ ├── DirectoryPageCompact.kt │ │ │ │ │ ├── DirectoryPageFragment.kt │ │ │ │ │ ├── DirectoryPageFragmentMaterial.kt │ │ │ │ │ ├── DirectoryPageFragmentOnline.kt │ │ │ │ │ ├── DirectoryService.kt │ │ │ │ │ ├── DirectoryUpdateService.kt │ │ │ │ │ ├── DirectoryViewModel.kt │ │ │ │ │ └── viewholders/ │ │ │ │ │ ├── DirMainFragmentHolder.kt │ │ │ │ │ └── DirMainFragmentMaterialHolder.kt │ │ │ │ ├── download/ │ │ │ │ │ ├── DownloadDialogActivity.kt │ │ │ │ │ ├── DownloadManager.kt │ │ │ │ │ ├── DownloadManagerCentral.kt │ │ │ │ │ ├── DownloadManagerJob.kt │ │ │ │ │ ├── DownloadReceiver.kt │ │ │ │ │ ├── DownloadService.kt │ │ │ │ │ ├── FileAccessHelper.kt │ │ │ │ │ ├── MultipleDownloadManager.kt │ │ │ │ │ ├── UriValidation.kt │ │ │ │ │ └── downloadKt.kt │ │ │ │ ├── emision/ │ │ │ │ │ ├── AnimeSubObject.kt │ │ │ │ │ ├── EmissionActivity.kt │ │ │ │ │ ├── EmissionActivityMaterial.kt │ │ │ │ │ ├── EmissionAdapter.kt │ │ │ │ │ ├── EmissionAdapterMaterial.kt │ │ │ │ │ ├── EmissionFragment.kt │ │ │ │ │ ├── EmissionFragmentMaterial.kt │ │ │ │ │ ├── EmissionPagerAdapter.kt │ │ │ │ │ ├── EmissionPagerAdapterMaterial.kt │ │ │ │ │ └── RemoveListener.kt │ │ │ │ ├── explorer/ │ │ │ │ │ ├── DownloadingAdapter.kt │ │ │ │ │ ├── DownloadingAdapterMaterial.kt │ │ │ │ │ ├── ExplorerActivity.kt │ │ │ │ │ ├── ExplorerActivityMaterial.kt │ │ │ │ │ ├── ExplorerChapsAdapter.kt │ │ │ │ │ ├── ExplorerChapsAdapterMaterial.kt │ │ │ │ │ ├── ExplorerCreator.kt │ │ │ │ │ ├── ExplorerFilesAdapter.kt │ │ │ │ │ ├── ExplorerFilesAdapterMaterial.kt │ │ │ │ │ ├── ExplorerFilesModel.kt │ │ │ │ │ ├── ExplorerObjectDiff.kt │ │ │ │ │ ├── ExplorerObjectWrap.kt │ │ │ │ │ ├── ExplorerPagerAdapter.kt │ │ │ │ │ ├── ExplorerPagerAdapterMaterial.kt │ │ │ │ │ ├── FragmentBase.kt │ │ │ │ │ ├── FragmentChapters.kt │ │ │ │ │ ├── FragmentChaptersMaterial.kt │ │ │ │ │ ├── FragmentDownloads.kt │ │ │ │ │ ├── FragmentDownloadsMaterial.kt │ │ │ │ │ ├── FragmentFiles.kt │ │ │ │ │ ├── FragmentFilesMaterial.kt │ │ │ │ │ ├── FragmentFilesRoot.kt │ │ │ │ │ ├── FragmentFilesRootMaterial.kt │ │ │ │ │ ├── FragmentPermission.kt │ │ │ │ │ ├── OnFileStateChange.kt │ │ │ │ │ ├── ThumbServer.kt │ │ │ │ │ └── creator/ │ │ │ │ │ ├── Creator.kt │ │ │ │ │ ├── DocumentFileCreator.kt │ │ │ │ │ ├── SimpleFileCreator.kt │ │ │ │ │ └── SubFile.kt │ │ │ │ ├── faq/ │ │ │ │ │ ├── FaqActivity.kt │ │ │ │ │ ├── FaqActivityMaterial.kt │ │ │ │ │ ├── FaqAdapter.kt │ │ │ │ │ └── FaqItem.kt │ │ │ │ ├── favorite/ │ │ │ │ │ ├── FavSectionHelper.kt │ │ │ │ │ ├── FavoriteFragment.kt │ │ │ │ │ ├── FavoriteFragmentMaterial.kt │ │ │ │ │ ├── FavoriteViewModel.kt │ │ │ │ │ ├── FavsSectionAdapter.kt │ │ │ │ │ ├── FavsSectionAdapterMaterial.kt │ │ │ │ │ └── objects/ │ │ │ │ │ ├── FavSorter.kt │ │ │ │ │ └── InfoContainer.kt │ │ │ │ ├── home/ │ │ │ │ │ ├── DirAdapter.kt │ │ │ │ │ ├── DirAdapterMaterial.kt │ │ │ │ │ ├── HomeFragment.kt │ │ │ │ │ ├── HomeFragmentMaterial.kt │ │ │ │ │ ├── QueueAdapter.kt │ │ │ │ │ ├── QueueAdapterMaterial.kt │ │ │ │ │ ├── RecentsAdapter.kt │ │ │ │ │ ├── RecentsAdapterMaterial.kt │ │ │ │ │ ├── RecommendedAdapter.kt │ │ │ │ │ ├── RecommendedAdapterMaterial.kt │ │ │ │ │ ├── SearchAdapter.kt │ │ │ │ │ ├── SearchAdapterMaterial.kt │ │ │ │ │ ├── StaffRecommendations.kt │ │ │ │ │ ├── UpdateableAdapter.kt │ │ │ │ │ ├── WaitingAdapter.kt │ │ │ │ │ └── WaitingAdapterMaterial.kt │ │ │ │ ├── jobscheduler/ │ │ │ │ │ ├── BackUpWork.kt │ │ │ │ │ ├── DirUpdateWork.kt │ │ │ │ │ ├── RecentsWork.kt │ │ │ │ │ ├── UpdateWork.kt │ │ │ │ │ └── WorkExt.kt │ │ │ │ ├── news/ │ │ │ │ │ ├── MaterialNewsActivity.kt │ │ │ │ │ ├── MaterialNewsAdapter.kt │ │ │ │ │ ├── NewsActivity.kt │ │ │ │ │ ├── NewsAdapter.kt │ │ │ │ │ ├── NewsCreator.kt │ │ │ │ │ ├── NewsDialog.kt │ │ │ │ │ ├── NewsObjects.kt │ │ │ │ │ └── NewsViewModel.kt │ │ │ │ ├── player/ │ │ │ │ │ ├── AudioFocusWrapper.kt │ │ │ │ │ ├── BVListener.kt │ │ │ │ │ ├── CustomExoPlayer.kt │ │ │ │ │ ├── MediaCatalog.kt │ │ │ │ │ ├── Player.kt │ │ │ │ │ ├── VideoActivity.kt │ │ │ │ │ └── WebPlayerActivity.kt │ │ │ │ ├── pojos/ │ │ │ │ │ ├── Achievement.kt │ │ │ │ │ ├── AchievementAd.kt │ │ │ │ │ ├── Anime.java │ │ │ │ │ ├── AnimeObject.java │ │ │ │ │ ├── AutoBackupObject.kt │ │ │ │ │ ├── DirectoryPage.kt │ │ │ │ │ ├── DownloadObject.java │ │ │ │ │ ├── EAObject.kt │ │ │ │ │ ├── ExplorerObject.java │ │ │ │ │ ├── FakeAutoBackup.java │ │ │ │ │ ├── FavSection.java │ │ │ │ │ ├── FavoriteObject.java │ │ │ │ │ ├── GenreStatusObject.java │ │ │ │ │ ├── NotificationObj.java │ │ │ │ │ ├── QueueObject.java │ │ │ │ │ ├── RecentObject.java │ │ │ │ │ ├── RecentWrap.kt │ │ │ │ │ ├── Recents.java │ │ │ │ │ ├── RecordObject.java │ │ │ │ │ ├── SeeingObject.java │ │ │ │ │ └── SeenObject.kt │ │ │ │ ├── preferences/ │ │ │ │ │ ├── AdsPreferenceActivity.kt │ │ │ │ │ ├── BottomPreferencesFragment.kt │ │ │ │ │ ├── BottomPreferencesMaterialFragment.kt │ │ │ │ │ ├── ConfigurationFragment.kt │ │ │ │ │ └── ConfigurationFragmentMaterial.kt │ │ │ │ ├── profile/ │ │ │ │ │ ├── TopActivity.kt │ │ │ │ │ ├── TopActivityMaterial.kt │ │ │ │ │ ├── TopAdapter.kt │ │ │ │ │ └── TopItem.kt │ │ │ │ ├── queue/ │ │ │ │ │ ├── ItemTouchHelperAdapter.kt │ │ │ │ │ ├── NoTouchHelperCallback.kt │ │ │ │ │ ├── QueueActivity.kt │ │ │ │ │ ├── QueueActivityMaterial.kt │ │ │ │ │ ├── QueueAllAdapter.kt │ │ │ │ │ ├── QueueAllAdapterMaterial.kt │ │ │ │ │ ├── QueueAnimesAdapter.kt │ │ │ │ │ ├── QueueAnimesAdapterMaterial.kt │ │ │ │ │ ├── QueueListAdapter.kt │ │ │ │ │ ├── QueueManager.kt │ │ │ │ │ └── SimpleItemTouchHelperCallback.kt │ │ │ │ ├── random/ │ │ │ │ │ ├── RandomActivity.kt │ │ │ │ │ ├── RandomActivityMaterial.kt │ │ │ │ │ ├── RandomAdapter.kt │ │ │ │ │ ├── RandomAdapterMaterial.kt │ │ │ │ │ └── RandomObject.kt │ │ │ │ ├── recents/ │ │ │ │ │ ├── RecentFragment.kt │ │ │ │ │ ├── RecentModel.kt │ │ │ │ │ ├── RecentModelAd.kt │ │ │ │ │ ├── RecentModelCh.kt │ │ │ │ │ ├── RecentModelsAdapter.kt │ │ │ │ │ ├── RecentModelsFragment.kt │ │ │ │ │ ├── RecentModelsViewModel.kt │ │ │ │ │ ├── RecentsActivity.kt │ │ │ │ │ ├── RecentsAdapter.kt │ │ │ │ │ ├── RecentsModelActivity.kt │ │ │ │ │ ├── RecentsNotReceiver.kt │ │ │ │ │ ├── RecentsViewModel.kt │ │ │ │ │ └── viewholders/ │ │ │ │ │ └── RecyclerRefreshHolder.kt │ │ │ │ ├── recommended/ │ │ │ │ │ ├── AnimeShortObject.kt │ │ │ │ │ ├── BlacklistDialog.kt │ │ │ │ │ ├── RHHolder.kt │ │ │ │ │ ├── RIHolder.kt │ │ │ │ │ ├── RankType.kt │ │ │ │ │ ├── RankingActivity.kt │ │ │ │ │ ├── RankingActivityMaterial.kt │ │ │ │ │ ├── RankingAdapter.kt │ │ │ │ │ ├── RankingAdapterMaterial.kt │ │ │ │ │ ├── RecommendActivity.kt │ │ │ │ │ ├── RecommendActivityMaterial.kt │ │ │ │ │ ├── RecommendHelper.kt │ │ │ │ │ └── sections/ │ │ │ │ │ ├── MultipleSection.kt │ │ │ │ │ └── MultipleSectionMaterial.kt │ │ │ │ ├── record/ │ │ │ │ │ ├── RecordActivity.kt │ │ │ │ │ ├── RecordActivityMaterial.kt │ │ │ │ │ ├── RecordsAdapter.kt │ │ │ │ │ └── RecordsAdapterMaterial.kt │ │ │ │ ├── retrofit/ │ │ │ │ │ └── Repository.kt │ │ │ │ ├── search/ │ │ │ │ │ ├── FiltersSuggestion.kt │ │ │ │ │ ├── GenreActivity.kt │ │ │ │ │ ├── GenreActivityMaterial.kt │ │ │ │ │ ├── GenreAdapter.kt │ │ │ │ │ ├── GenreAdapterMaterial.kt │ │ │ │ │ ├── GenresDialog.kt │ │ │ │ │ ├── SearchActivity.kt │ │ │ │ │ ├── SearchAdapter.kt │ │ │ │ │ ├── SearchAdapterCompact.kt │ │ │ │ │ ├── SearchAdapterCompactMaterial.kt │ │ │ │ │ ├── SearchAdapterMaterial.kt │ │ │ │ │ ├── SearchAdvObject.kt │ │ │ │ │ ├── SearchCompactDataSource.kt │ │ │ │ │ ├── SearchFragment.kt │ │ │ │ │ ├── SearchFragmentMaterial.kt │ │ │ │ │ ├── SearchObject.kt │ │ │ │ │ ├── SearchObjectFav.kt │ │ │ │ │ └── SearchViewModel.kt │ │ │ │ ├── seeing/ │ │ │ │ │ ├── FavToSeeing.kt │ │ │ │ │ ├── SeeingActivity.kt │ │ │ │ │ ├── SeeingActivityMaterial.kt │ │ │ │ │ ├── SeeingAdapter.kt │ │ │ │ │ ├── SeeingAdapterMaterial.kt │ │ │ │ │ ├── SeeingFragment.kt │ │ │ │ │ ├── SeeingFragmentMaterial.kt │ │ │ │ │ ├── SeeingPagerAdapter.kt │ │ │ │ │ └── SeeingPagerAdapterMaterial.kt │ │ │ │ ├── shortcuts/ │ │ │ │ │ ├── DummyActivity.kt │ │ │ │ │ ├── DummyEmissionActivity.kt │ │ │ │ │ ├── DummyExplorerActivity.kt │ │ │ │ │ └── DummyMainActivity.kt │ │ │ │ ├── slices/ │ │ │ │ │ └── AnimeSliceObject.kt │ │ │ │ ├── tv/ │ │ │ │ │ ├── AnimeRow.kt │ │ │ │ │ ├── BindableCardView.kt │ │ │ │ │ ├── ChannelUtils.kt │ │ │ │ │ ├── GlideBackgroundManager.kt │ │ │ │ │ ├── TVBaseActivity.kt │ │ │ │ │ ├── TVServersFactory.kt │ │ │ │ │ ├── anime/ │ │ │ │ │ │ ├── AnimePresenter.kt │ │ │ │ │ │ ├── ChapterPresenter.kt │ │ │ │ │ │ ├── EmissionPresenter.kt │ │ │ │ │ │ ├── FavPresenter.kt │ │ │ │ │ │ ├── RecentsPresenter.kt │ │ │ │ │ │ ├── RecordPresenter.kt │ │ │ │ │ │ ├── RelatedPresenter.kt │ │ │ │ │ │ ├── SectionPresenter.kt │ │ │ │ │ │ └── SyncPresenter.kt │ │ │ │ │ ├── cards/ │ │ │ │ │ │ ├── AnimeCardView.kt │ │ │ │ │ │ ├── ChapterCardView.kt │ │ │ │ │ │ ├── DirAdvCardView.kt │ │ │ │ │ │ ├── DirCardView.kt │ │ │ │ │ │ ├── EmissionCardView.kt │ │ │ │ │ │ ├── FavCardView.kt │ │ │ │ │ │ ├── RecentsCardView.kt │ │ │ │ │ │ ├── RecordCardView.kt │ │ │ │ │ │ ├── RelatedCardView.kt │ │ │ │ │ │ ├── SectionCardView.kt │ │ │ │ │ │ ├── SyncCardView.kt │ │ │ │ │ │ └── TagCardView.kt │ │ │ │ │ ├── details/ │ │ │ │ │ │ ├── ChaptersListPresenter.kt │ │ │ │ │ │ ├── ChaptersListRow.kt │ │ │ │ │ │ ├── CustomFullWidthDetailsOverviewRowPresenter.kt │ │ │ │ │ │ ├── DetailsDescriptionPresenter.kt │ │ │ │ │ │ ├── TVAnimesDetails.kt │ │ │ │ │ │ └── TVAnimesDetailsFragment.kt │ │ │ │ │ ├── directory/ │ │ │ │ │ │ ├── DirAdvPresenter.kt │ │ │ │ │ │ ├── DirPresenter.kt │ │ │ │ │ │ ├── TVDir.kt │ │ │ │ │ │ └── TVDirFragment.kt │ │ │ │ │ ├── emission/ │ │ │ │ │ │ ├── TVEmission.kt │ │ │ │ │ │ └── TVEmissionFragment.kt │ │ │ │ │ ├── exoplayer/ │ │ │ │ │ │ ├── LeanbackPlayerAdapter.kt │ │ │ │ │ │ ├── PlaybackFragment.kt │ │ │ │ │ │ ├── TVPlayer.kt │ │ │ │ │ │ ├── Video.kt │ │ │ │ │ │ └── VideoPlayerGlue.kt │ │ │ │ │ ├── search/ │ │ │ │ │ │ ├── BasicAnimeObject.kt │ │ │ │ │ │ ├── TVSearch.kt │ │ │ │ │ │ ├── TVSearchFragment.kt │ │ │ │ │ │ ├── TVTag.kt │ │ │ │ │ │ ├── TVTagFragment.kt │ │ │ │ │ │ └── TagPresenter.kt │ │ │ │ │ ├── sections/ │ │ │ │ │ │ ├── DirSection.kt │ │ │ │ │ │ ├── EmissionSection.kt │ │ │ │ │ │ └── SectionObject.kt │ │ │ │ │ ├── streaming/ │ │ │ │ │ │ ├── StreamTvActivity.kt │ │ │ │ │ │ ├── TVMultiSelection.kt │ │ │ │ │ │ ├── TVMultiSelectionFragment.kt │ │ │ │ │ │ ├── TVServerSelection.kt │ │ │ │ │ │ └── TVServerSelectionFragment.kt │ │ │ │ │ ├── sync/ │ │ │ │ │ │ ├── BypassObject.kt │ │ │ │ │ │ ├── LogOutObject.kt │ │ │ │ │ │ └── SyncObject.kt │ │ │ │ │ └── ui/ │ │ │ │ │ ├── TVMain.kt │ │ │ │ │ └── TVMainFragment.kt │ │ │ │ ├── updater/ │ │ │ │ │ ├── UpdateActivity.kt │ │ │ │ │ ├── UpdateChecker.kt │ │ │ │ │ └── UpdaterViewModel.kt │ │ │ │ ├── videoservers/ │ │ │ │ │ ├── FembedServer.kt │ │ │ │ │ ├── FenixServer.kt │ │ │ │ │ ├── FileActions.kt │ │ │ │ │ ├── FireServer.kt │ │ │ │ │ ├── GoCDNServer.kt │ │ │ │ │ ├── Headers.kt │ │ │ │ │ ├── HyperionServer.kt │ │ │ │ │ ├── IzanagiServer.kt │ │ │ │ │ ├── KDecoder.kt │ │ │ │ │ ├── MP4UploadServer.kt │ │ │ │ │ ├── MangoServer.kt │ │ │ │ │ ├── MegaServer.kt │ │ │ │ │ ├── NatsukiServer.kt │ │ │ │ │ ├── OkruServer.kt │ │ │ │ │ ├── Option.kt │ │ │ │ │ ├── RVServer.kt │ │ │ │ │ ├── SBServer.kt │ │ │ │ │ ├── Server.kt │ │ │ │ │ ├── ServersFactory.kt │ │ │ │ │ ├── StapeServer.kt │ │ │ │ │ ├── StreamWishServer.kt │ │ │ │ │ ├── Unpacker.kt │ │ │ │ │ ├── VeryStreamServer.kt │ │ │ │ │ ├── VideoServer.kt │ │ │ │ │ ├── WebJS.kt │ │ │ │ │ ├── WebServer.kt │ │ │ │ │ ├── YUServer.kt │ │ │ │ │ └── ZippyServer.kt │ │ │ │ └── widgets/ │ │ │ │ ├── AdTemplateView.java │ │ │ │ ├── NativeTemplateStyle.java │ │ │ │ ├── emision/ │ │ │ │ │ ├── WEListItem.kt │ │ │ │ │ ├── WEListProvider.kt │ │ │ │ │ ├── WEmisionProvider.kt │ │ │ │ │ └── WEmissionService.kt │ │ │ │ └── test.kt │ │ │ └── res/ │ │ │ ├── anim/ │ │ │ │ ├── anim_fall_down.xml │ │ │ │ ├── anim_fall_up.xml │ │ │ │ ├── fade_in.xml │ │ │ │ ├── fade_out.xml │ │ │ │ ├── fadein.xml │ │ │ │ ├── fadeout.xml │ │ │ │ ├── grid_fall_down.xml │ │ │ │ ├── layout_fall_down.xml │ │ │ │ ├── scale_down.xml │ │ │ │ └── scale_up.xml │ │ │ ├── color/ │ │ │ │ ├── accent_button_state.xml │ │ │ │ ├── firebase_selector.xml │ │ │ │ ├── raised_button_text_state.xml │ │ │ │ ├── sync_button_color.xml │ │ │ │ └── text_secondary.xml │ │ │ ├── drawable/ │ │ │ │ ├── action_chapter.xml │ │ │ │ ├── action_expand.xml │ │ │ │ ├── action_search.xml │ │ │ │ ├── action_shrink.xml │ │ │ │ ├── anim_check_sync.xml │ │ │ │ ├── anim_sync_check.xml │ │ │ │ ├── background_ripple.xml │ │ │ │ ├── bottom_directory.xml │ │ │ │ ├── bottom_recents.xml │ │ │ │ ├── bottom_settings.xml │ │ │ │ ├── bottom_state.xml │ │ │ │ ├── chip_change.xml │ │ │ │ ├── chip_error.xml │ │ │ │ ├── chip_new.xml │ │ │ │ ├── chip_ripple.xml │ │ │ │ ├── chip_shape.xml │ │ │ │ ├── circular_shade.xml │ │ │ │ ├── drawable_splash.xml │ │ │ │ ├── faq_indicator.xml │ │ │ │ ├── grad_1.xml │ │ │ │ ├── grad_2.xml │ │ │ │ ├── grad_3.xml │ │ │ │ ├── grad_4.xml │ │ │ │ ├── heart_broken.xml │ │ │ │ ├── heart_empty.xml │ │ │ │ ├── heart_full.xml │ │ │ │ ├── ic_account_edit.xml │ │ │ │ ├── ic_achievement_airplane.xml │ │ │ │ ├── ic_achievement_battery.xml │ │ │ │ ├── ic_achievement_bored.xml │ │ │ │ ├── ic_achievement_calendar.xml │ │ │ │ ├── ic_achievement_cast.xml │ │ │ │ ├── ic_achievement_clock.xml │ │ │ │ ├── ic_achievement_cloud.xml │ │ │ │ ├── ic_achievement_completed.xml │ │ │ │ ├── ic_achievement_droped.xml │ │ │ │ ├── ic_achievement_egg.xml │ │ │ │ ├── ic_achievement_evangelion.xml │ │ │ │ ├── ic_achievement_fav.xml │ │ │ │ ├── ic_achievement_following.xml │ │ │ │ ├── ic_achievement_memory.xml │ │ │ │ ├── ic_achievement_midoriya.xml │ │ │ │ ├── ic_achievement_news.xml │ │ │ │ ├── ic_achievement_onepiece.xml │ │ │ │ ├── ic_achievement_otaku.xml │ │ │ │ ├── ic_achievement_pig.xml │ │ │ │ ├── ic_achievement_question.xml │ │ │ │ ├── ic_achievement_sad.xml │ │ │ │ ├── ic_achievement_share.xml │ │ │ │ ├── ic_achievement_start.xml │ │ │ │ ├── ic_achievement_vampire.xml │ │ │ │ ├── ic_achievements.xml │ │ │ │ ├── ic_action_close.xml │ │ │ │ ├── ic_add_category.xml │ │ │ │ ├── ic_add_list.xml │ │ │ │ ├── ic_animations.xml │ │ │ │ ├── ic_arrow_left.xml │ │ │ │ ├── ic_author.xml │ │ │ │ ├── ic_backup.xml │ │ │ │ ├── ic_beta.xml │ │ │ │ ├── ic_blacklist.xml │ │ │ │ ├── ic_buffer.xml │ │ │ │ ├── ic_bug.xml │ │ │ │ ├── ic_cash.xml │ │ │ │ ├── ic_cash_multi.xml │ │ │ │ ├── ic_casting.xml │ │ │ │ ├── ic_casting_menu.xml │ │ │ │ ├── ic_changelog_get.xml │ │ │ │ ├── ic_chap_down.xml │ │ │ │ ├── ic_check.xml │ │ │ │ ├── ic_check_bold.xml │ │ │ │ ├── ic_check_tv.xml │ │ │ │ ├── ic_clear.xml │ │ │ │ ├── ic_clear_all.xml │ │ │ │ ├── ic_clear_white.xml │ │ │ │ ├── ic_clock.xml │ │ │ │ ├── ic_close.xml │ │ │ │ ├── ic_cloud.xml │ │ │ │ ├── ic_cloud_download.xml │ │ │ │ ├── ic_cloud_firestore.xml │ │ │ │ ├── ic_cloud_upload.xml │ │ │ │ ├── ic_coin.xml │ │ │ │ ├── ic_coin_ach.xml │ │ │ │ ├── ic_collapse.xml │ │ │ │ ├── ic_comments.xml │ │ │ │ ├── ic_completed.xml │ │ │ │ ├── ic_considering.xml │ │ │ │ ├── ic_cuplogo.xml │ │ │ │ ├── ic_danger.xml │ │ │ │ ├── ic_delete.xml │ │ │ │ ├── ic_delete_all.xml │ │ │ │ ├── ic_design_ab.xml │ │ │ │ ├── ic_diagnostic.xml │ │ │ │ ├── ic_dir_update.xml │ │ │ │ ├── ic_directory.xml │ │ │ │ ├── ic_directory_not.xml │ │ │ │ ├── ic_discord.xml │ │ │ │ ├── ic_download.xml │ │ │ │ ├── ic_download_multiple.xml │ │ │ │ ├── ic_download_progress.xml │ │ │ │ ├── ic_downloader.xml │ │ │ │ ├── ic_drag.xml │ │ │ │ ├── ic_drive.xml │ │ │ │ ├── ic_dropbox.xml │ │ │ │ ├── ic_droped.xml │ │ │ │ ├── ic_edit.xml │ │ │ │ ├── ic_egg.xml │ │ │ │ ├── ic_emision.xml │ │ │ │ ├── ic_emision_not.xml │ │ │ │ ├── ic_error.xml │ │ │ │ ├── ic_experimental.xml │ │ │ │ ├── ic_facebook.xml │ │ │ │ ├── ic_facebook_group.xml │ │ │ │ ├── ic_faq.xml │ │ │ │ ├── ic_ff.xml │ │ │ │ ├── ic_ffwd.xml │ │ │ │ ├── ic_files.xml │ │ │ │ ├── ic_filter.xml │ │ │ │ ├── ic_genres_0.xml │ │ │ │ ├── ic_genres_1.xml │ │ │ │ ├── ic_genres_2.xml │ │ │ │ ├── ic_genres_3.xml │ │ │ │ ├── ic_genres_4.xml │ │ │ │ ├── ic_genres_5.xml │ │ │ │ ├── ic_genres_6.xml │ │ │ │ ├── ic_genres_7.xml │ │ │ │ ├── ic_genres_8.xml │ │ │ │ ├── ic_genres_9.xml │ │ │ │ ├── ic_genres_more.xml │ │ │ │ ├── ic_github.xml │ │ │ │ ├── ic_group.xml │ │ │ │ ├── ic_grouped.xml │ │ │ │ ├── ic_hash.xml │ │ │ │ ├── ic_heart.xml │ │ │ │ ├── ic_heart_full_menu.xml │ │ │ │ ├── ic_hide.xml │ │ │ │ ├── ic_hide_pref.xml │ │ │ │ ├── ic_home.xml │ │ │ │ ├── ic_img.xml │ │ │ │ ├── ic_import.xml │ │ │ │ ├── ic_incognito.xml │ │ │ │ ├── ic_info.xml │ │ │ │ ├── ic_info_white.xml │ │ │ │ ├── ic_key.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── ic_lay_type.xml │ │ │ │ ├── ic_library_video.xml │ │ │ │ ├── ic_list_placeholder.xml │ │ │ │ ├── ic_locked.xml │ │ │ │ ├── ic_locked_solid.xml │ │ │ │ ├── ic_magic.xml │ │ │ │ ├── ic_map.xml │ │ │ │ ├── ic_menu.xml │ │ │ │ ├── ic_menu_ham.xml │ │ │ │ ├── ic_move.xml │ │ │ │ ├── ic_new.xml │ │ │ │ ├── ic_new_recent.xml │ │ │ │ ├── ic_news.xml │ │ │ │ ├── ic_next.xml │ │ │ │ ├── ic_no_downloading.xml │ │ │ │ ├── ic_no_downloads.xml │ │ │ │ ├── ic_no_emision.xml │ │ │ │ ├── ic_no_genres.xml │ │ │ │ ├── ic_no_network.xml │ │ │ │ ├── ic_no_recents.xml │ │ │ │ ├── ic_no_records.xml │ │ │ │ ├── ic_no_thumb.xml │ │ │ │ ├── ic_not_found.xml │ │ │ │ ├── ic_not_seeing.xml │ │ │ │ ├── ic_not_update.xml │ │ │ │ ├── ic_note.xml │ │ │ │ ├── ic_number.xml │ │ │ │ ├── ic_open.xml │ │ │ │ ├── ic_option_not_seen.xml │ │ │ │ ├── ic_option_seen.xml │ │ │ │ ├── ic_palette.xml │ │ │ │ ├── ic_patreon.xml │ │ │ │ ├── ic_pause.xml │ │ │ │ ├── ic_pause_normal.xml │ │ │ │ ├── ic_pause_not.xml │ │ │ │ ├── ic_paused.xml │ │ │ │ ├── ic_paypal.xml │ │ │ │ ├── ic_pip.xml │ │ │ │ ├── ic_pip_exit.xml │ │ │ │ ├── ic_play.xml │ │ │ │ ├── ic_play_all.xml │ │ │ │ ├── ic_play_not.xml │ │ │ │ ├── ic_play_queue.xml │ │ │ │ ├── ic_player.xml │ │ │ │ ├── ic_podium.xml │ │ │ │ ├── ic_previous.xml │ │ │ │ ├── ic_queue_file.xml │ │ │ │ ├── ic_queue_list.xml │ │ │ │ ├── ic_queue_normal.xml │ │ │ │ ├── ic_random.xml │ │ │ │ ├── ic_rating.xml │ │ │ │ ├── ic_recents.xml │ │ │ │ ├── ic_recents_group.xml │ │ │ │ ├── ic_record.xml │ │ │ │ ├── ic_remember.xml │ │ │ │ ├── ic_resize.xml │ │ │ │ ├── ic_restore.xml │ │ │ │ ├── ic_rewind.xml │ │ │ │ ├── ic_samsung_tv.xml │ │ │ │ ├── ic_save.xml │ │ │ │ ├── ic_save_w.xml │ │ │ │ ├── ic_scale.xml │ │ │ │ ├── ic_search_black_24dp.xml │ │ │ │ ├── ic_sectioned_favs.xml │ │ │ │ ├── ic_seeing.xml │ │ │ │ ├── ic_seeing_drawer.xml │ │ │ │ ├── ic_seen.xml │ │ │ │ ├── ic_server_running.xml │ │ │ │ ├── ic_service.xml │ │ │ │ ├── ic_setting_asc_list.xml │ │ │ │ ├── ic_setting_gps.xml │ │ │ │ ├── ic_settings_reload.xml │ │ │ │ ├── ic_share.xml │ │ │ │ ├── ic_show.xml │ │ │ │ ├── ic_skip.xml │ │ │ │ ├── ic_star_heart.xml │ │ │ │ ├── ic_stop.xml │ │ │ │ ├── ic_suggestions.xml │ │ │ │ ├── ic_sync.xml │ │ │ │ ├── ic_sync_menu.xml │ │ │ │ ├── ic_sync_rotate.xml │ │ │ │ ├── ic_tag.xml │ │ │ │ ├── ic_terminal.xml │ │ │ │ ├── ic_theme.xml │ │ │ │ ├── ic_timeout.xml │ │ │ │ ├── ic_treasure.xml │ │ │ │ ├── ic_trophy.xml │ │ │ │ ├── ic_trophy_bronze.xml │ │ │ │ ├── ic_trophy_gold.xml │ │ │ │ ├── ic_trophy_normal.xml │ │ │ │ ├── ic_trophy_silver.xml │ │ │ │ ├── ic_umaru.xml │ │ │ │ ├── ic_umaru_simple.xml │ │ │ │ ├── ic_unlock.xml │ │ │ │ ├── ic_unlocked.xml │ │ │ │ ├── ic_version.xml │ │ │ │ ├── ic_warning.xml │ │ │ │ ├── ic_watching.xml │ │ │ │ ├── ic_web.xml │ │ │ │ ├── material_tab_indicator.xml │ │ │ │ ├── progressbar_circle.xml │ │ │ │ ├── shadow_gradient.xml │ │ │ │ ├── shape_circle.xml │ │ │ │ ├── side_nav_bar.xml │ │ │ │ ├── side_nav_bar_amber.xml │ │ │ │ ├── side_nav_bar_blue.xml │ │ │ │ ├── side_nav_bar_blue_gray.xml │ │ │ │ ├── side_nav_bar_brown.xml │ │ │ │ ├── side_nav_bar_cyan.xml │ │ │ │ ├── side_nav_bar_deep_orange.xml │ │ │ │ ├── side_nav_bar_deep_purple.xml │ │ │ │ ├── side_nav_bar_gray.xml │ │ │ │ ├── side_nav_bar_green.xml │ │ │ │ ├── side_nav_bar_indigo.xml │ │ │ │ ├── side_nav_bar_light_blue.xml │ │ │ │ ├── side_nav_bar_light_green.xml │ │ │ │ ├── side_nav_bar_lime.xml │ │ │ │ ├── side_nav_bar_orange.xml │ │ │ │ ├── side_nav_bar_pink.xml │ │ │ │ ├── side_nav_bar_purple.xml │ │ │ │ ├── side_nav_bar_teal.xml │ │ │ │ ├── side_nav_bar_yellow.xml │ │ │ │ └── updater_background.xml │ │ │ ├── drawable-night/ │ │ │ │ ├── anim_check_sync.xml │ │ │ │ ├── anim_sync_check.xml │ │ │ │ └── shadow_gradient.xml │ │ │ ├── layout/ │ │ │ │ ├── activity_achievement_profile.xml │ │ │ │ ├── activity_achievement_profile_material.xml │ │ │ │ ├── activity_ads_settings.xml │ │ │ │ ├── activity_anime_info.xml │ │ │ │ ├── activity_anime_info_material.xml │ │ │ │ ├── activity_anime_info_nwv.xml │ │ │ │ ├── activity_blank.xml │ │ │ │ ├── activity_browser.xml │ │ │ │ ├── activity_ea.xml │ │ │ │ ├── activity_eamap.xml │ │ │ │ ├── activity_emision.xml │ │ │ │ ├── activity_emision_material.xml │ │ │ │ ├── activity_explorer.xml │ │ │ │ ├── activity_explorer_material.xml │ │ │ │ ├── activity_fragment.xml │ │ │ │ ├── activity_fragment_material.xml │ │ │ │ ├── activity_login.xml │ │ │ │ ├── activity_login_buttons.xml │ │ │ │ ├── activity_login_firestore.xml │ │ │ │ ├── activity_login_main.xml │ │ │ │ ├── activity_main_drawer.xml │ │ │ │ ├── activity_main_drawer_nwv.xml │ │ │ │ ├── activity_main_material.xml │ │ │ │ ├── activity_migrate.xml │ │ │ │ ├── activity_news.xml │ │ │ │ ├── activity_news_material.xml │ │ │ │ ├── activity_queue.xml │ │ │ │ ├── activity_queue_grid.xml │ │ │ │ ├── activity_queue_grid_material.xml │ │ │ │ ├── activity_queue_material.xml │ │ │ │ ├── activity_search.xml │ │ │ │ ├── activity_seening.xml │ │ │ │ ├── activity_seening_material.xml │ │ │ │ ├── activity_updater.xml │ │ │ │ ├── activity_webview.xml │ │ │ │ ├── activity_webview_nwv.xml │ │ │ │ ├── admob_ad_alone.xml │ │ │ │ ├── admob_ad_card.xml │ │ │ │ ├── admob_ad_news.xml │ │ │ │ ├── admob_ad_plain.xml │ │ │ │ ├── app_bar_main.xml │ │ │ │ ├── app_bar_main_nwv.xml │ │ │ │ ├── dialog_ff_enable.xml │ │ │ │ ├── dialog_random_picker.xml │ │ │ │ ├── dialog_wallet.xml │ │ │ │ ├── exo_playback_control_view.xml │ │ │ │ ├── exo_playback_youtube_control_view.xml │ │ │ │ ├── exo_player.xml │ │ │ │ ├── fragment_achievements.xml │ │ │ │ ├── fragment_anime_details.xml │ │ │ │ ├── fragment_anime_details_material.xml │ │ │ │ ├── fragment_directory.xml │ │ │ │ ├── fragment_directory_material.xml │ │ │ │ ├── fragment_explorer_files.xml │ │ │ │ ├── fragment_explorer_permission_pending.xml │ │ │ │ ├── fragment_home.xml │ │ │ │ ├── fragment_home_material.xml │ │ │ │ ├── fragment_preferences.xml │ │ │ │ ├── fragment_preferences_material.xml │ │ │ │ ├── fragment_recent_material.xml │ │ │ │ ├── fragment_search.xml │ │ │ │ ├── fragment_search_grid.xml │ │ │ │ ├── fragment_seeing.xml │ │ │ │ ├── item_achievements.xml │ │ │ │ ├── item_ad.xml │ │ │ │ ├── item_ad_achievements.xml │ │ │ │ ├── item_ad_fav.xml │ │ │ │ ├── item_ad_news.xml │ │ │ │ ├── item_ad_recents_material.xml │ │ │ │ ├── item_anim_queue.xml │ │ │ │ ├── item_anim_queue_grid.xml │ │ │ │ ├── item_anim_queue_grid_material.xml │ │ │ │ ├── item_anim_queue_material.xml │ │ │ │ ├── item_changelog.xml │ │ │ │ ├── item_changelog_material.xml │ │ │ │ ├── item_chap.xml │ │ │ │ ├── item_chap_grid.xml │ │ │ │ ├── item_chap_grid_material.xml │ │ │ │ ├── item_chap_material.xml │ │ │ │ ├── item_chapter_preview.xml │ │ │ │ ├── item_chapter_preview_material.xml │ │ │ │ ├── item_chip.xml │ │ │ │ ├── item_dir.xml │ │ │ │ ├── item_dir_grid.xml │ │ │ │ ├── item_dir_grid_material.xml │ │ │ │ ├── item_dir_material.xml │ │ │ │ ├── item_downloading_extra.xml │ │ │ │ ├── item_downloading_extra_material.xml │ │ │ │ ├── item_ea_step.xml │ │ │ │ ├── item_emision.xml │ │ │ │ ├── item_emision_material.xml │ │ │ │ ├── item_explorer.xml │ │ │ │ ├── item_explorer_grid.xml │ │ │ │ ├── item_explorer_grid_material.xml │ │ │ │ ├── item_explorer_material.xml │ │ │ │ ├── item_faq.xml │ │ │ │ ├── item_fav.xml │ │ │ │ ├── item_fav_grid.xml │ │ │ │ ├── item_fav_grid_card.xml │ │ │ │ ├── item_fav_grid_card_material.xml │ │ │ │ ├── item_fav_grid_card_simple.xml │ │ │ │ ├── item_fav_grid_card_simple_material.xml │ │ │ │ ├── item_fav_grid_material.xml │ │ │ │ ├── item_fav_header.xml │ │ │ │ ├── item_fav_material.xml │ │ │ │ ├── item_native_reduced.xml │ │ │ │ ├── item_native_small.xml │ │ │ │ ├── item_native_small_rounded.xml │ │ │ │ ├── item_news.xml │ │ │ │ ├── item_news_material.xml │ │ │ │ ├── item_queue.xml │ │ │ │ ├── item_queue_full.xml │ │ │ │ ├── item_queue_full_material.xml │ │ │ │ ├── item_ranking.xml │ │ │ │ ├── item_ranking_material.xml │ │ │ │ ├── item_recents.xml │ │ │ │ ├── item_recents_material.xml │ │ │ │ ├── item_recommend_header.xml │ │ │ │ ├── item_record.xml │ │ │ │ ├── item_record_grid.xml │ │ │ │ ├── item_record_grid_material.xml │ │ │ │ ├── item_record_material.xml │ │ │ │ ├── item_related.xml │ │ │ │ ├── item_release.xml │ │ │ │ ├── item_release_material.xml │ │ │ │ ├── item_simple_spinner.xml │ │ │ │ ├── item_top.xml │ │ │ │ ├── item_top_current.xml │ │ │ │ ├── item_tv_card.xml │ │ │ │ ├── item_tv_card_adv.xml │ │ │ │ ├── item_tv_card_chapter.xml │ │ │ │ ├── item_tv_card_chapter_preview.xml │ │ │ │ ├── item_tv_card_rate.xml │ │ │ │ ├── item_tv_card_section.xml │ │ │ │ ├── item_tv_card_sync.xml │ │ │ │ ├── item_tv_tag.xml │ │ │ │ ├── item_widget_list.xml │ │ │ │ ├── lay_banner_container.xml │ │ │ │ ├── lay_bottom_actions.xml │ │ │ │ ├── lay_comments.xml │ │ │ │ ├── lay_migrate_directory.xml │ │ │ │ ├── lay_migrate_success.xml │ │ │ │ ├── lay_migrate_version.xml │ │ │ │ ├── lay_news.xml │ │ │ │ ├── lay_status_bar.xml │ │ │ │ ├── layout_comments_dialog.xml │ │ │ │ ├── layout_diagnostic.xml │ │ │ │ ├── layout_diagnostic_material.xml │ │ │ │ ├── layout_expandable_textview.xml │ │ │ │ ├── layout_img_big.xml │ │ │ │ ├── layout_img_big_base.xml │ │ │ │ ├── layout_loading_text.xml │ │ │ │ ├── layout_loading_text_material.xml │ │ │ │ ├── nav_header_main.xml │ │ │ │ ├── nav_header_main_material.xml │ │ │ │ ├── overlay.xml │ │ │ │ ├── player_view.xml │ │ │ │ ├── preference_recyclerview.xml │ │ │ │ ├── recycler_changelog.xml │ │ │ │ ├── recycler_changelog_material.xml │ │ │ │ ├── recycler_chapters.xml │ │ │ │ ├── recycler_dir.xml │ │ │ │ ├── recycler_dir_grid.xml │ │ │ │ ├── recycler_downloading.xml │ │ │ │ ├── recycler_emision.xml │ │ │ │ ├── recycler_explorer.xml │ │ │ │ ├── recycler_explorer_chaps.xml │ │ │ │ ├── recycler_explorer_chaps_grid.xml │ │ │ │ ├── recycler_explorer_grid.xml │ │ │ │ ├── recycler_faq.xml │ │ │ │ ├── recycler_faq_material.xml │ │ │ │ ├── recycler_favs.xml │ │ │ │ ├── recycler_favs_grid.xml │ │ │ │ ├── recycler_favs_grid_material.xml │ │ │ │ ├── recycler_favs_matertial.xml │ │ │ │ ├── recycler_genre.xml │ │ │ │ ├── recycler_genre_material.xml │ │ │ │ ├── recycler_loader.xml │ │ │ │ ├── recycler_loader_material.xml │ │ │ │ ├── recycler_ranking.xml │ │ │ │ ├── recycler_ranking_material.xml │ │ │ │ ├── recycler_recommends.xml │ │ │ │ ├── recycler_recommends_grid.xml │ │ │ │ ├── recycler_recommends_grid_material.xml │ │ │ │ ├── recycler_recommends_material.xml │ │ │ │ ├── recycler_records.xml │ │ │ │ ├── recycler_records_grid.xml │ │ │ │ ├── recycler_records_grid_material.xml │ │ │ │ ├── recycler_records_material.xml │ │ │ │ ├── recycler_refresh.xml │ │ │ │ ├── recycler_refresh_fragment.xml │ │ │ │ ├── recycler_refresh_grid.xml │ │ │ │ ├── recycler_refresh_grid_material.xml │ │ │ │ ├── recycler_refresh_material.xml │ │ │ │ ├── snackprogressbar.xml │ │ │ │ ├── sync_item_layout.xml │ │ │ │ ├── tv_activity_main.xml │ │ │ │ ├── view_hidden_overlay.xml │ │ │ │ ├── view_home_list.xml │ │ │ │ ├── view_home_list_large.xml │ │ │ │ ├── view_seen_overlay.xml │ │ │ │ ├── view_sync_firestore.xml │ │ │ │ └── widget_emision.xml │ │ │ ├── menu/ │ │ │ │ ├── activity_main_drawer_drawer.xml │ │ │ │ ├── bottom_menu.xml │ │ │ │ ├── chapter_casting_menu.xml │ │ │ │ ├── chapter_downloaded_menu.xml │ │ │ │ ├── chapter_menu.xml │ │ │ │ ├── chapter_menu_offline.xml │ │ │ │ ├── dir_menu.xml │ │ │ │ ├── dir_menu_material.xml │ │ │ │ ├── fav_menu.xml │ │ │ │ ├── fav_menu_material.xml │ │ │ │ ├── main.xml │ │ │ │ ├── main_material.xml │ │ │ │ ├── menu_achievements.xml │ │ │ │ ├── menu_anime_info.xml │ │ │ │ ├── menu_bug.xml │ │ │ │ ├── menu_download_info.xml │ │ │ │ ├── menu_download_options.xml │ │ │ │ ├── menu_ea.xml │ │ │ │ ├── menu_emision.xml │ │ │ │ ├── menu_explorer_connected.xml │ │ │ │ ├── menu_img.xml │ │ │ │ ├── menu_news_filters.xml │ │ │ │ ├── menu_play_queue.xml │ │ │ │ ├── menu_queue_group.xml │ │ │ │ ├── menu_queue_list.xml │ │ │ │ ├── menu_random.xml │ │ │ │ ├── menu_rating.xml │ │ │ │ ├── menu_records.xml │ │ │ │ ├── menu_seeing.xml │ │ │ │ ├── menu_seeing_auto.xml │ │ │ │ ├── menu_suggestions.xml │ │ │ │ └── menu_top.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values/ │ │ │ │ ├── attr.xml │ │ │ │ ├── bool.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── font_certs.xml │ │ │ │ ├── google_maps_api.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── ints.xml │ │ │ │ ├── preloaded_fonts.xml │ │ │ │ ├── shapes.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── values-land/ │ │ │ │ ├── bool.xml │ │ │ │ └── ints.xml │ │ │ ├── values-large/ │ │ │ │ └── bool.xml │ │ │ ├── values-night/ │ │ │ │ ├── bool.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── values-night-v27/ │ │ │ │ └── styles.xml │ │ │ ├── values-night-v28/ │ │ │ │ └── styles.xml │ │ │ ├── values-television/ │ │ │ │ └── bool.xml │ │ │ ├── values-v27/ │ │ │ │ └── styles.xml │ │ │ ├── values-v28/ │ │ │ │ └── styles.xml │ │ │ ├── values-v29/ │ │ │ │ └── strings.xml │ │ │ ├── values-xlarge/ │ │ │ │ └── bool.xml │ │ │ └── xml/ │ │ │ ├── backup_descriptor.xml │ │ │ ├── network_security.xml │ │ │ ├── path_providers.xml │ │ │ ├── preferences.xml │ │ │ ├── shortcuts.xml │ │ │ └── widget_emision.xml │ │ ├── playstore/ │ │ │ ├── AndroidManifest.xml │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ ├── ic_launcher_foregound.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── menu/ │ │ │ │ └── activity_main_drawer_drawer.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values/ │ │ │ │ ├── bool.xml │ │ │ │ ├── google_maps_api.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ └── xml/ │ │ │ └── preferences.xml │ │ ├── release/ │ │ │ └── res/ │ │ │ └── values/ │ │ │ └── google_maps_api.xml │ │ ├── test/ │ │ │ └── java/ │ │ │ └── knf/ │ │ │ └── kuma/ │ │ │ └── ExampleUnitTest.kt │ │ └── tv/ │ │ ├── AndroidManifest.xml │ │ └── res/ │ │ └── values/ │ │ ├── bool.xml │ │ └── strings.xml │ └── tv/ │ └── app-tv.apk ├── build.gradle ├── fastlane/ │ ├── .gitignore │ ├── Appfile │ └── Fastfile ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── persistentsearchview/ │ ├── .gitignore │ ├── bintray.gradle │ ├── build.gradle │ ├── gradle.properties │ ├── install.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── org/ │ │ └── cryse/ │ │ └── widget/ │ │ └── persistentsearch/ │ │ └── ApplicationTest.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ ├── androidx/ │ │ │ └── appcompat/ │ │ │ └── graphics/ │ │ │ └── drawable/ │ │ │ └── SupportDrawerArrowDrawable.java │ │ ├── io/ │ │ │ └── codetail/ │ │ │ ├── animation/ │ │ │ │ ├── RevealAnimator.java │ │ │ │ ├── SupportAnimator.java │ │ │ │ ├── SupportAnimatorLollipop.java │ │ │ │ ├── SupportAnimatorPreL.java │ │ │ │ └── ViewAnimationUtils.java │ │ │ └── widget/ │ │ │ ├── RevealFrameLayout.java │ │ │ └── RevealLinearLayout.java │ │ └── org/ │ │ └── cryse/ │ │ └── widget/ │ │ └── persistentsearch/ │ │ ├── DefaultVoiceRecognizerDelegate.java │ │ ├── HomeButton.java │ │ ├── LogoView.java │ │ ├── PersistentSearchView.java │ │ ├── RevealViewGroup.java │ │ ├── SearchItem.java │ │ ├── SearchItemAdapter.java │ │ ├── SearchSuggestionsBuilder.java │ │ ├── SimpleSearchListener.java │ │ ├── TextDrawable.java │ │ └── VoiceRecognitionDelegate.java │ └── res/ │ ├── layout/ │ │ ├── layout_searchitem.xml │ │ └── layout_searchview.xml │ └── values/ │ ├── attrs.xml │ ├── dimens.xml │ └── strings.xml ├── settings.gradle └── version.num ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [jordyamc] patreon: animeflvapp open_collective: # Replace with a single Open Collective username ko_fi: unbarredstream tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel custom: https://paypal.me/jordyamc ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Smartphone (please complete the following information):** - Device: [e.g. Google Pixel] - Android Version: [e.g. 5.0] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ *.iml .gradle local.properties .idea/workspace.xml .idea/libraries .DS_Store build captures .externalNativeBuild app/google-services.json app/src/main/java/knf/kuma/commons/EAHelper.java app/src/main/res/values/styles_colors.xml /*.png *.psd *.zip *.json *.pepk keys.keystore .idea /web/ /web/public/thumbs/ /resizer/ /app/playstore/ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jordyamc@hotmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Jordy Alexis Mendoza Caballero Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![](https://github.com/jordyamc/UKIKU/blob/master/web/img/UKIKU%20Facebook.png) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/4f8228880e0a4d3da8ee1c861801e5bf)](https://app.codacy.com/app/jordyamc/UKIKU?utm_source=github.com&utm_medium=referral&utm_content=jordyamc/UKIKU&utm_campaign=Badge_Grade_Dashboard) [![GitHub last commit](https://img.shields.io/github/last-commit/google/skia.svg)](https://github.com/jordyamc/UKIKU/commits/master) [![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/jordyamc/UKIKU/blob/master/LICENSE) [![GitHub forks](https://img.shields.io/github/forks/jordyamc/UKIKU.svg)](https://github.com/jordyamc/UKIKU/network/members) ## ¿Qué es UKIKU? Es una app derivada de mi antiguo proyecto [Animeflv App](https://github.com/jordyamc/Animeflv), tiene casi las mismas funciones, pero hecha desde cero para dispositivos con Android 5 o superior, esta pensada para tener mejor rendimiento y optimización, así como menor tamaño de [APK](https://github.com/jordyamc/UKIKU/raw/master/app/release/app-release.apk) (~6MB) ## ¿Por que un proyecto tan grande no tiene mucha publicidad y es código abierto? Soy un programador por hobby, no tengo experiencia profesional en la programación, pero aun así decidí aprender por mi cuenta, mis proyectos están hechos con el propósito de aprender a programar en el camino, aun con todo ese tiempo invertido quise que mis apps sean hechas por un fan para fans de anime, me gusta que la comunidad se apoye entre si, así que decidí que los propios usuarios elijan como y cuando apoyar a la app, pero aun así las donaciones son muy bien recibidas ;) ## ¿Como puedo modificar este proyecto? En la [wiki](https://github.com/jordyamc/UKIKU/wiki) de éste repositorio encontrarás los pasos para hacerlo! ================================================ FILE: app/.gitattributes ================================================ * -crlf ================================================ FILE: app/.gitignore ================================================ build app/src/main/java/knf/kuma/commons/EAHelper.java app/src/main/res/values/styles_colors.xml release/baselineProfiles tv/baselineProfiles ================================================ FILE: app/amazon/app-amazon.apk ================================================ [File too large to display: 12.4 MB] ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' def localProperties = new Properties() localProperties.load(new FileInputStream(rootProject.file("local.properties"))) android { compileSdk 36 defaultConfig { applicationId "knf.kuma" minSdkVersion 24 targetSdkVersion 36 versionCode 228 versionName "5.3.18" multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } } buildTypes { debug { manifestPlaceholders.admobID = localProperties['UKIKU_ADMOB_ID'] ?: "" buildConfigField "String", "IAB_KEY", localProperties['APPCOINS_IAB_KEY'] ?: "" buildConfigField "String", "IAB_UPDATE_ACTION", localProperties['APPCOINS_IAB_UPDATE_ACTION'] ?: "" buildConfigField "String", "IAB_BIND_ACTION", localProperties['APPCOINS_DEV_IAB_BIND_ACTION'] ?: "" buildConfigField "String", "IAB_BIND_PACKAGE", localProperties['APPCOINS_DEV_IAB_BIND_PACKAGE'] ?: "" buildConfigField "String", "APPCOINS_ADDRESS", localProperties['APPCOINS_ADDRESS'] ?: "" buildConfigField "String", "DROPBOX_TOKEN", localProperties['DROPBOX_TOKEN'] ?: "" buildConfigField "String", "EASTER_SEARCH", localProperties['EASTER_SEARCH'] ?: "" buildConfigField "String", "CIPHER_PWD_12", localProperties['UKIKU_PWD_12'] ?: "" buildConfigField "String", "CIPHER_PWD_16", localProperties['UKIKU_PWD_16'] ?: "" buildConfigField "String", "CIPHER_PWD_32", localProperties['UKIKU_PWD_32'] ?: "" buildConfigField "String", "ADM_FILE", localProperties['UKIKU_ADM_FILE'] ?: "" buildConfigField "String", "APPODEAL_KEY", localProperties['UKIKU_APPODEAL_KEY'] ?: "" resValue "string", "twitter_consumer_key", localProperties['UKIKU_TWITTER_API_KEY'] ?: "" resValue "string", "twitter_consumer_secret", localProperties['UKIKU_TWITTER_API_SECRET'] ?: "" } release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'proguard-android.txt' manifestPlaceholders.admobID = localProperties['UKIKU_ADMOB_ID'] ?: "" buildConfigField "String", "IAB_KEY", localProperties['APPCOINS_IAB_KEY'] ?: "" buildConfigField "String", "IAB_UPDATE_ACTION", localProperties['APPCOINS_IAB_UPDATE_ACTION'] ?: "" buildConfigField "String", "IAB_BIND_ACTION", localProperties['APPCOINS_DEV_IAB_BIND_ACTION'] ?: "" buildConfigField "String", "IAB_BIND_PACKAGE", localProperties['APPCOINS_DEV_IAB_BIND_PACKAGE'] ?: "" buildConfigField "String", "APPCOINS_ADDRESS", localProperties['APPCOINS_ADDRESS'] ?: "" buildConfigField "String", "DROPBOX_TOKEN", localProperties['DROPBOX_TOKEN'] ?: "" buildConfigField "String", "EASTER_SEARCH", localProperties['EASTER_SEARCH'] ?: "" buildConfigField "String", "CIPHER_PWD_12", localProperties['UKIKU_PWD_12'] ?: "" buildConfigField "String", "CIPHER_PWD_16", localProperties['UKIKU_PWD_16'] ?: "" buildConfigField "String", "CIPHER_PWD_32", localProperties['UKIKU_PWD_32'] ?: "" buildConfigField "String", "ADM_FILE", localProperties['UKIKU_ADM_FILE'] ?: "" buildConfigField "String", "APPODEAL_KEY", localProperties['UKIKU_APPODEAL_KEY'] ?: "" resValue "string", "twitter_consumer_key", localProperties['UKIKU_TWITTER_API_KEY'] ?: "" resValue "string", "twitter_consumer_secret", localProperties['UKIKU_TWITTER_API_SECRET'] ?: "" } tv { initWith release applicationIdSuffix ".tv" } playstore { initWith release } amazon { initWith release } } sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } buildFeatures { viewBinding = true } compileOptions { sourceCompatibility = '17' targetCompatibility = '17' } kotlinOptions { jvmTarget = '17' } packagingOptions { resources { excludes += ['AndroidManifest.xml'] excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'] } } namespace 'knf.kuma' } configurations.all { resolutionStrategy { force 'org.jsoup:jsoup:1.10.3' //force 'androidx.core:core-ktx:1.6.0' } } allprojects { repositories { mavenLocal() google() mavenCentral() //jcenter() maven { url "https://jitpack.io" } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } maven { url "https://s3.amazonaws.com/repo.commonsware.com" } maven { url "https://dl.bintray.com/drummer-aidan/maven/" } maven { url "https://dl.bintray.com/blockchainds/bds" } maven { url "https://dl.bintray.com/asf/asf" } } } dependencies { //implementation project(path:':multidisplaycast', configuration:'default') implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') implementation 'com.github.jordyamc:Multi-Display-Cast:2.20' implementation 'com.github.jordyamc:PersistentSearchView:1.0.10' implementation 'com.github.jordyamc:UAGenerator:1.2.2' implementation 'com.github.jordyamc:AndroidVideoCache:2.7.6' implementation 'com.github.jordyamc:Cloudflare-Bypasser:1.0.26' implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.browser:browser:1.5.0' //Keep implementation 'androidx.lifecycle:lifecycle-common-java8:2.10.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.10.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0' implementation 'androidx.activity:activity-ktx:1.7.2' //Keep implementation 'androidx.fragment:fragment-ktx:1.8.9' implementation 'androidx.startup:startup-runtime:1.2.0' implementation 'com.google.android.material:material:1.13.0' implementation 'androidx.vectordrawable:vectordrawable-animated:1.2.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.mediarouter:mediarouter:1.4.0' //Keep implementation 'androidx.recyclerview:recyclerview:1.4.0' implementation 'androidx.leanback:leanback:1.2.0' implementation 'androidx.tvprovider:tvprovider:1.1.0' implementation 'androidx.palette:palette-ktx:1.0.0' implementation 'com.google.android.gms:play-services-cast:22.2.0' implementation platform('com.google.firebase:firebase-bom:34.7.0') implementation 'com.google.firebase:firebase-firestore' implementation 'com.google.firebase:firebase-auth' implementation 'com.google.firebase:firebase-config' implementation 'com.google.firebase:firebase-crashlytics' implementation 'com.google.android.gms:play-services-auth:20.5.0' //Keep //implementation 'com.google.ads.mediation:adcolony:4.4.1.0' implementation 'com.firebaseui:firebase-ui-auth:9.1.1' implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' implementation 'io.reactivex.rxjava3:rxjava:3.1.12' implementation 'com.google.android.gms:play-services-maps:19.2.0' api 'com.google.guava:guava:33.5.0-android' implementation 'androidx.annotation:annotation:1.9.1' implementation 'androidx.slice:slice-builders-ktx:1.0.0-alpha08' implementation 'androidx.slice:slice-builders:1.0.0' implementation 'androidx.work:work-runtime-ktx:2.11.0' implementation 'androidx.room:room-runtime:2.8.4' implementation 'androidx.room:room-ktx:2.8.4' kapt 'androidx.room:room-compiler:2.8.4' androidTestImplementation 'androidx.room:room-testing:2.8.4' implementation 'com.google.dagger:dagger-android-support:2.57.2' kapt 'com.google.dagger:dagger-android-processor:2.57.2' implementation 'androidx.paging:paging-runtime-ktx:3.3.6' implementation 'pl.droidsonroids:jspoon:1.3.3' implementation 'pl.droidsonroids.retrofit2:converter-jspoon:1.3.2' implementation 'org.jsoup:jsoup:1.13.1' //Keep implementation 'com.squareup.retrofit2:adapter-rxjava2:3.0.0' implementation 'com.squareup.picasso:picasso:2.8' implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' implementation 'com.squareup.okhttp3:okhttp:5.3.2' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:5.3.2' implementation 'com.github.jordyamc:ChipsLayoutManager:0.5.3' implementation 'com.google.code.gson:gson:2.13.2' implementation 'me.zhanghai.android.materialratingbar:library:1.4.0' implementation 'at.blogc:expandabletextview:1.0.5' implementation 'com.google.android.exoplayer:exoplayer:2.19.1' implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1' implementation 'com.google.android.exoplayer:extension-mediasession:2.19.1' implementation 'com.google.android.exoplayer:extension-okhttp:2.19.1' //MaterialDialogs implementation 'com.afollestad.material-dialogs:core:3.3.0' implementation 'com.afollestad.material-dialogs:lifecycle:3.3.0' implementation 'com.afollestad.material-dialogs:input:3.3.0' implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' //--------- implementation 'org.apache.commons:commons-text:1.15.0' implementation 'com.shamanland:xdroid-toaster:0.3.0' implementation 'com.github.MFlisar:DragSelectRecyclerView:0.3' implementation 'com.github.StephenVinouze:MaterialNumberPicker:1.1.0' implementation 'biz.kasual:materialnumberpicker:1.2.1' implementation 'org.nanohttpd:nanohttpd:2.3.1' implementation 'com.dropbox.core:dropbox-core-sdk:7.0.0' implementation 'com.dropbox.core:dropbox-android-sdk:7.0.0' implementation 'com.github.daniel-stoneuk:material-about-library:3.2.0-rc01' implementation 'io.github.luizgrp.sectionedrecyclerviewadapter:sectionedrecyclerviewadapter:1.2.0' implementation 'nl.dionsegijn:konfetti:1.3.2' implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' implementation "com.github.tonyofrancis.Fetch:fetch2:3.4.1" implementation "com.github.tonyofrancis.Fetch:fetch2:3.4.1" implementation "com.github.tonyofrancis.Fetch:fetch2rx:3.4.1" implementation "com.github.tonyofrancis.Fetch:fetch2okhttp:3.4.1" implementation 'q.rorbin:badgeview:1.1.3' implementation 'com.jaredrummler:android-device-names:2.1.1' implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1' implementation 'com.mikhaellopez:circularprogressbar:2.0.0' implementation 'moe.feng:MaterialStepperView:0.2.4.2' implementation 'com.github.bumptech.glide:glide:4.16.0' implementation 'com.github.jordyamc:kprobability:1.0.5' implementation 'org.nield:kotlin-statistics:1.2.1' implementation 'com.github.Jamshid-M:ExactRatingBar:1.0.0' implementation 'com.github.jordyamc:CloudflareBypassWebview:1.0.4' implementation 'com.github.jordyamc:apk-signatures:1.0.3' implementation 'com.github.sephiroth74:android-target-tooltip:2.0.4' implementation 'com.github.jordyamc.oasis-jsbridge-android:oasis-jsbridge-duktape:1.0.2' kapt 'com.github.bumptech.glide:compiler:4.16.0' implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2' implementation 'fr.bmartel:jspeedtest:1.32.1' implementation 'com.mani:ThinDownloadManager:1.4.0' implementation 'com.github.simbiose:Encryption:1.4.0' implementation 'com.github.marlonlom:timeago:4.1.0' implementation 'com.scottyab:secure-preferences-lib:0.1.7' implementation 'com.github.florent37:expansionpanel:1.2.2' implementation 'com.github.evgenyneu:js-evaluator-for-android:v5.0.0' implementation 'com.github.vkay94:DoubleTapPlayerView:1.0.4' implementation 'org.conscrypt:conscrypt-android:2.5.3' testImplementation 'junit:junit:4.13.2' //Kotlin implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.1.21' implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2' implementation 'org.jetbrains.anko:anko:0.10.8' implementation 'org.jetbrains.anko:anko-coroutines:0.10.8' //------ //Moon //implementation 'io.github.darkryh.moongetter:moongetter-core:2.0.0-alpha02' //implementation 'io.github.darkryh.moongetter:moongetter-client-okhttp:2.0.0-alpha02' //implementation 'io.github.darkryh.moongetter:moongetter-client-cookie-java-net:2.0.0-alpha02' //implementation 'io.github.darkryh.moongetter:moongetter-client-trustmanager-java-net:2.0.0-alpha02' //implementation 'io.github.darkryh.moongetter:moongetter-android-robot:2.0.0-alpha02' // //Applovin implementation 'com.applovin:applovin-sdk:+' implementation 'com.applovin.mediation:inmobi-adapter:+' implementation 'com.google.android.ump:user-messaging-platform:3.1.0' //------- androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.work:work-testing:2.8.1' //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' } ================================================ FILE: app/fabric.properties ================================================ #Contains API Secret used to validate your application. Commit to internal source control; avoid making secret public. #Sun Jan 07 11:02:25 CST 2018 apiSecret=88ba4d6880340c6e16c2690f626c60dcedff2e3a38650f2a8fb206f22f3e62d8 ================================================ FILE: app/proguard-android.txt ================================================ # This is a configuration file for ProGuard. # http://proguard.sourceforge.net/index.html#manual/usage.html # # This file is no longer maintained and is not used by new (2.2+) versions of the # Android plugin for Gradle. Instead, the Android plugin for Gradle generates the # default rules at build time and stores them in the build directory. -dontusemixedcaseclassnames -dontskipnonpubliclibraryclasses -verbose # Optimization is turned off by default. Dex does not like code run # through the ProGuard optimize and preverify steps (and performs some # of these optimizations on its own). -dontoptimize -dontpreverify # Note that if you want to enable optimization, you cannot just # include optimization flags in your own project configuration file; # instead you will need to point to the # "proguard-android-optimize.txt" file instead of this one from your # project.properties file. -keepattributes *Annotation* -keep public class com.google.vending.licensing.ILicensingService -keep public class com.android.vending.licensing.ILicensingService # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native -keepclasseswithmembernames class * { native ; } # keep setters in Views so that animations can still work. # see http://proguard.sourceforge.net/manual/examples.html#beans -keepclassmembers public class * extends android.view.View { void set*(***); *** get*(); } # We want to keep methods in Activity that could be used in the XML attribute onClick -keepclassmembers class * extends android.app.Activity { public void *(android.view.View); } # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } -keepclassmembers class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator CREATOR; } -keepclassmembers class **.R$* { public static ; } # The support library contains references to newer platform versions. # Don't warn about those in case this app is linking against an older # platform version. We know about them, and they are safe. -dontwarn android.support.** # Understand the @Keep support annotation. -keep class android.support.annotation.Keep -keep @android.support.annotation.Keep class * {*;} -keepclasseswithmembers class * { @android.support.annotation.Keep ; } -keepclasseswithmembers class * { @android.support.annotation.Keep ; } -keepclasseswithmembers class * { @android.support.annotation.Keep (...); } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # 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 *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile -keepattributes *Annotation* -keepattributes SourceFile,LineNumberTable -keep public class * extends java.lang.Exception -keep @interface kotlin.coroutines.jvm.internal.DebugMetadata { *; } -keep class **$$TypeAdapter { *; } -keep class knf.kuma.pojos.AnimeObject.WebInfo.AnimeChapter {*;} -keep class knf.kuma.pojos.AnimeObject.WebInfo.AnimeRelated {*;} -keep class androidx.startup.InitializationProvider {*;} -keepclasseswithmembernames class * { @com.tickaroo.tikxml.* ; } -keepclasseswithmembernames class * { @com.tickaroo.tikxml.* ; } -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } -dontwarn retrofit2.** -keep class retrofit2.** { *; } -keepclassmembers class * implements pl.droidsonroids.jspoon.ElementConverter -keep class pl.droidsonroids.jspoon.** { *; } -keepattributes Signature -keepattributes Exceptions -keepclasseswithmembers class * { @retrofit2.http.* ; } -keep class okhttp3.** { *; } -keep interface okhttp3.** { *; } -dontwarn okhttp3.** -dontwarn knf.kuma.** -dontwarn kotlinx.coroutines.** -keep public enum knf.kuma.**{*;} -keep class es.munix.multidisplaycast.**{*;} -keep class com.connectsdk.**{* ;} -keepclassmembers public class * implements pl.droidsonroids.jspoon.ElementConverter { public (...); } -keepclassmembernames class kotlinx.** { volatile ; } -keep public class * extends java.lang.Exception -keep class org.jsoup.**{*;} -keep class com.google.gson.reflect.TypeToken -keep class * extends com.google.gson.reflect.TypeToken -keep public class * implements java.lang.reflect.Type -keeppackagenames org.jsoup.nodes -dontwarn com.amazon.client.metrics.** -dontwarn com.beloo.widget.chipslayoutmanager.** -dontwarn com.squareup.okhttp.** -dontwarn dagger.android.** -dontwarn dagger.android.support.** -dontwarn okhttp3.** -dontwarn okhttp3.internal.** -dontwarn okio.** -dontwarn retrofit2.** -dontwarn com.dropbox.core.** -dontwarn io.branch.** -dontwarn com.bumptech.glide.** -dontwarn com.smaato.soma.SomaUnityPlugin* -dontwarn com.millennialmedia** -dontwarn com.facebook.** -dontwarn com.google.common.** -dontwarn com.google.android.** -dontwarn com.google.firebase.** -dontwarn org.jetbrains.anko.db.** -dontwarn com.afollestad.materialdialogs.** -dontwarn com.amazon.** -dontwarn xdroid.toaster.** -dontwarn com.pavelsikun.seekbarpreference.** -dontwarn com.jakewharton.picasso.** -dontwarn io.grpc.** -dontwarn kotlin.internal.** -dontwarn at.blogc.android.** -dontwarn com.afollestad.** -dontwarn com.connectsdk.** -dontwarn com.danielstone.materialaboutlibrary.** -dontwarn com.github.rubensousa.previewseekbar.** -dontwarn com.mikhaellopez.circularprogressbar.** -dontwarn com.simplecityapps.** -dontwarn com.tbuonomo.viewpagerdotsindicator.** -dontwarn es.munix.multidisplaycast.** -dontwarn fr.bmartel.speedtest.** -dontwarn me.zhanghai.android.** -dontwarn moe.feng.common.** -dontwarn com.github.stephenvinouze.materialnumberpickercore.** -dontwarn org.cryse.widget.persistentsearch.** -dontwarn pl.droidsonroids.jspoon.** -dontwarn org.jetbrains.anko.** -dontwarn nl.dionsegijn.konfetti.** -dontwarn kotlinx.android.** -dontwarn kotlin.** -dontwarn io.opencensus.** -dontwarn dagger.** -dontwarn com.tonyodev.** -dontwarn ir.mahdiparastesh.chlm.Orientation ================================================ FILE: app/release/app-release.apk ================================================ [File too large to display: 33.1 MB] ================================================ FILE: app/src/amazon/AndroidManifest.xml ================================================ ================================================ FILE: app/src/amazon/res/drawable/ic_launcher_foregound.xml ================================================ ================================================ FILE: app/src/amazon/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/amazon/res/menu/activity_main_drawer_drawer.xml ================================================ ================================================ FILE: app/src/amazon/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/amazon/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/amazon/res/values/bool.xml ================================================ false false true false false 0 false ================================================ FILE: app/src/amazon/res/values/google_maps_api.xml ================================================ AIzaSyD2QS_E66qKxXmJhlXW8eRT7OYNdHphWUY ================================================ FILE: app/src/amazon/res/values/ic_launcher_background.xml ================================================ #000000 ================================================ FILE: app/src/amazon/res/xml/preferences.xml ================================================ ================================================ FILE: app/src/androidTest/java/knf/kuma/ExampleInstrumentedTest.kt ================================================ package knf.kuma import android.util.Log import androidx.test.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import androidx.work.Configuration import androidx.work.testing.SynchronousExecutor import androidx.work.testing.WorkManagerTestInitHelper import org.junit.Before import org.junit.Test import org.junit.runner.RunWith /** * Instrumented test, which will execute on an Android device. * * @see [Testing documentation](http://d.android.com/tools/testing) */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Before fun setup() { val context = InstrumentationRegistry.getTargetContext() val config = Configuration.Builder() .setMinimumLoggingLevel(Log.DEBUG) .setExecutor(SynchronousExecutor()) .build() WorkManagerTestInitHelper.initializeTestWorkManager(context, config) } @Test @Throws(Exception::class) fun testRecentWork() { } } ================================================ FILE: app/src/debug/res/xml/preferences.xml ================================================ ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/assets/changelog.xml ================================================ Mejoras StreamWish Error al mostrar lista de recientes en modo "Home" Corrección servidor StreamWish Prevenir error al descargar actualización Enlaces de telegram actualizados Corrección StreamWish Corrección imágenes en explorador Error al reproducir videos en el reproductor interno en algunos casos Error al descargar en algunos dispositivos Corrección de imágenes Error al obtener lista de recientes en algunos casos Corrección servidor StreamWish Corrección de imágenes en recientes Soporte Android 16 Corrección en login de firebase Correcciónes generales para Android 15 Corrección en noticias Corrección StreamWish Corrección comentarios Corrección StreamWish Mejora de conexión para CAST Errores generales Corrección StreamWish Eliminar librerias sin usar Errores generales Error al reproducir en TV Error en streaming iniciado desde informacion de anime Error al obtener información de animes con comillas en el nombre Error en descargas Soporte Android 14 Corrección de bugs Corrección especifica para Xiaomi de SSL Corrección de bugs Corrección de SSL para telefonos sin GPlay Corrección de Streamwish para algunos capitulos viejos Corrección de bugs Corrección StreamWish Error al mostrar dialogo de actualizacion Error en bypass Error en bypass Error en bypass Servidor Streamwish (Solo streaming) Mensaje de error para error desconocido en Peru Soporte mejorado para Okru y Yourupload Error al expandir texto en informacion de anime Error al buscar imagenes de mejor calidad Error al verificar suscripciones Error en backup local Error al iniciar en TV Correccion para noticias Correccion para Android 12 Imagenes de noticias Servidor SBVideo Error al guardar progreso en reproductor interno Botón de pausa en reproductor interno Diagnostico reflejara el bypass en estado general Servidor stape Arreglo en version de TV Error al mostrar imagenes en noticias Errores en login de Firestore Errores generales Error al mostrar noticias Errores generales Mejoras de bypass Errores generales Mejoras de bypass para TV Errores generales Servidor stape Errores generales Errores generales Errores generales Errores generales Error al mostrar algunas secciones en telefonos sin Play Services Error al mostrar memoria disponible en versiones de Android recientes Errores generales Cambio de pagina Ukiku.app Errores generales Seleccion de carpeta para descargas en Android 11 Mejora en creacion de bypass Mejora al cargar recientes Mejoras en explorador Errores generales Nuevo metodo de bypass Errores generales Errores en servidores Errores generales Descargar directorio para aligerar carga Errores al abrir informacion de anime Errores generales Errores generales Optimización general Mejoras en reproductor Reproductor Experimental renombrado a Avanzado Intento para sobrepasar bloqueo 403 Errores generales Errores generales Errores generales Errores generales Nuevo diseño de la app Agregada opcion para cambiar a diseño clasico Errores generales Errores generales Anuncios activados por defecto Cambios en personalización de anuncios Error al crear directorio Errores generales Optimizacion para anuncios generales Errores generales Optimizacion para anuncios en listas Errores generales Servidor GoCDN Servidor Stape Errores generales Error al detectar numero de episodio en explorador Error al detectar episodios en explorador Errores generales Errores generales Error al detectar capitulos descargados en Android 10 Errores generales Barra de estado con aviso para creacion de directorio Avisos mas claros cuando falta permiso de almacenamiento Error al reproducir animes descargados en reproductor interno Error al marcar como visto desde explorador Error al desmarcar como visto en algunas secciones Errores generales Error al detectar capitulos descargados Mensajes de error al otorgar permiso de almacenamiento Errores por cambio en Animeflv Comentarios por Disqus Fembed revivido Errores generales Servidor Fembed revivido Servidor ZippyShare Error al reproducir izanagi en ALGUNOS episodios Error al abrir animes con modo Home Errores generales Errores generales Errores generales Error en explorador - Android 10 Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Seccion FAQ (Preguntas frecuentes) Errores generales Suscripcion para Firestore sin anuncios Error al reproducir en modo TV Errores generales Suscripcion para Firestore sin anuncios Errores generales Overlay para mejorar visibilidad de botones en informacion de anime Errores generales Errores generales Errores generales Error al notificar recientes Errores generales UI mas compacta en pantalla de backups Top de videos vistos Modo obscuro por defecto para Android 10 Error al obtener monedas en algunos telefonos Errores generales Anuncios opcionales Método de backup Firestore Modo family friendly Moneda de la app loli-coins Nuevo servidor VeryStream Errores generales Mejoras en interfaz de TV Errores generales Errores generales Opción para ocultar animes nuevos en "Home" Errores generales Errores generales Errores generales Errores generales Estructura interna de respaldos mejorado Método de respaldo local (memoria interna) Errores generales Errores generales Errores generales Barra de estado para Animeflv Errores generales Estilo home para recientes Botón para saltar 1:30m en CAST (OP y ED) Errores generales Prueba rapida de velocidad en diagnostico Soporte previo para Android Q Filtros de busqueda mejorados Comentarios mejorados Lista de descargas mejorada Errores generales Servidor Fembed Restaurar compras al entrar a la pantalla del easter egg Errores generales en TV Errores generales Errores generales Errores generales Errores en TV Errores en respaldos Errores en autobackup Cambios al diseño para MD2 Nuevo dato en informacion: "Seguidores" Optimizacion general (sobre todo en sugeridos) Google Drive deshabilitado Categoría pausado en seguidos Recordar último servidor Descarga por lotes Añadir a cola por lotes Preview al avanzar en reproductor experimental Opcion para usar proxy en CAST Errores generales Material Design 2 Mejora de carga en historial Pantalla de diagnóstico en informacion de la app Errores generales Opción para subir tiempo de espera de conexión (timeout) Errores generales Errores generales Opción para comprar pasos de Easter egg Mostrar todas las imágenes disponibles de MAL Errores de bypass en TV Mensajes de servicio Errores de bypass en TV Errores generales Errores generales Indicador de favoritos en emisión Errores generales Errores generales Errores generales Errores generales Errores generales Errores generales Soporte para multiples idiomas en algunos animes Errores generales Errores generales Errores generales Errores generales Errores generales Mejora en imagen de Splash Errores generales Errores generales Errores generales Errores generales Errores con algunos logros Mejoras esteticas para modo claro en Android 8+ Mejora de modo cuadrícula para teléfonos anchos y tablets Errores generales Logros Mejoras generales Tema claro mejorado Cambios con Material Design 2 Errores generales Sección simple de noticias Errores generales Arreglos menores con CAST Mejora en imagenes de error en siguiendo Errores generales Mejoras graficas en algunas partes Mejoras al modificar categorias en favoritos Opcion para cambiar tono de notificación en recientes Seccion de Siguiendo recreada Error en pantalla de actualización Errores generales Errores en Android TV Errores generales Migración de Java a Kotlin Mejoras de estabilidad Launch screen al iniciar la app Algunos dialogos cambiados a snackbar Errores generales Errores generales Botón en reproductor para saltar 1:25min(Oppening) Carátula del anime en notificacion de reciente Mejora al ocultar barras en reproductor interno Errores generales Errores generales Errores generales Opción para auto respaldar periodicamente Errores generales Botón cast en información de anime Filtrado por ID en directorio Errores generales Importacion multiple Cambio en easteregg, se necesita completar de nuevo Errores generales Errores generales Indicadores numericos en algunas partes de la app Migración a librerias androidx Errores generales Cambiar administrador de descargas Mejora en notificaciones de descarga Mejora en iconos de atajos Errores generales Errores generales Servidor Fenix Servidor Natsuki Las descargas ahora mostraran tiempo restante y velocidad de descarga Errores generales Errores generales Errores generales Administrador de descargas rehecho Descargas paralelas Pausar descargas Tiempo estimado de descarga Errores generales Configurar buffer de descarga Mejoras al importar Errores generales Importar videos a la app Descarga/Streaming desde notificacion de reciente Mejoras generales Errores generales Compartir links de episodios Descarga/Streaming abriendo links de episodios Se podra añadir animes a seguidos y favoritos Errores generales Errores generales Servidor Mango Error en preview de episodio Errores en explorador Error al mostrar algunas imagenes Servidor Mp4Upload (Gracias raulhaag) Tiempo reducido al obtener lista de animes en explorador Errores generales Errores en RV Servidor Izanagi restaurado Errores generales Errores en OKRU y RV Categorias en favoritos Errores generales Seccion emision Errores generales Migracion de datos mejorada Obtencion de imagenes de episodios Opción para deshabilitar progreso en pantalla Obtencion de lista de episodios Progreso de descarga en recientes y lista de episodios Notificacion persistente al crear/recrear directorio Error en listar episodios de animes largos Errores en Cast Errores generales en Android TV Errores generales Diseño especial para Android TV Errores generales Servidor RV Mejora en pendientes Mejora en explorador Error en nombre de animes Soporte para Android TV Errores generales Error al hacer cast de archivos locales Servidor Mega duplicado Servidor Fire corregido Marcar episodios pendientes como vistos y el ultimo es añadido a historial Errores generales Cola de reproducción Error de colores Error al abrir informacion de animes sin conexion Click largo en emisión para ocultar Widget de emisión agregado Abrir pantalla de genero desde informacion de anime Easter egg agregado, pista: "easteregg" Modo PIP en reproductor mejorado Explorador optimizado Errores generales Version final Accesos directos en launcher Errores generales Agregada mas informacion en acerca de Errores generales Error al notificar animes nuevos Seccion de sugerencias Errores generales Icono arreglado para algunos telefonos Bypass de cloudflare agregado Boton cast en explorador ahora abre controles Pantalla de informacion Errores generales Error al actualizar app en Android 5 y 6 Pantalla para descargas activas (Explorador>Descargas) Error al migrar datos Sincronizacion en la nube (Dropbox y Google Drive) Migración de favoritos desde AnimeflvApp Errores en explorador al girar la pantalla Cast de episodios descargados Error al abrir la app sin conexión Errores generales Changelog Sección de animes random Errores generales ================================================ FILE: app/src/main/java/knf/kuma/App.kt ================================================ package knf.kuma /*import com.asf.appcoins.sdk.ads.AppCoinsAds import com.asf.appcoins.sdk.ads.AppCoinsAdsBuilder*/ import android.annotation.SuppressLint import android.annotation.TargetApi import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.media.AudioAttributes import android.os.Build import androidx.appcompat.app.AppCompatDelegate import androidx.work.Configuration import es.munix.multidisplaycast.CastManager import knf.kuma.achievements.AchievementManager import knf.kuma.commons.AllSSLOkHttpClient import knf.kuma.commons.PrefsUtil import knf.kuma.directory.DirectoryService import knf.kuma.download.DownloadManager import knf.kuma.download.DownloadService import knf.kuma.jobscheduler.BackUpWork import knf.kuma.jobscheduler.RecentsWork import knf.kuma.jobscheduler.UpdateWork import knf.kuma.widgets.emision.WEmissionService class App : Application(), Configuration.Provider { //private lateinit var appCoinsAds: AppCoinsAds @TargetApi(Build.VERSION_CODES.O) private fun createChannels() { val manager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager val dirChannel = NotificationChannel( DirectoryService.CHANNEL, getString(R.string.directory_channel_title), NotificationManager.IMPORTANCE_MIN ) dirChannel.setSound( null, AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) .setUsage(AudioAttributes.USAGE_NOTIFICATION).build() ) dirChannel.setShowBadge(false) manager?.createNotificationChannel(dirChannel) manager?.createNotificationChannel( NotificationChannel( RecentsWork.CHANNEL_RECENTS, "Capitulos recientes", NotificationManager.IMPORTANCE_HIGH ) ) manager?.createNotificationChannel( NotificationChannel( DownloadService.CHANNEL, "Descargas", NotificationManager.IMPORTANCE_HIGH ) ) manager?.createNotificationChannel( NotificationChannel( DownloadService.CHANNEL_ONGOING, "Descargas en progreso", NotificationManager.IMPORTANCE_LOW ).apply { setShowBadge(false) }) manager?.createNotificationChannel( NotificationChannel( DownloadManager.CHANNEL_FOREGROUND, "Administrador de descargas", NotificationManager.IMPORTANCE_MIN ).apply { setShowBadge(false) }) manager?.createNotificationChannel( NotificationChannel( UpdateWork.CHANNEL, "Actualización de la app", NotificationManager.IMPORTANCE_DEFAULT ) ) manager?.createNotificationChannel( NotificationChannel( WEmissionService.CHANNEL, "Actualizador de widget", NotificationManager.IMPORTANCE_MIN ).apply { setShowBadge(false) }) } override val workManagerConfiguration: Configuration get() = Configuration.Builder().build() override fun onCreate() { super.onCreate() context = this if (!PrefsUtil.isFetchDBReset) { PrefsUtil.isFetchDBReset = true deleteDatabase("LibGlobalFetchLib.db") } AppCompatDelegate.setDefaultNightMode(PrefsUtil.themeOption.toInt()) AllSSLOkHttpClient.enableTLS() BackUpWork.checkInit() CastManager.register(this) AchievementManager.init(this) initAppCoins() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannels() } private fun initAppCoins() { /*appCoinsAds= AppCoinsAdsBuilder() .withDebug(BuildConfig.DEBUG) .createAdvertisementSdk(this) .also { it.init(this) }*/ } companion object { @SuppressLint("StaticFieldLeak") lateinit var context: Context private set } } ================================================ FILE: app/src/main/java/knf/kuma/AppInfoActivity.kt ================================================ package knf.kuma import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.Fragment import knf.kuma.custom.SingleFragmentActivity class AppInfoActivity: SingleFragmentActivity() { override fun createFragment(): Fragment = AppInfoFragment().apply { arguments = Bundle().apply { putBoolean("isFlat",false) } } override fun getActivityTitle(): String = "Acerca de" companion object{ fun open(context: Context){ context.startActivity(Intent(context,AppInfoActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/AppInfoActivityMaterial.kt ================================================ package knf.kuma import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.Fragment import knf.kuma.custom.SingleFragmentMaterialActivity class AppInfoActivityMaterial: SingleFragmentMaterialActivity() { override fun createFragment(): Fragment = AppInfoFragment().apply { arguments = Bundle().apply { putBoolean("isFlat",true) } } override fun getActivityTitle(): String = "Acerca de" companion object{ fun open(context: Context){ context.startActivity(Intent(context,AppInfoActivityMaterial::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/AppInfoFragment.kt ================================================ package knf.kuma import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.core.net.toUri import com.afollestad.materialdialogs.MaterialDialog import com.danielstone.materialaboutlibrary.ConvenienceBuilder import com.danielstone.materialaboutlibrary.MaterialAboutFragment import com.danielstone.materialaboutlibrary.items.MaterialAboutActionItem import com.danielstone.materialaboutlibrary.model.MaterialAboutCard import com.danielstone.materialaboutlibrary.model.MaterialAboutList import knf.kuma.ads.AdsUtils import knf.kuma.ads.FullscreenAdLoader import knf.kuma.ads.getFAdLoaderInterstitial import knf.kuma.ads.getFAdLoaderRewarded import knf.kuma.backup.Backups import knf.kuma.changelog.ChangelogActivity import knf.kuma.changelog.ChangelogActivityMaterial import knf.kuma.commons.DesignUtils import knf.kuma.commons.EAUnlockActivity import knf.kuma.commons.Economy import knf.kuma.commons.PrefsUtil import knf.kuma.commons.isFullMode import knf.kuma.commons.safeShow import knf.kuma.profile.TopActivity import knf.kuma.profile.TopActivityMaterial import knf.tools.kprobability.item import knf.tools.kprobability.probabilityOf import org.jetbrains.anko.support.v4.toast class AppInfoFragment: MaterialAboutFragment() { private val paypalUri: Uri get() { val uriBuilder = Uri.Builder() uriBuilder.scheme("https").authority("www.paypal.com").path("cgi-bin/webscr") uriBuilder.appendQueryParameter("cmd", "_donations") uriBuilder.appendQueryParameter("business", "jordyamc@hotmail.com") uriBuilder.appendQueryParameter("lc", "US") uriBuilder.appendQueryParameter("item_name", "Donación UKIKU") uriBuilder.appendQueryParameter("no_note", "1") uriBuilder.appendQueryParameter("no_shipping", "1") uriBuilder.appendQueryParameter("currency_code", "USD") return uriBuilder.build() } private val isFlat: Boolean by lazy { requireArguments().getBoolean("isFlat",true) } private val rewardedAd: FullscreenAdLoader by lazy { getFAdLoaderRewarded(requireActivity()) } private lateinit var interstitial: FullscreenAdLoader private lateinit var videoItem: MaterialAboutActionItem private fun showAd() { probabilityOf<() -> Unit> { item({ rewardedAd.show() }, AdsUtils.remoteConfigs.getDouble("rewarded_percent")) item({ interstitial.show() }, AdsUtils.remoteConfigs.getDouble("interstitial_percent")) }.random()() } private fun setupUpdateCount() { Economy.rewardedVideoLiveData.observe(viewLifecycleOwner) { if (::videoItem.isInitialized) { videoItem.subText = "Vistos: $it" setMaterialAboutList(getMaterialAboutList(requireContext())) } } } override fun getMaterialAboutList(context: Context): MaterialAboutList { val infoCard = MaterialAboutCard.Builder() infoCard.outline(isFlat) infoCard.addItem(ConvenienceBuilder.createAppTitleItem(requireContext())) infoCard.addItem(ConvenienceBuilder.createVersionActionItem(requireContext(), getDrawable(requireContext(),R.drawable.ic_version), "Versión", true)) infoCard.addItem(MaterialAboutActionItem.Builder().text("Changelog").icon(R.drawable.ic_changelog_get).setOnClickAction { openChangelog() }.build()) infoCard.addItem(MaterialAboutActionItem.Builder().text("Diagnóstico").icon(R.drawable.ic_diagnostic).setOnClickAction { openDiagnostic() }.build()) infoCard.addItem(MaterialAboutActionItem.Builder().text("Suscripción").icon(R.drawable.ic_key).setOnClickAction { if (Backups.isKeyInstalled) { val intent = requireContext().packageManager.getLaunchIntentForPackage("knf.kuma.key") intent?.let { startActivity(intent) } ?: toast("Error al abrir UKIKU Key") } else { MaterialDialog(requireContext()).safeShow { message(text = "Con la suscripción podrás usar el backup por Firestore sin activar los anuncios!") positiveButton(text = "Suscribirse") { startActivity(Intent(Intent.ACTION_VIEW, "https://play.google.com/store/apps/details?id=knf.kuma.key".toUri())) } negativeButton(text = "cancelar") } } }.build()) val authorCard = MaterialAboutCard.Builder() authorCard.outline(isFlat) authorCard.title("Autor") authorCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_author), "Jordy Mendoza", true, "https://t.me/unbarred_stream".toUri() ) ) val donateCard = MaterialAboutCard.Builder() if (isFullMode) { donateCard.outline(isFlat) donateCard.title("Donar") donateCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_paypal), "Paypal", false, "https://www.paypal.com/donate/?hosted_button_id=9WXSCG3AP639J".toUri() ) ) donateCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_patreon), "Patreon", false, "https://www.patreon.com/animeflvapp".toUri() ) ) donateCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_cuplogo), "Ko-fi", false, "https://ko-fi.com/unbarredstream".toUri() ) ) donateCard.addItem( MaterialAboutActionItem.Builder().text("Ver anuncio") .subText("Vistos: ${PrefsUtil.userRewardedVideoCount}").icon(R.drawable.ic_cash) .setOnClickAction { showAd() }.build().also { videoItem = it }) } val extraCard = MaterialAboutCard.Builder() extraCard.outline(isFlat) extraCard.title("Extras") extraCard.addItem( MaterialAboutActionItem.Builder().text("Cartera de loli-coins").icon(R.drawable.ic_coin) .setOnClickAction { Economy.showWallet(requireActivity(), true) { showAd() } } .build() ) extraCard.addItem( MaterialAboutActionItem.Builder().text("Top videos vistos").icon(R.drawable.ic_podium) .setOnClickAction { openTop() }.build() ) extraCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_web), "Politica de privacidad", true, "https://ukiku.app/policy.html".toUri() ) ) extraCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_github), "Proyecto en github", true, "https://github.com/jordyamc/UKIKU".toUri() ) ) extraCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_discord), "Discord", false, "https://discord.gg/6hzpua6".toUri() ) ) extraCard.addItem( ConvenienceBuilder.createWebsiteActionItem( requireContext(), getDrawable(requireContext(), R.drawable.ic_beta), "Grupo Beta", false, "https://t.me/ukiku_group".toUri() ) ) extraCard.addItem( MaterialAboutActionItem.Builder().text("Easter egg").icon(R.drawable.ic_egg) .setOnClickAction { EAUnlockActivity.start(requireContext()) }.build() ) return MaterialAboutList.Builder().apply { addCard(infoCard.build()) addCard(authorCard.build()) donateCard.build().let { if (it.items.isNotEmpty()) addCard(it) } addCard(extraCard.build()) }.build() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) interstitial = getFAdLoaderInterstitial(requireActivity()) rewardedAd.load() interstitial.load() setupUpdateCount() } fun openChangelog(){ if (DesignUtils.isFlat) ChangelogActivityMaterial.open(requireContext()) else ChangelogActivity.open(requireContext()) } fun openDiagnostic(){ if (DesignUtils.isFlat) DiagnosticMaterial.open(requireContext()) else Diagnostic.open(requireContext()) } fun openTop() { if (DesignUtils.isFlat) TopActivityMaterial.open(requireContext()) else TopActivity.open(requireContext()) } } ================================================ FILE: app/src/main/java/knf/kuma/BottomFragment.kt ================================================ package knf.kuma import android.app.Activity import android.content.Intent import androidx.fragment.app.Fragment import knf.kuma.download.FileAccessHelper import xdroid.toaster.Toaster abstract class BottomFragment : Fragment() { abstract fun onReselect() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == FileAccessHelper.SD_REQUEST && resultCode == Activity.RESULT_OK) { val validation = FileAccessHelper.isUriValid(data?.data) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") FileAccessHelper.openTreeChooser(this) } } } } ================================================ FILE: app/src/main/java/knf/kuma/Diagnostic.kt ================================================ package knf.kuma import android.content.Context import android.content.Intent import android.os.Bundle import android.text.format.Formatter import android.text.method.ScrollingMovementMethod import android.view.View import android.widget.TextView import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.floatingactionbutton.FloatingActionButton import fr.bmartel.speedtest.SpeedTestReport import fr.bmartel.speedtest.SpeedTestSocket import fr.bmartel.speedtest.inter.ISpeedTestListener import fr.bmartel.speedtest.model.SpeedTestError import knf.kuma.ads.SubscriptionReceiver import knf.kuma.backup.Backups import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.BypassUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.jsoupCookies import knf.kuma.commons.noCrash import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.custom.StateView import knf.kuma.database.CacheDB import knf.kuma.databinding.LayoutDiagnosticBinding import knf.kuma.directory.DirectoryService import knf.kuma.directory.DirectoryUpdateService import knf.kuma.uagen.randomUA import knf.tools.bypass.startBypass import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick import org.json.JSONObject import org.jsoup.HttpStatusException import org.jsoup.Jsoup import java.math.BigDecimal import java.math.RoundingMode import java.net.ConnectException import java.net.URL import java.net.UnknownHostException class Diagnostic : GenericActivity() { private val binding by lazy { LayoutDiagnosticBinding.inflate(layoutInflater) } private val networkStatus by lazy { NetworkStatus() } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.title = "Diagnóstico" supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.toolbar.setNavigationOnClickListener { finish() } startTests() } private fun startTests() { runNetworkTests() //runInternetTest() runDirectoryTest() runMemoryTest() runBackupTest() } private suspend fun runMainTest() { val startTime = System.currentTimeMillis() val responseCode = try { val response = Jsoup.connect(BypassUtil.testLink).timeout(0).execute() response.body() response.statusCode() } catch (e: HttpStatusException) { e.statusCode } catch (_: UnknownHostException) { 3000 } catch (_: ConnectException) { 4000 } catch (_: Exception) { 5000 } networkStatus.mainResult = responseCode val loadingTime = System.currentTimeMillis() - startTime withContext(Dispatchers.Main) { binding.codeState.load( responseCode.toString(), when (responseCode) { 200 -> StateView.STATE_OK 503 -> StateView.STATE_WARNING else -> StateView.STATE_ERROR } ) binding.timeoutState.load( "$loadingTime ms", when { responseCode < 1000 && loadingTime < 1000 -> StateView.STATE_OK responseCode < 1000 && loadingTime < 2000 -> StateView.STATE_WARNING else -> StateView.STATE_ERROR } ) binding.generalState.load(when { responseCode == 200 && loadingTime < 1000 -> "Correcto" responseCode == 502 -> "Animeflv caido" responseCode == 503 -> "Cloudflare activado" responseCode == 403 -> "Bloqueado por animeflv" responseCode in listOf(3000, 4000) -> "Error de conexión a Animeflv" responseCode == 5000 -> "Error al intentar conectar" loadingTime > 1000 -> "Página lenta" else -> "Desconocido" }, when { responseCode == 200 && loadingTime < 1000 -> StateView.STATE_OK.also { binding.info.visibility = View.GONE } responseCode in listOf(503, 403) || loadingTime > 1000 -> StateView.STATE_WARNING.also { binding.info.visibility = View.VISIBLE } responseCode == 502 -> StateView.STATE_ERROR.also { binding.info.visibility = View.VISIBLE } else -> StateView.STATE_ERROR.also { binding.info.visibility = View.GONE } }) binding.info.setOnClickListener { when { networkStatus.mainResult == 502 -> show502Info() networkStatus.mainResult == 503 -> show503Info() networkStatus.mainResult == 403 -> show403Info() loadingTime > 1000 -> showTimeoutInfo() } } } networkStatus.isMainTestExecuted = true } private suspend fun runBypassTest() { try { Jsoup.connect(BypassUtil.testLink).followRedirects(true).timeout(0).execute() binding.bypassState.load("No se necesita") withContext(Dispatchers.Main) { binding.bypassRecreate.visibility = View.GONE } } catch (e: HttpStatusException) { withContext(Dispatchers.Main) { binding.bypassRecreate.apply { visibility = View.VISIBLE onClick { startBypass( 5546, BypassUtil.createRequest() ) } } } try { jsoupCookies(BypassUtil.testLink).timeout(0).get() binding.bypassState.load("Valido", StateView.STATE_OK) if (networkStatus.isMainTestExecuted && networkStatus.mainResult in listOf( 403, 503 ) ) { withContext(Dispatchers.Main) { binding.codeState.load("200", StateView.STATE_OK) binding.generalState.load("Bypass activo", StateView.STATE_OK) binding.info.isVisible = false } } } catch (e: HttpStatusException) { when (e.statusCode) { 502 -> binding.bypassState.load("Animeflv caido", StateView.STATE_ERROR) 503 -> binding.bypassState.load("Caducado", StateView.STATE_WARNING) else -> binding.bypassState.load( "Error en página: HTTP ${e.statusCode}", StateView.STATE_ERROR ) } } loadBypassInfo() } catch (e: Exception) { e.printStackTrace() binding.bypassState.load("Error en página: ${e.message}", StateView.STATE_ERROR) } networkStatus.isBypassTestExecuted = true } private fun runNetworkTests() { lifecycleScope.launch(Dispatchers.IO) { runMainTest() runBypassTest() } } private fun loadBypassInfo() { doAsync { val json = JSONObject(URL("https://ipinfo.io/json").readText()) val region = json.getString("region") val country = json.getString("country") if (country == "PE") { binding.countryState.load("$region - VPN necesario", StateView.STATE_ERROR) } else { binding.countryState.load(region, StateView.STATE_OK) } } binding.clearanceState.apply { val data = BypassUtil.getClearance(this@Diagnostic) if (data.isNotEmpty()) load(data) } binding.cfduidState.apply { val data = BypassUtil.getCFDuid(this@Diagnostic) if (data.isNotEmpty()) load(data) } binding.userAgentState.apply { load(BypassUtil.userAgent) } } private fun runInternetTest() { doAsync { SpeedTestSocket().apply { addSpeedTestListener(object : ISpeedTestListener { override fun onCompletion(report: SpeedTestReport?) { report?.let { binding.downState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onProgress(percent: Float, report: SpeedTestReport?) { report?.let { binding.downState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onError(speedTestError: SpeedTestError?, errorMessage: String?) { binding.downState.load("Error: ${errorMessage ?: ""}", StateView.STATE_ERROR) } }) startDownload("http://1.testdebit.info/10M.iso") } } doAsync { SpeedTestSocket().apply { addSpeedTestListener(object : ISpeedTestListener { override fun onCompletion(report: SpeedTestReport?) { report?.let { binding.upState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onProgress(percent: Float, report: SpeedTestReport?) { report?.let { binding.upState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onError(speedTestError: SpeedTestError?, errorMessage: String?) { binding.upState.load("Error: ${errorMessage ?: ""}", StateView.STATE_ERROR) } }) startUpload("http://ipv4.ikoula.testdebit.info/", 5000000) } } } private fun formatBigDecimal(bigDecimal: BigDecimal): String { var decimal = bigDecimal.movePointLeft(3) val unit = when { decimal >= BigDecimal.valueOf(1000000) -> { decimal = decimal.movePointLeft(6) "Gb/s" } decimal >= BigDecimal.valueOf(1000) -> { decimal = decimal.movePointLeft(3) "Mb/s" } else -> "Kb/s" } return "${decimal.setScale(1, RoundingMode.HALF_UP)}$unit~" } private fun runDirectoryTest() { binding.dirState.load( when { PrefsUtil.isDirectoryFinished && !DirectoryUpdateService.isRunning -> "Completo" PrefsUtil.isDirectoryFinished && DirectoryUpdateService.isRunning -> "Actualizando" !PrefsUtil.isDirectoryFinished && DirectoryService.isRunning -> "Creando" else -> "Incompleto" } ) CacheDB.INSTANCE.animeDAO().countLive.observe(this) { binding.dirTotalState.load(it.toString()) } } private fun runMemoryTest() { val dirs = getExternalFilesDirs(null) noCrash { binding.internalState.load(getAvailable(dirs[0].freeSpace)) } noCrash { if (dirs.size > 1) binding.externalState.load(getAvailable(dirs[1].freeSpace)) } } private fun getAvailable(size: Long): String { return Formatter.formatFileSize(this, size) } private fun runBackupTest() { binding.uuid.text = FirestoreManager.uid ?: "Solo firestore" GlobalScope.launch(Dispatchers.IO) { if (PrefsUtil.isSubscriptionEnabled) { val status = SubscriptionReceiver.checkStatus( PrefsUtil.subscriptionToken ?: "" ) if (status.isActive) { if (status.isActive) binding.subscriptionState.load("Activa") else binding.subscriptionState.load("Activa pero no renovada") } else binding.subscriptionState.load("Cancelada o inexistente") } else binding.subscriptionState.load("No suscrito") } binding.backupState.load( when (Backups.type) { Backups.Type.DROPBOX -> "Dropbox" Backups.Type.FIRESTORE -> "Firestore" Backups.Type.LOCAL -> "Local" else -> "Sin respaldos" } ) if (Backups.type != Backups.Type.NONE) binding.lastBackupState.load(PrefsUtil.lastBackup) } private fun show502Info() { MaterialDialog(this).safeShow { title(text = "HTTP 502") message(text = "Animeflv esta caido por el momento, revisa de nuevo en unas horas") } } private fun show503Info() { MaterialDialog(this).safeShow { title(text = "HTTP 503") message(text = "Animeflv tiene el cloudflare activado, la app crea un bypass para funcionar normalmente") } } private fun show403Info() { MaterialDialog(this).safeShow { title(text = "HTTP 403") message(text = "Tu proveedor de internet bloquea la conexión con Animeflv, reinicia tu modem!") } } private fun showTimeoutInfo() { MaterialDialog(this).safeShow { title(text = "Timeout") message(text = "La página de Animeflv carga muy lento, modifica la espera de conexión desde configuración") } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 5546) { data?.let { PrefsUtil.useDefaultUserAgent = false PrefsUtil.userAgent = it.getStringExtra("user_agent") ?: randomUA() BypassUtil.saveCookies(this, it.getStringExtra("cookies") ?: "null") } runNetworkTests() } } companion object { fun open(context: Context) { context.startActivity(Intent(context, Diagnostic::class.java)) } } private data class NetworkStatus( var isMainTestExecuted: Boolean = false, var isBypassTestExecuted: Boolean = false, var mainResult: Int = -1, ) class FullBypass : GenericActivity() { private val overlay: View by lazy { find(R.id.overlay) as View } private val logText: TextView by lazy { find(R.id.logText) as TextView } private val fab: FloatingActionButton by lazy { find(R.id.fab) as FloatingActionButton } private var isOpened = false private var isFinishPending = false private val builder = StringBuilder("Initializing log...\n") override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) try { setContentView(R.layout.activity_webview) } catch (e: Exception) { setContentView(R.layout.activity_webview_nwv) } logText.movementMethod = ScrollingMovementMethod() fab.setOnClickListener { if (isOpened && isFinishPending) finish() else if (isOpened) { isOpened = false overlay.visibility = View.GONE logText.visibility = View.GONE fab.setImageResource(R.drawable.ic_terminal) } else { isOpened = true overlay.visibility = View.VISIBLE logText.visibility = View.VISIBLE fab.setImageResource(R.drawable.ic_close) } } logText("On Create check") checkBypass() } override fun forceCreation(): Boolean = true //override fun getSnackbarAnchor(): View? = find(R.id.coordinator) override fun onBypassUpdated() { if (isOpened) isFinishPending = true else finish() } override fun logText(text: String) { super.logText(text) builder.apply { append(text) append("\n") } doOnUI { logText.text = builder.toString() } } } } ================================================ FILE: app/src/main/java/knf/kuma/DiagnosticMaterial.kt ================================================ package knf.kuma import android.content.Context import android.content.Intent import android.os.Bundle import android.text.format.Formatter import android.text.method.ScrollingMovementMethod import android.view.View import android.widget.TextView import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.floatingactionbutton.FloatingActionButton import fr.bmartel.speedtest.SpeedTestReport import fr.bmartel.speedtest.SpeedTestSocket import fr.bmartel.speedtest.inter.ISpeedTestListener import fr.bmartel.speedtest.model.SpeedTestError import knf.kuma.ads.SubscriptionReceiver import knf.kuma.backup.Backups import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.BypassUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.jsoupCookies import knf.kuma.commons.noCrash import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.custom.StateView import knf.kuma.custom.StateViewMaterial import knf.kuma.database.CacheDB import knf.kuma.databinding.LayoutDiagnosticMaterialBinding import knf.kuma.directory.DirectoryService import knf.kuma.directory.DirectoryUpdateService import knf.kuma.uagen.randomUA import knf.tools.bypass.startBypass import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick import org.json.JSONObject import org.jsoup.HttpStatusException import org.jsoup.Jsoup import java.math.BigDecimal import java.math.RoundingMode import java.net.ConnectException import java.net.URL import java.net.UnknownHostException class DiagnosticMaterial : GenericActivity() { private val binding by lazy { LayoutDiagnosticMaterialBinding.inflate(layoutInflater) } private val networkStatus by lazy { NetworkStatus() } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.title = "Diagnóstico" supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.toolbar.setNavigationOnClickListener { finish() } startTests() } private fun startTests() { runNetworkTests() //runInternetTest() runDirectoryTest() runMemoryTest() runBackupTest() } private suspend fun runMainTest() { val startTime = System.currentTimeMillis() val responseCode = try { val response = Jsoup.connect(BypassUtil.testLink).timeout(0).execute() response.body() response.statusCode() } catch (e: HttpStatusException) { e.statusCode } catch (_: UnknownHostException) { 404 } catch (_: ConnectException) { 4000 } catch (_: Throwable) { 5000 } networkStatus.mainResult = responseCode val loadingTime = System.currentTimeMillis() - startTime withContext(Dispatchers.Main) { binding.codeState.load( responseCode.toString(), when (responseCode) { 200 -> StateView.STATE_OK 503 -> StateView.STATE_WARNING else -> StateView.STATE_ERROR } ) binding.timeoutState.load( "$loadingTime ms", when { responseCode < 1000 && loadingTime < 1000 -> StateView.STATE_OK responseCode < 1000 && loadingTime < 2000 -> StateView.STATE_WARNING else -> StateView.STATE_ERROR } ) binding.generalState.load(when { responseCode == 200 && loadingTime < 1000 -> "Correcto" responseCode == 502 -> "Animeflv caido" responseCode == 503 -> "Cloudflare activado" responseCode == 403 -> "Bloqueado por animeflv" responseCode in listOf(3000, 4000) -> "Error de conexión a Animeflv" responseCode == 5000 -> "Error al intentar conectar" loadingTime > 1000 -> "Página lenta" else -> "Desconocido" }, when { responseCode == 200 && loadingTime < 1000 -> StateView.STATE_OK.also { binding.info.visibility = View.GONE } responseCode in listOf(503, 403) || loadingTime > 1000 -> StateView.STATE_WARNING.also { binding.info.visibility = View.VISIBLE } responseCode == 502 -> StateView.STATE_ERROR.also { binding.info.visibility = View.VISIBLE } else -> StateView.STATE_ERROR.also { binding.info.visibility = View.GONE } }) binding.info.setOnClickListener { when { networkStatus.mainResult == 502 -> show502Info() networkStatus.mainResult == 503 -> show503Info() networkStatus.mainResult == 403 -> show403Info() loadingTime > 1000 -> showTimeoutInfo() } } } networkStatus.isMainTestExecuted = true } private suspend fun runBypassTest() { try { Jsoup.connect(BypassUtil.testLink).followRedirects(true).timeout(0).execute() binding.bypassState.load("No se necesita") withContext(Dispatchers.Main) { binding.bypassRecreate.visibility = View.GONE } } catch (e: HttpStatusException) { withContext(Dispatchers.Main) { binding.bypassRecreate.apply { visibility = View.VISIBLE onClick { startBypass( 5546, BypassUtil.createRequest() ) } } } try { jsoupCookies(BypassUtil.testLink).timeout(0).get() binding.bypassState.load("Valido", StateView.STATE_OK) if (networkStatus.isMainTestExecuted && networkStatus.mainResult in listOf( 403, 503 ) ) { withContext(Dispatchers.Main) { binding.codeState.load("200", StateView.STATE_OK) binding.generalState.load("Bypass activo", StateView.STATE_OK) binding.info.isVisible = false } } } catch (e: HttpStatusException) { when (e.statusCode) { 502 -> binding.bypassState.load("Animeflv caido", StateView.STATE_ERROR) 503 -> binding.bypassState.load("Caducado", StateView.STATE_WARNING) else -> binding.bypassState.load( "Error en página: HTTP ${e.statusCode}", StateView.STATE_ERROR ) } } loadBypassInfo() } catch (e: Throwable) { e.printStackTrace() binding.bypassState.load("Error en página: ${e.message}", StateView.STATE_ERROR) } networkStatus.isBypassTestExecuted = true } private fun runNetworkTests() { lifecycleScope.launch(Dispatchers.IO) { runMainTest() runBypassTest() } } private fun loadBypassInfo() { doAsync { val json = JSONObject(URL("https://ipinfo.io/json").readText()) val region = json.getString("region") val country = json.getString("country") if (country == "PE") { binding.countryState.load("$region - VPN necesario", StateView.STATE_ERROR) } else { binding.countryState.load(region, StateView.STATE_OK) } } binding.clearanceState.apply { val data = BypassUtil.getClearance(this@DiagnosticMaterial) if (data.isNotEmpty()) load(data) } binding.cfduidState.apply { val data = BypassUtil.getCFDuid(this@DiagnosticMaterial) if (data.isNotEmpty()) load(data) } binding.userAgentState.apply { load(BypassUtil.userAgent) } } private fun runInternetTest() { doAsync { SpeedTestSocket().apply { addSpeedTestListener(object : ISpeedTestListener { override fun onCompletion(report: SpeedTestReport?) { report?.let { binding.downState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onProgress(percent: Float, report: SpeedTestReport?) { report?.let { binding.downState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onError(speedTestError: SpeedTestError?, errorMessage: String?) { binding.downState.load("Error: ${errorMessage ?: ""}", StateViewMaterial.STATE_ERROR) } }) startDownload("https://speed.hetzner.de/100MB.bin") } } doAsync { SpeedTestSocket().apply { addSpeedTestListener(object : ISpeedTestListener { override fun onCompletion(report: SpeedTestReport?) { report?.let { binding.upState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onProgress(percent: Float, report: SpeedTestReport?) { report?.let { binding.upState.load(formatBigDecimal(it.transferRateOctet)) } } override fun onError(speedTestError: SpeedTestError?, errorMessage: String?) { binding.upState.load("Error: ${errorMessage ?: ""}", StateViewMaterial.STATE_ERROR) } }) startUpload("http://bouygues.testdebit.info/ul/", 5000000) } } } private fun formatBigDecimal(bigDecimal: BigDecimal): String { var decimal = bigDecimal.movePointLeft(3) val unit = when { decimal >= BigDecimal.valueOf(1000000) -> { decimal = decimal.movePointLeft(6) "Gb/s" } decimal >= BigDecimal.valueOf(1000) -> { decimal = decimal.movePointLeft(3) "Mb/s" } else -> "Kb/s" } return "${decimal.setScale(1, RoundingMode.HALF_UP)}$unit~" } private fun runDirectoryTest() { binding.dirState.load(when { PrefsUtil.isDirectoryFinished && !DirectoryUpdateService.isRunning -> "Completo" PrefsUtil.isDirectoryFinished && DirectoryUpdateService.isRunning -> "Actualizando" !PrefsUtil.isDirectoryFinished && DirectoryService.isRunning -> "Creando" else -> "Incompleto" }) CacheDB.INSTANCE.animeDAO().countLive.observe(this, Observer { binding.dirTotalState.load(it.toString()) }) } private fun runMemoryTest() { val dirs = getExternalFilesDirs(null).toList().filterNotNull() noCrash { binding.internalState.load(getAvailable(dirs[0].freeSpace)) } noCrash { if (dirs.size > 1) binding.externalState.load(getAvailable(dirs[1].freeSpace)) } } private fun getAvailable(size: Long): String { return Formatter.formatFileSize(this, size) } private fun runBackupTest() { binding.uuid.text = FirestoreManager.uid ?: "Solo firestore" GlobalScope.launch(Dispatchers.IO) { if (PrefsUtil.isSubscriptionEnabled) { val status = SubscriptionReceiver.checkStatus(PrefsUtil.subscriptionToken ?: "") if (status.isActive) { if (status.isActive) binding.subscriptionState.load("Activa") else binding.subscriptionState.load("Activa pero no renovada") } else binding.subscriptionState.load("Cancelada o inexistente") } else binding.subscriptionState.load("No suscrito") } binding.backupState.load(when (Backups.type) { Backups.Type.DROPBOX -> "Dropbox" Backups.Type.FIRESTORE -> "Firestore" Backups.Type.LOCAL -> "Local" else -> "Sin respaldos" }) if (Backups.type != Backups.Type.NONE) binding.lastBackupState.load(PrefsUtil.lastBackup) } private fun show502Info() { MaterialDialog(this).safeShow { title(text = "HTTP 502") message(text = "Animeflv esta caido por el momento, revisa de nuevo en unas horas") } } private fun show503Info() { MaterialDialog(this).safeShow { title(text = "HTTP 503") message(text = "Animeflv tiene el cloudflare activado, la app crea un bypass para funcionar normalmente") } } private fun show403Info() { MaterialDialog(this).safeShow { title(text = "HTTP 403") message(text = "Tu proveedor de internet bloquea la conexión con Animeflv, reinicia tu modem!") } } private fun showTimeoutInfo() { MaterialDialog(this).safeShow { title(text = "Timeout") message(text = "La página de Animeflv carga muy lento, modifica la espera de conexión desde configuración") } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 5546) { data?.let { PrefsUtil.useDefaultUserAgent = false PrefsUtil.userAgent = it.getStringExtra("user_agent") ?: randomUA() BypassUtil.saveCookies(this, it.getStringExtra("cookies") ?: "null") } runNetworkTests() } } companion object { fun open(context: Context) { context.startActivity(Intent(context, DiagnosticMaterial::class.java)) } } private data class NetworkStatus( var isMainTestExecuted: Boolean = false, var isBypassTestExecuted: Boolean = false, var mainResult: Int = -1, ) class FullBypass : GenericActivity() { private val overlay: View by lazy { find(R.id.overlay) as View } private val logText: TextView by lazy { find(R.id.logText) as TextView } private val fab: FloatingActionButton by lazy { find(R.id.fab) as FloatingActionButton } private var isOpened = false private var isFinishPending = false private val builder = StringBuilder("Initializing log...\n") override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) try { setContentView(R.layout.activity_webview) } catch (e: Exception) { setContentView(R.layout.activity_webview_nwv) } logText.movementMethod = ScrollingMovementMethod() fab.setOnClickListener { if (isOpened && isFinishPending) finish() else if (isOpened) { isOpened = false overlay.visibility = View.GONE logText.visibility = View.GONE fab.setImageResource(R.drawable.ic_terminal) } else { isOpened = true overlay.visibility = View.VISIBLE logText.visibility = View.VISIBLE fab.setImageResource(R.drawable.ic_close) } } logText("On Create check") checkBypass() } override fun forceCreation(): Boolean = true //override fun getSnackbarAnchor(): View? = find(R.id.coordinator) override fun onBypassUpdated() { if (isOpened) isFinishPending = true else finish() } override fun logText(text: String) { super.logText(text) builder.apply { append(text) append("\n") } lifecycleScope.launch(Dispatchers.Main) { logText.text = builder.toString() } } } } ================================================ FILE: app/src/main/java/knf/kuma/Main.kt ================================================ package knf.kuma import androidx.activity.addCallback import android.Manifest import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.graphics.Typeface import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.provider.Settings import android.text.InputType import android.util.Log import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.TextView import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationView import knf.kuma.achievements.AchievementActivity import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdsUtils import knf.kuma.backup.BackUpActivity import knf.kuma.backup.Backups import knf.kuma.backup.MigrationActivity import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.backup.firestore.syncData import knf.kuma.commons.BypassUtil import knf.kuma.commons.CastUtil import knf.kuma.commons.DesignUtils import knf.kuma.commons.EAHelper import knf.kuma.commons.EAMapActivity import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.changeToolbarFont import knf.kuma.commons.isFullMode import knf.kuma.commons.jsoupCookiesDir import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashLet import knf.kuma.commons.safeShow import knf.kuma.commons.stringLiveData import knf.kuma.commons.toast import knf.kuma.commons.verifiyFF import knf.kuma.custom.ConnectionState import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.directory.DirManager import knf.kuma.directory.DirectoryFragment import knf.kuma.directory.DirectoryService import knf.kuma.download.FileAccessHelper import knf.kuma.emision.EmissionActivity import knf.kuma.explorer.ExplorerActivity import knf.kuma.faq.FaqActivity import knf.kuma.favorite.FavoriteFragment import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.jobscheduler.RecentsWork import knf.kuma.jobscheduler.UpdateWork import knf.kuma.news.NewsActivity import knf.kuma.pojos.migrateSeen import knf.kuma.preferences.BottomPreferencesFragment import knf.kuma.preferences.ConfigurationFragment import knf.kuma.queue.QueueActivity import knf.kuma.random.RandomActivity import knf.kuma.recents.RecentFragment import knf.kuma.recents.RecentsNotReceiver import knf.kuma.recommended.RecommendActivity import knf.kuma.record.RecordActivity import knf.kuma.search.FiltersSuggestion import knf.kuma.search.SearchFragment import knf.kuma.seeing.SeeingActivity import knf.kuma.uagen.randomUA import knf.kuma.updater.UpdateActivity import knf.kuma.updater.UpdateChecker import knh.kuma.commons.cloudflarebypass.CfCallback import knh.kuma.commons.cloudflarebypass.Cloudflare import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.cryse.widget.persistentsearch.PersistentSearchView import org.cryse.widget.persistentsearch.SearchItem import org.jetbrains.anko.hintTextColor import org.jetbrains.anko.sdk27.coroutines.onClick import org.jetbrains.anko.textColor import q.rorbin.badgeview.Badge import q.rorbin.badgeview.QBadgeView import xdroid.toaster.Toaster import java.net.HttpCookie import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import androidx.core.net.toUri class Main : GenericActivity(), NavigationView.OnNavigationItemSelectedListener, NavigationBarView.OnItemSelectedListener, NavigationBarView.OnItemReselectedListener, UpdateChecker.CheckListener, BypassUtil.BypassListener, ConfigurationFragment.UAChangeListener { private val toolbar by bind(R.id.toolbar) private val searchView by bind(R.id.searchview) private val drawer by bind(R.id.drawer_layout) private val navigationView by bind(R.id.nav_view) private val coordinator by bind(R.id.coordinator) private val connectionState by bind(R.id.connectionState) private val bottomNavigationView by bind(R.id.bottomNavigation) internal var selectedFragment: BottomFragment? = null private var searchFragment: SearchFragment? = null private lateinit var badgeEmission: TextView private lateinit var badgeSeeing: TextView private lateinit var badgeQueue: TextView private var badgeView: Badge? = null private var readyToFinish = false private var isFirst = true override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getThemeNA()) super.onCreate(savedInstanceState) if (getString(R.string.app_name) != "UKIKU") { Toaster.toast("Te dije que no lo cambiaras") finish() return } try { setContentView(R.layout.activity_main_drawer) } catch (e: Exception) { setContentView(R.layout.activity_main_drawer_nwv) } //setDefaults() setSupportActionBar(toolbar) val toggle = ActionBarDrawerToggle( this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) drawer.addDrawerListener(toggle) toggle.syncState() toolbar.changeToolbarFont(R.font.audiowide) setNavigationButtons() navigationView.setNavigationItemSelectedListener(this) bottomNavigationView.setOnItemSelectedListener(this) bottomNavigationView.setOnItemReselectedListener(this) setSearch() if (savedInstanceState == null) { checkServices() startChange() } else returnSelectFragment() //checkBypass() migrateSeen() FirestoreManager.start() onBackPressedDispatcher.addCallback(this) { when { drawer.isDrawerOpen(GravityCompat.START) -> drawer.closeDrawer(GravityCompat.START) searchView.isSearching -> closeSearch() readyToFinish -> finish() else -> { readyToFinish = true Toaster.toast("Presione de nuevo para salir") Handler().postDelayed({ readyToFinish = false }, 2000) } } } DesignUtils.listenDesignChange(this) } private fun checkServices() { lifecycleScope.launch(Dispatchers.IO) { BypassUtil.clearCookiesIfNeeded() checkPermissions() checkDirectoryState() UpdateWork.schedule() RecentsWork.schedule(this@Main) DirUpdateWork.schedule(this@Main) RecentsNotReceiver.removeAll(this@Main) EAHelper.clear1() verifiyFF() } } private suspend fun checkDirectoryState() { DirManager.checkPreDir() if (PrefsUtil.useDefaultUserAgent && Network.isConnected) { val isBrowserOk = noCrashLet(false) { jsoupCookiesDir("https://www3.animeflv.net/browse?order=added&page=5", BypassUtil.isCloudflareActive()).execute() true } if (!isBrowserOk) { val randomUA = randomUA() PrefsUtil.userAgentDir = randomUA suspendCoroutine { lifecycleScope.launch(Dispatchers.Main) { noCrash { Cloudflare(this@Main, "https://www3.animeflv.net/browse?order=added&page=5", PrefsUtil.userAgentDir).apply { setCfCallback(object : CfCallback { override fun onSuccess(cookieList: MutableList?, hasNewUrl: Boolean, newUrl: String?) { PrefsUtil.dirCookies = cookieList ?: emptyList() noCrash { it.resume(true) } } override fun onFail(code: Int, msg: String?) { Log.e("Dir cookies", "On error, code $code, msg: $msg") noCrash { it.resume(false) } } }) }.getCookies() } } } } } DirectoryService.run(this) } @SuppressLint("SetTextI18n") private fun setNavigationButtons() { lifecycleScope.launch(Dispatchers.Main) { badgeEmission = navigationView.menu.findItem(R.id.drawer_emision).actionView as TextView badgeSeeing = navigationView.menu.findItem(R.id.drawer_seeing).actionView as TextView badgeQueue = navigationView.menu.findItem(R.id.drawer_queue).actionView as TextView navigationView.getHeaderView(0).findViewById(R.id.img).setBackgroundResource(EAHelper.getThemeImg()) val header = navigationView.getHeaderView(0).findViewById(R.id.img) ViewCompat.setOnApplyWindowInsetsListener(header) { v, insets -> v.apply { if (insets.getInsets(WindowInsetsCompat.Type.systemBars()).top > 0) setPadding(paddingLeft, insets.getInsets(WindowInsetsCompat.Type.systemBars()).top, paddingRight, paddingBottom) } insets } val actionShare = navigationView.getHeaderView(0).findViewById(R.id.action_share) val actionInfo = navigationView.getHeaderView(0).findViewById(R.id.action_info) val actionTrophy = navigationView.getHeaderView(0).findViewById(R.id.action_trophy) val actionLogin = navigationView.getHeaderView(0).findViewById(R.id.action_login) val actionMigrate = navigationView.getHeaderView(0).findViewById(R.id.action_migrate) val actionMap = navigationView.getHeaderView(0).findViewById(R.id.action_map) actionShare.onClick { startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, "Hola,\n" + "\n" + "UKIKU es una aplicación rápida y simple que uso para ver mis animes favoritos.\n" + "\n" + "Descárgala gratis desde https://ukiku.app/") }, "Compartir UKIKU")) } actionInfo.onClick { AppInfoActivity.open(this@Main) } actionTrophy.onClick { AchievementActivity.open(this@Main) } actionLogin.onClick { BackUpActivity.start(this@Main) } actionMigrate.onClick { MigrationActivity.start(this@Main) } actionMap.onClick { EAMapActivity.start(this@Main) } actionMigrate.visibility = if (Backups.isAnimeflvInstalled) View.VISIBLE else View.GONE actionMap.visibility = if (EAHelper.phase == 3) View.VISIBLE else View.GONE val backupLocation = navigationView.getHeaderView(0).findViewById(R.id.backupLocation) backupLocation.text = when (Backups.type) { Backups.Type.NONE -> "Sin respaldos" Backups.Type.DROPBOX -> "Dropbox" Backups.Type.FIRESTORE -> "Firestore" Backups.Type.LOCAL -> "Local" } subscribeBadges() } } private fun subscribeBadges() { val bottomNavigationMenuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView try { val v = bottomNavigationMenuView.getChildAt(1) if (badgeView == null) { badgeView = QBadgeView(this) .bindTarget(v) .setExactMode(true) .setShowShadow(false) .setGravityOffset(5f, 5f, true) .setBadgeBackgroundColor(ContextCompat.getColor(this, EAHelper.getThemeColorLight())) CacheDB.INSTANCE.favsDAO().countLive.observe(this) { integer -> if (badgeView != null && integer != null) if (PrefsUtil.showFavIndicator) badgeView?.badgeNumber = integer else badgeView?.hide(false) } PrefsUtil.getLiveShowFavIndicator().observe(this) { aBoolean -> if (badgeView != null) { if (aBoolean) lifecycleScope.launch { badgeView?.badgeNumber = withContext(Dispatchers.IO) { CacheDB.INSTANCE.favsDAO().count } } else badgeView?.hide(false) } } PreferenceManager.getDefaultSharedPreferences(this).stringLiveData("theme_color", "0") .observe(this) { (badgeView as? QBadgeView)?.badgeBackgroundColor = ContextCompat.getColor(this, EAHelper.getThemeColorLight(it)) badgeEmission.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor(it))) badgeSeeing.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor(it))) badgeQueue.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor(it))) navigationView.getHeaderView(0).findViewById(R.id.img).setBackgroundResource(EAHelper.getThemeImg(it)) } } badgeEmission.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) badgeEmission.setTypeface(null, Typeface.BOLD) badgeEmission.gravity = Gravity.CENTER_VERTICAL badgeSeeing.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) badgeSeeing.setTypeface(null, Typeface.BOLD) badgeSeeing.gravity = Gravity.CENTER_VERTICAL badgeQueue.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) badgeQueue.setTypeface(null, Typeface.BOLD) badgeQueue.gravity = Gravity.CENTER_VERTICAL PrefsUtil.getLiveEmissionBlackList().observe(this) { strings -> CacheDB.INSTANCE.animeDAO().getInEmission(strings).observe(this) { integer -> badgeEmission.text = integer.toString() badgeEmission.visibility = if (integer == 0) View.GONE else View.VISIBLE } } CacheDB.INSTANCE.seeingDAO().countWatchingLive.observe(this) { integer -> badgeSeeing.text = integer.toString() badgeSeeing.visibility = if (integer == 0) View.GONE else View.VISIBLE } CacheDB.INSTANCE.queueDAO().countLive.observe(this) { integer -> badgeQueue.text = integer.toString() badgeQueue.visibility = if (integer == 0) View.GONE else View.VISIBLE } } catch (e: Exception) { e.printStackTrace() } } private fun checkPermissions() { try { val permissions = mutableListOf() if (isFullMode) { if (Build.VERSION.SDK_INT in Build.VERSION_CODES.M..Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { permissions.add(Manifest.permission.POST_NOTIFICATIONS) } if (permissions.isNotEmpty()) { requestPermissions(permissions.toTypedArray(), 55498) } } catch (e: Exception) { e.printStackTrace() } } private fun showRationalPermission(denied: Boolean = false) { MaterialDialog(this).safeShow { title(text = "Permiso de escritura") message(text = "Esta aplicación necesita el permiso obligatoriamente para guardar cache y descargar los episodios") positiveButton(text = "Aceptar") { if (denied) { try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, "package:$packageName".toUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } finish() startActivity(intent) } catch (e: ActivityNotFoundException) { "Error al mostrar configuración".toast() } } else checkPermissions() } negativeButton(text = "Salir") { finish() } cancelable(false) } } override fun onNeedUpdate(o_code: String, n_code: String) { runOnUiThread { try { if (n_code.toInt() > AdsUtils.remoteConfigs.getLong("min_version").toInt()) { MaterialDialog(this@Main).safeShow { title(text = "Actualización") message(text = "Parece que la versión $n_code está disponible, ¿Quieres actualizar?") positiveButton(text = "si") { UpdateActivity.start(this@Main, true, n_code) } negativeButton(text = "despues") { checkBypass() } } } else { finish() UpdateActivity.start( this@Main, false, n_code ) } } catch (e: Exception) { e.printStackTrace() } } } override fun onUpdateNotRequired() { checkBypass() } private fun setSearch() { searchView.searchEditText.apply { inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS textColor = ContextCompat.getColor(this@Main, android.R.color.black) hintTextColor = ContextCompat.getColor(this@Main, android.R.color.darker_gray) } searchView.setSuggestionBuilder(FiltersSuggestion(this)) searchView.setSearchListener(object : PersistentSearchView.SearchListener { override fun onSearchCleared() { searchFragment?.setSearch("") } override fun onSearchTermChanged(term: String) { EAHelper.checkStart(term) AchievementManager.onSearch(term) searchFragment?.setSearch(term) } override fun onSearch(query: String) {} override fun onSearchEditOpened() { requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED } override fun onSearchEditClosed() { /*closeSearch();*/ } override fun onSearchEditBackPressed(): Boolean { closeSearch() return true } override fun onSearchExit() { closeSearch() } override fun onSuggestion(searchItem: SearchItem?): Boolean = true }) } private fun onStateDialog(message: String) { MaterialDialog(this).safeShow { message(text = message) positiveButton() } } override fun onCreateOptionsMenu(menu: Menu): Boolean { if (selectedFragment == null || selectedFragment is RecentFragment) { menuInflater.inflate(R.menu.main, menu) } else if (selectedFragment is FavoriteFragment) { menuInflater.inflate(R.menu.fav_menu, menu) when (PrefsUtil.favsOrder) { 0 -> menu.findItem(R.id.by_name).isChecked = true 1 -> menu.findItem(R.id.by_id).isChecked = true } if (!PrefsUtil.showFavSections()) menu.findItem(R.id.action_new_category).isVisible = false } else if (selectedFragment is DirectoryFragment) { menuInflater.inflate(R.menu.dir_menu, menu) when (PrefsUtil.dirOrder) { 0 -> menu.findItem(R.id.by_name_dir).isChecked = true 1 -> menu.findItem(R.id.by_votes).isChecked = true 2 -> menu.findItem(R.id.by_id_dir).isChecked = true 3 -> menu.findItem(R.id.by_added_dir).isChecked = true 4 -> menu.findItem(R.id.by_followers).isChecked = true } } else { menuInflater.inflate(R.menu.main, menu) } searchView.setStartPositionFromMenuItem(findViewById(R.id.action_search)) CastUtil.registerActivity(this, menu, R.id.castMenu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_search -> { searchView.openSearch() searchFragment = SearchFragment.get() setFragment(searchFragment as BottomFragment) } R.id.action_new_category -> if (selectedFragment is FavoriteFragment) (selectedFragment as FavoriteFragment).showNewCategory(null) R.id.by_name -> { PrefsUtil.favsOrder = 0 changeOrder() } R.id.by_name_dir -> { PrefsUtil.dirOrder = 0 changeOrder() } R.id.by_votes -> { PrefsUtil.dirOrder = 1 changeOrder() } R.id.by_id -> { PrefsUtil.favsOrder = 1 changeOrder() } R.id.by_id_dir -> { PrefsUtil.dirOrder = 2 changeOrder() } R.id.by_added_dir -> { PrefsUtil.dirOrder = 3 changeOrder() } R.id.by_followers -> { PrefsUtil.dirOrder = 4 changeOrder() } } return super.onOptionsItemSelected(item) } private fun changeOrder() { if (selectedFragment is FavoriteFragment) { (selectedFragment as FavoriteFragment).onChangeOrder() } else if (selectedFragment is DirectoryFragment) { (selectedFragment as DirectoryFragment).onChangeOrder() } invalidateOptionsMenu() } override fun onNavigationItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_bottom_recents -> setFragment(RecentFragment.get()) R.id.action_bottom_favorites -> setFragment(FavoriteFragment.get()) R.id.action_bottom_directory -> setFragment(DirectoryFragment.get()) R.id.action_bottom_settings -> setFragment(BottomPreferencesFragment.get()) R.id.drawer_explorer -> ExplorerActivity.open(this) R.id.drawer_emision -> EmissionActivity.open(this) R.id.drawer_queue -> QueueActivity.open(this) R.id.drawer_suggestions -> RecommendActivity.open(this) R.id.drawer_news -> NewsActivity.open(this) R.id.drawer_records -> RecordActivity.open(this) R.id.drawer_seeing -> SeeingActivity.open(this) R.id.drawer_random -> RandomActivity.open(this) R.id.drawer_faq -> FaqActivity.open(this) } closeSearchBar() closeDrawer() return true } private fun setFragment(fragment: BottomFragment) { lifecycleScope.launch(Dispatchers.Main) { try { if (fragment !is SearchFragment) selectedFragment = fragment val transaction = supportFragmentManager.beginTransaction() //transaction.setCustomAnimations(R.anim.fadein, R.anim.fadeout) transaction.replace(R.id.root, fragment) transaction.commit() invalidateOptionsMenu() } catch (e: Exception) { e.printStackTrace() } } } private fun closeDrawer() { drawer.closeDrawer(GravityCompat.START) } private fun closeSearch() { closeSearchBar() returnFragment() } private fun closeSearchBar() { searchView.closeSearch() } private fun returnFragment() { selectedFragment?.let { setFragment(it) } ?: let { bottomNavigationView.selectedItemId = R.id.action_bottom_recents setFragment(RecentFragment.get()) } requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } private fun returnSelectFragment() { if (selectedFragment != null) { when (selectedFragment) { is FavoriteFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_favorites is DirectoryFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_directory is BottomPreferencesFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_settings else -> bottomNavigationView.selectedItemId = R.id.action_bottom_recents } } requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } private fun startChange() { if (intent.dataString?.let { return@let if ("ukiku.app/search/" in it) { val query = it.substringAfter("ukiku.app/search/") selectedFragment = RecentFragment.get() searchView.openSearch() setFragment(SearchFragment[query]) searchView.setSearchString(query, false) true } else false } == true) return when (intent.getIntExtra("start_position", -1)) { 0 -> setFragment(RecentFragment.get()) 1 -> bottomNavigationView.selectedItemId = R.id.action_bottom_favorites 2 -> bottomNavigationView.selectedItemId = R.id.action_bottom_directory 3 -> bottomNavigationView.selectedItemId = R.id.action_bottom_settings 4 -> { selectedFragment = RecentFragment.get() searchView.openSearch() setFragment(SearchFragment[intent.getStringExtra("search_query") ?: ""]) searchView.setSearchString(intent.getStringExtra("search_query") ?: "", false) } else -> setFragment(RecentFragment.get()) } } private fun reselectFragment() { if (selectedFragment != null) { when (selectedFragment) { is FavoriteFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_recents is DirectoryFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_directory is BottomPreferencesFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_settings else -> bottomNavigationView.selectedItemId = R.id.action_bottom_recents } } requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } override fun onNavigationItemReselected(item: MenuItem) { if (selectedFragment != null && searchView.isSearching) { closeSearch() } else if (selectedFragment != null) { selectedFragment?.onReselect() } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) permissions.forEachIndexed { result, permission -> if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE && result != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) showRationalPermission() else showRationalPermission(true) } } } @SuppressLint("SetTextI18n") override fun onResume() { super.onResume() invalidateOptionsMenu() connectionState.setUp(this, ::onStateDialog) lifecycleScope.launch(Dispatchers.Main) { val backupLocation = navigationView.getHeaderView(0).findViewById(R.id.backupLocation) backupLocation.text = when (Backups.type) { Backups.Type.NONE -> "Sin respaldos" Backups.Type.DROPBOX -> "Dropbox" Backups.Type.FIRESTORE -> "Firestore" Backups.Type.LOCAL -> "Local" } } if (isFirst) { isFirst = false } } override fun onPause() { syncData { achievements() } super.onPause() } override fun onUAChange() { checkBypass() } override fun onAttachedToWindow() { super.onAttachedToWindow() //ChangelogActivity.check(this) UpdateChecker.check(this, this) } override fun onNeedRecreate() { reselectFragment() } override fun onDestroy() { if (!isChangingConfigurations) CastUtil.get().onDestroy() super.onDestroy() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) noCrash { if (requestCode == FileAccessHelper.SD_REQUEST && resultCode == RESULT_OK) { val validation = FileAccessHelper.isUriValid(data?.data) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") FileAccessHelper.openTreeChooser(this) } } } setNavigationButtons() } override fun onBypassUpdated() { onNeedRecreate() } override fun getSnackbarAnchor(): View { return coordinator } } ================================================ FILE: app/src/main/java/knf/kuma/MainMaterial.kt ================================================ package knf.kuma import androidx.activity.addCallback import android.Manifest import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.graphics.Typeface import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.provider.Settings import android.util.Log import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationView import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.gson.Gson import knf.kuma.achievements.AchievementActivityMaterial import knf.kuma.ads.AdsUtils import knf.kuma.backup.BackUpActivity import knf.kuma.backup.Backups import knf.kuma.backup.MigrationActivity import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.backup.firestore.syncData import knf.kuma.changelog.ChangelogActivityMaterial import knf.kuma.commons.BypassUtil import knf.kuma.commons.CastUtil import knf.kuma.commons.DesignUtils import knf.kuma.commons.EAHelper import knf.kuma.commons.EAMapActivity import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.changeToolbarFont import knf.kuma.commons.getSurfaceColor import knf.kuma.commons.isFullMode import knf.kuma.commons.noCrash import knf.kuma.commons.safeShow import knf.kuma.commons.stringLiveData import knf.kuma.commons.toast import knf.kuma.commons.verifiyFF import knf.kuma.custom.ConnectionState import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.directory.DirManager import knf.kuma.directory.DirectoryFragmentMaterial import knf.kuma.directory.DirectoryService import knf.kuma.download.FileAccessHelper import knf.kuma.emision.EmissionActivityMaterial import knf.kuma.explorer.ExplorerActivityMaterial import knf.kuma.faq.FaqActivityMaterial import knf.kuma.favorite.FavoriteFragmentMaterial import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.jobscheduler.RecentsWork import knf.kuma.jobscheduler.UpdateWork import knf.kuma.news.MaterialNewsActivity import knf.kuma.pojos.migrateSeen import knf.kuma.preferences.BottomPreferencesFragment import knf.kuma.preferences.BottomPreferencesMaterialFragment import knf.kuma.preferences.ConfigurationFragment import knf.kuma.queue.QueueActivityMaterial import knf.kuma.random.RandomActivityMaterial import knf.kuma.recents.RecentFragment import knf.kuma.recents.RecentModelsFragment import knf.kuma.recents.RecentsNotReceiver import knf.kuma.recommended.RecommendActivityMaterial import knf.kuma.record.RecordActivityMaterial import knf.kuma.search.SearchActivity import knf.kuma.seeing.SeeingActivityMaterial import knf.kuma.updater.UpdateActivity import knf.kuma.updater.UpdateChecker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.sdk27.coroutines.onClick import org.json.JSONObject import q.rorbin.badgeview.Badge import q.rorbin.badgeview.QBadgeView import xdroid.toaster.Toaster import java.io.File class MainMaterial : GenericActivity(), NavigationView.OnNavigationItemSelectedListener, NavigationBarView.OnItemSelectedListener, NavigationBarView.OnItemReselectedListener, UpdateChecker.CheckListener, BypassUtil.BypassListener, ConfigurationFragment.UAChangeListener { private val toolbar by bind(R.id.toolbar) private val drawer by bind(R.id.drawer_layout) private val navigationView by bind(R.id.nav_view) private val connectionState by bind(R.id.connectionState) private val root by bind(R.id.root) private val bottomNavigationView by bind(R.id.bottomNavigation) internal var selectedFragment: BottomFragment? = null private lateinit var badgeEmission: TextView private lateinit var badgeSeeing: TextView private lateinit var badgeQueue: TextView private var badgeView: Badge? = null private var readyToFinish = false private var isFirst = true override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getThemeNA()) super.onCreate(savedInstanceState) if (getString(R.string.app_name) != "UKIKU") { Toaster.toast("Te dije que no lo cambiaras") finish() return } try { setContentView(R.layout.activity_main_material) } catch (_: Exception) { setContentView(R.layout.activity_main_drawer_nwv) } //setDefaults() drawer.setStatusBarBackgroundColor(getSurfaceColor()) setSupportActionBar(toolbar) supportActionBar?.title = "Buscar animes" toolbar.setNavigationOnClickListener { drawer.openDrawer(GravityCompat.START) } toolbar.setOnClickListener { if (selectedFragment !is BottomPreferencesMaterialFragment) SearchActivity.open(this) } setNavigationButtons() navigationView.setNavigationItemSelectedListener(this) bottomNavigationView.setOnItemSelectedListener(this) bottomNavigationView.setOnItemReselectedListener(this) if (savedInstanceState == null) { checkServices() startChange() } else returnSelectFragment() //checkBypass() migrateSeen() FirestoreManager.start() onBackPressedDispatcher.addCallback(this) { if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START) } else { if (readyToFinish) { finish() } else { readyToFinish = true Toaster.toast("Presione de nuevo para salir") Handler().postDelayed({ readyToFinish = false }, 2000) } } } DesignUtils.listenDesignChange(this) //BypassUtil.doConnectionTests() //ThumbsDownloader.start(this) /*lifecycleScope.launch(Dispatchers.IO) { StapeServer(this@MainMaterial, "https://streamtape.com/v/lW9e90W7b0S7ylb/").videoServer }*/ } private fun checkServices() { lifecycleScope.launch(Dispatchers.IO) { BypassUtil.clearCookiesIfNeeded() checkPermissions() checkDirectoryState() UpdateWork.schedule() RecentsWork.schedule(this@MainMaterial) DirUpdateWork.schedule(this@MainMaterial) RecentsNotReceiver.removeAll(this@MainMaterial) EAHelper.clear1() verifiyFF() saveDir() } } private fun saveDir() { if (!BuildConfig.DEBUG) return val lists = CacheDB.INSTANCE.animeDAO().all.chunked(500) var number = 0 val json = JSONObject() lists.forEach { list -> val info = JSONObject().apply { put("idF", list.first().aid) put("idL", list.last().aid) } json.put(number.toString(), info) val file = File(getExternalFilesDir(null), "directory$number.json") if (!file.exists()) { file.createNewFile() file.writeText(Gson().toJson(list)) } number++ Log.e("Dir files", "Process chunk: $number") } val file = File(getExternalFilesDir(null), "directoryInfo.json") if (!file.exists()) { file.createNewFile() file.writeText(json.toString()) Log.e("Dir files", "Save finished") } } private fun checkDirectoryState() { DirManager.checkPreDir() DirectoryService.run(this@MainMaterial) } @SuppressLint("SetTextI18n") private fun setNavigationButtons() { lifecycleScope.launch(Dispatchers.Main) { badgeEmission = navigationView.menu.findItem(R.id.drawer_emision).actionView as TextView badgeSeeing = navigationView.menu.findItem(R.id.drawer_seeing).actionView as TextView badgeQueue = navigationView.menu.findItem(R.id.drawer_queue).actionView as TextView navigationView.getHeaderView(0).findViewById(R.id.img).setBackgroundResource(EAHelper.getThemeImg()) val header = navigationView.getHeaderView(0).findViewById(R.id.img) ViewCompat.setOnApplyWindowInsetsListener(header) { v, insets -> v.apply { if (insets.systemWindowInsetTop > 0) setPadding(paddingLeft, insets.systemWindowInsetTop, paddingRight, paddingBottom) } insets } val actionShare = navigationView.getHeaderView(0).findViewById(R.id.action_share) val actionInfo = navigationView.getHeaderView(0).findViewById(R.id.action_info) val actionTrophy = navigationView.getHeaderView(0).findViewById(R.id.action_trophy) val actionLogin = navigationView.getHeaderView(0).findViewById(R.id.action_login) val actionMigrate = navigationView.getHeaderView(0).findViewById(R.id.action_migrate) val actionMap = navigationView.getHeaderView(0).findViewById(R.id.action_map) actionShare.onClick { startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, "Hola,\n" + "\n" + "UKIKU es una aplicación rápida y simple que uso para ver mis animes favoritos.\n" + "\n" + "Descárgala gratis desde https://ukiku.app/") }, "Compartir UKIKU")) } actionInfo.onClick { AppInfoActivityMaterial.open(this@MainMaterial) } actionTrophy.onClick { AchievementActivityMaterial.open(this@MainMaterial) } actionLogin.onClick { BackUpActivity.start(this@MainMaterial) } actionMigrate.onClick { MigrationActivity.start(this@MainMaterial) } actionMap.onClick { EAMapActivity.start(this@MainMaterial) } actionMigrate.visibility = if (Backups.isAnimeflvInstalled) View.VISIBLE else View.GONE actionMap.visibility = if (EAHelper.phase == 3) View.VISIBLE else View.GONE val backupLocation = navigationView.getHeaderView(0).findViewById(R.id.backupLocation) backupLocation.text = when (Backups.type) { Backups.Type.NONE -> "Sin respaldos" Backups.Type.DROPBOX -> "Dropbox" Backups.Type.FIRESTORE -> "Firestore" Backups.Type.LOCAL -> "Local" } subscribeBadges() } } private fun subscribeBadges() { val bottomNavigationMenuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView try { val v = bottomNavigationMenuView.getChildAt(1) if (badgeView == null) { badgeView = QBadgeView(this) .bindTarget(v) .setExactMode(true) .setShowShadow(false) .setGravityOffset(5f, 5f, true) .setBadgeBackgroundColor(ContextCompat.getColor(this, EAHelper.getThemeColorLight())) CacheDB.INSTANCE.favsDAO().countLive.observe(this, Observer { integer -> if (badgeView != null && integer != null) if (PrefsUtil.showFavIndicator) badgeView?.badgeNumber = integer else badgeView?.hide(false) }) PrefsUtil.getLiveShowFavIndicator().observe(this, Observer { aBoolean -> if (badgeView != null) { if (aBoolean) lifecycleScope.launch { badgeView?.badgeNumber = withContext(Dispatchers.IO) { CacheDB.INSTANCE.favsDAO().count } } else badgeView?.hide(false) } }) PreferenceManager.getDefaultSharedPreferences(this).stringLiveData("theme_color", "0") .observe(this, Observer { (badgeView as? QBadgeView)?.badgeBackgroundColor = ContextCompat.getColor(this, EAHelper.getThemeColorLight(it)) badgeEmission.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor(it))) badgeSeeing.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor(it))) badgeQueue.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor(it))) navigationView.getHeaderView(0).findViewById(R.id.img).setBackgroundResource(EAHelper.getThemeImg(it)) }) } badgeEmission.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) badgeEmission.setTypeface(null, Typeface.BOLD) badgeEmission.gravity = Gravity.CENTER_VERTICAL badgeSeeing.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) badgeSeeing.setTypeface(null, Typeface.BOLD) badgeSeeing.gravity = Gravity.CENTER_VERTICAL badgeQueue.setTextColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) badgeQueue.setTypeface(null, Typeface.BOLD) badgeQueue.gravity = Gravity.CENTER_VERTICAL PrefsUtil.getLiveEmissionBlackList().observe(this, Observer { strings -> CacheDB.INSTANCE.animeDAO().getInEmission(strings).observe(this, Observer { integer -> badgeEmission.text = integer.toString() badgeEmission.visibility = if (integer == 0) View.GONE else View.VISIBLE }) }) CacheDB.INSTANCE.seeingDAO().countWatchingLive.observe(this, Observer { integer -> badgeSeeing.text = integer.toString() badgeSeeing.visibility = if (integer == 0) View.GONE else View.VISIBLE }) CacheDB.INSTANCE.queueDAO().countLive.observe(this, Observer { integer -> badgeQueue.text = integer.toString() badgeQueue.visibility = if (integer == 0) View.GONE else View.VISIBLE }) } catch (e: Exception) { e.printStackTrace() } } @TargetApi(Build.VERSION_CODES.M) private fun checkPermissions() { val permissions = mutableListOf() if (isFullMode) { if (Build.VERSION.SDK_INT in Build.VERSION_CODES.M..Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { permissions.add(Manifest.permission.POST_NOTIFICATIONS) } if (permissions.isNotEmpty()) { noCrash { requestPermissions(permissions.toTypedArray(), 55498) } } } private fun showRationalPermission(denied: Boolean = false) { MaterialDialog(this).safeShow { title(text = "Permiso de escritura") message(text = "Esta aplicación necesita el permiso obligatoriamente para guardar cache y descargar los episodios") positiveButton(text = "Aceptar") { if (denied) { try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName")).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } finish() startActivity(intent) } catch (e: ActivityNotFoundException) { "Error al mostrar configuración".toast() } } else checkPermissions() } negativeButton(text = "Salir") { finish() } cancelable(false) } } override fun onNeedUpdate(o_code: String, n_code: String) { runOnUiThread { try { if (n_code.toInt() > AdsUtils.remoteConfigs.getLong("min_version").toInt()) { MaterialDialog(this@MainMaterial).safeShow { title(text = "Actualización") message(text = "Parece que la versión $n_code está disponible, ¿Quieres actualizar?") positiveButton(text = "si") { UpdateActivity.start(this@MainMaterial, true, n_code) } negativeButton(text = "despues") { checkBypass() } } } else { finish() UpdateActivity.start( this@MainMaterial, false, n_code ) } } catch (e: Exception) { e.printStackTrace() } } } override fun onUpdateNotRequired() { ChangelogActivityMaterial.check(this) checkBypass() } private fun onStateDialog(message: String) { MaterialDialog(this).safeShow { message(text = message) positiveButton() } } override fun onCreateOptionsMenu(menu: Menu): Boolean { if (selectedFragment == null || selectedFragment is RecentFragment) { menuInflater.inflate(R.menu.main_material, menu) } else if (selectedFragment is FavoriteFragmentMaterial) { menuInflater.inflate(R.menu.fav_menu_material, menu) when (PrefsUtil.favsOrder) { 0 -> menu.findItem(R.id.by_name).isChecked = true 1 -> menu.findItem(R.id.by_id).isChecked = true } if (!PrefsUtil.showFavSections()) menu.findItem(R.id.action_new_category).isVisible = false } else if (selectedFragment is DirectoryFragmentMaterial && (PrefsUtil.isDirectoryFinished || !Network.isConnected)) { menuInflater.inflate(R.menu.dir_menu_material, menu) when (PrefsUtil.dirOrder) { 0 -> menu.findItem(R.id.by_name_dir).isChecked = true 1 -> menu.findItem(R.id.by_votes).isChecked = true 2 -> menu.findItem(R.id.by_id_dir).isChecked = true 3 -> menu.findItem(R.id.by_added_dir).isChecked = true 4 -> menu.findItem(R.id.by_followers).isChecked = true } } else { menuInflater.inflate(R.menu.main_material, menu) } CastUtil.registerActivity(this, menu, R.id.castMenu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_new_category -> if (selectedFragment is FavoriteFragmentMaterial) (selectedFragment as FavoriteFragmentMaterial).showNewCategory(null) R.id.by_name -> { PrefsUtil.favsOrder = 0 changeOrder() } R.id.by_name_dir -> { PrefsUtil.dirOrder = 0 changeOrder() } R.id.by_votes -> { PrefsUtil.dirOrder = 1 changeOrder() } R.id.by_id -> { PrefsUtil.favsOrder = 1 changeOrder() } R.id.by_id_dir -> { PrefsUtil.dirOrder = 2 changeOrder() } R.id.by_added_dir -> { PrefsUtil.dirOrder = 3 changeOrder() } R.id.by_followers -> { PrefsUtil.dirOrder = 4 changeOrder() } } return super.onOptionsItemSelected(item) } private fun changeOrder() { if (selectedFragment is FavoriteFragmentMaterial) { (selectedFragment as FavoriteFragmentMaterial).onChangeOrder() } else if (selectedFragment is DirectoryFragmentMaterial) { (selectedFragment as DirectoryFragmentMaterial).onChangeOrder() } invalidateOptionsMenu() } override fun onNavigationItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_bottom_recents -> setFragment(RecentModelsFragment.get()) R.id.action_bottom_favorites -> setFragment(FavoriteFragmentMaterial.get()) R.id.action_bottom_directory -> setFragment(DirectoryFragmentMaterial.get()) R.id.action_bottom_settings -> setFragment(BottomPreferencesMaterialFragment.get()) R.id.drawer_explorer -> ExplorerActivityMaterial.open(this) R.id.drawer_emision -> EmissionActivityMaterial.open(this) R.id.drawer_queue -> QueueActivityMaterial.open(this) R.id.drawer_suggestions -> RecommendActivityMaterial.open(this) R.id.drawer_news -> MaterialNewsActivity.open(this) R.id.drawer_records -> RecordActivityMaterial.open(this) R.id.drawer_seeing -> SeeingActivityMaterial.open(this) R.id.drawer_random -> RandomActivityMaterial.open(this) R.id.drawer_faq -> FaqActivityMaterial.open(this) } closeDrawer() return true } private fun setFragment(fragment: BottomFragment) { lifecycleScope.launch(Dispatchers.Main) { try { selectedFragment = fragment if (selectedFragment is BottomPreferencesMaterialFragment) { toolbar.title = "UKIKU" toolbar.changeToolbarFont(R.font.audiowide) } else { toolbar.title = "Buscar animes" toolbar.changeToolbarFont(R.font.open_sans) } val transaction = supportFragmentManager.beginTransaction() //transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out) transaction.replace(R.id.root, fragment) transaction.commit() invalidateOptionsMenu() } catch (e: Exception) { e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) Toaster.toastLong("Error en fragmento: ${e.message}") } } } private fun closeDrawer() { drawer.closeDrawer(GravityCompat.START) } private fun returnSelectFragment() { if (selectedFragment != null) { when (selectedFragment) { is FavoriteFragmentMaterial -> bottomNavigationView.selectedItemId = R.id.action_bottom_favorites is DirectoryFragmentMaterial -> bottomNavigationView.selectedItemId = R.id.action_bottom_directory is BottomPreferencesFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_settings else -> bottomNavigationView.selectedItemId = R.id.action_bottom_recents } } requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } private fun startChange() { when (intent.getIntExtra("start_position", -1)) { 0 -> setFragment(RecentModelsFragment.get()) 1 -> bottomNavigationView.selectedItemId = R.id.action_bottom_favorites 2 -> bottomNavigationView.selectedItemId = R.id.action_bottom_directory 3 -> bottomNavigationView.selectedItemId = R.id.action_bottom_settings else -> setFragment(RecentModelsFragment.get()) } } private fun reselectFragment() { if (selectedFragment != null) { when (selectedFragment) { is FavoriteFragmentMaterial -> bottomNavigationView.selectedItemId = R.id.action_bottom_recents is DirectoryFragmentMaterial -> bottomNavigationView.selectedItemId = R.id.action_bottom_directory is BottomPreferencesFragment -> bottomNavigationView.selectedItemId = R.id.action_bottom_settings else -> bottomNavigationView.selectedItemId = R.id.action_bottom_recents } } requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } override fun onNavigationItemReselected(item: MenuItem) { selectedFragment?.onReselect() } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) permissions.forEachIndexed { result, permission -> if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE && result != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) showRationalPermission() else showRationalPermission(true) } } } @SuppressLint("SetTextI18n") override fun onResume() { super.onResume() invalidateOptionsMenu() connectionState.setUp(this, ::onStateDialog) lifecycleScope.launch(Dispatchers.Main) { val backupLocation = navigationView.getHeaderView(0).findViewById(R.id.backupLocation) backupLocation.text = when (Backups.type) { Backups.Type.NONE -> "Sin respaldos" Backups.Type.DROPBOX -> "Dropbox" Backups.Type.FIRESTORE -> "Firestore" Backups.Type.LOCAL -> "Local" } } if (isFirst) { isFirst = false } } override fun onPause() { syncData { achievements() } super.onPause() } override fun onUAChange() { checkBypass() } override fun onAttachedToWindow() { super.onAttachedToWindow() UpdateChecker.check(this, this) } override fun onNeedRecreate() { reselectFragment() } override fun onDestroy() { if (!isChangingConfigurations) CastUtil.get().onDestroy() super.onDestroy() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) noCrash { if (requestCode == FileAccessHelper.SD_REQUEST && resultCode == RESULT_OK) { val validation = FileAccessHelper.isUriValid(data?.data) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") FileAccessHelper.openTreeChooser(this) } } } setNavigationButtons() } override fun onBypassUpdated() { onNeedRecreate() } override fun getSnackbarAnchor(): View { return root } } ================================================ FILE: app/src/main/java/knf/kuma/SplashActivity.kt ================================================ package knf.kuma import android.content.Intent import android.os.Bundle import android.util.Log import androidx.lifecycle.lifecycleScope import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.security.ProviderInstaller import com.google.android.ump.ConsentInformation import com.google.android.ump.ConsentRequestParameters import com.google.android.ump.UserMessagingPlatform import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdsUtils import knf.kuma.ads.SubscriptionReceiver import knf.kuma.commons.BypassUtil import knf.kuma.commons.DesignUtils import knf.kuma.commons.PrefsUtil import knf.kuma.custom.GenericActivity import knf.kuma.tv.ui.TVMain import knf.tools.signatures.getSignatures import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import xdroid.toaster.Toaster import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class SplashActivity : GenericActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_blank) AchievementManager.onAppStart() SubscriptionReceiver.check(intent) printSignatures() when { resources.getBoolean(R.bool.isTv) -> { startActivity(Intent(this, TVMain::class.java)) finish() } /*!isFullMode && BuildConfig.BUILD_TYPE != "amazon" && !PrefsUtil.isPSWarned -> MaterialDialog( this ).safeShow { title(text = "Aviso") message(text = "Usted esta usando la version de Google Play, esta version tiene caracteristicas deshabilitadas, para una experiencia completa por favor use la version de la pagina oficial\nEscriba \"confirmar\" para continuar.") input(hint = "Respuesta...", waitForPositiveButton = false) { _, text -> getActionButton(WhichButton.POSITIVE).isEnabled = text.toString() .lowercase(Locale.getDefault()) == "confirmar" } negativeButton(text = "Web") { noCrash { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://ukiku.app"))) } } positiveButton(text = "Continuar") { PrefsUtil.isPSWarned = true startApp() } cancelOnTouchOutside(false) }*/ else -> { lifecycleScope.launch { showGDPR { startApp() } } } } } private suspend fun showGDPR(onFinish: () -> Unit) { val consentInfo = UserMessagingPlatform.getConsentInformation(this) val params = ConsentRequestParameters.Builder().apply { setTagForUnderAgeOfConsent(false) }.build() suspendCoroutine { val ok = { it.resume(true) } consentInfo.requestConsentInfoUpdate(this, params, { ok() }, { ok() }) } Log.e("GDPR", "On consent, status: ${consentInfo.consentStatus}, available: ${consentInfo.isConsentFormAvailable}") if (consentInfo.consentStatus == ConsentInformation.ConsentStatus.REQUIRED && consentInfo.isConsentFormAvailable) { val form = suspendCoroutine { continuation -> UserMessagingPlatform.loadConsentForm(this, { continuation.resume(it) }, { continuation.resume(null) } ) } form?.show(this) { Log.e("GDPR", "On form dismiss, obtained: ${consentInfo.consentStatus == ConsentInformation.ConsentStatus.OBTAINED}") onFinish() } } else { onFinish() } } private fun doBlockTests(): Boolean { var blockCount = 0 repeat(3) { if (BypassUtil.isCloudflareActiveRandom()) blockCount++ if (blockCount >= 2) return true } return blockCount >= 2 } private fun printSignatures() { if (BuildConfig.DEBUG) { getSignatures().signatures.forEach { Log.e("Signature", it.encoded) } } } private suspend fun installSecurityProvider() { withContext(Dispatchers.IO) { try { ProviderInstaller.installIfNeeded(this@SplashActivity) PrefsUtil.isSecurityUpdated = true PrefsUtil.spErrorType = null } catch (e: GooglePlayServicesRepairableException) { PrefsUtil.isSecurityUpdated = false PrefsUtil.spErrorType = "Gplay services deshabilitado o desactualizado" e.printStackTrace() } catch (e: GooglePlayServicesNotAvailableException) { PrefsUtil.isSecurityUpdated = false PrefsUtil.spErrorType = "GPlay services no esta disponible" e.printStackTrace() } catch (e: Exception) { PrefsUtil.isSecurityUpdated = false Toaster.toastLong("SProvider: Unknown error, ${e.message}") PrefsUtil.spErrorType = "Error desconocido: ${e.message}" e.printStackTrace() } if (!PrefsUtil.isSecurityUpdated && FirebaseCrashlytics.getInstance().didCrashOnPreviousExecution()) { PrefsUtil.spProtectionEnabled = true Toaster.toastLong("Proteccion de SP reactivada") } } } private fun startApp() { lifecycleScope.launch(Dispatchers.Main) { if (PrefsUtil.mayUseRandomUA) PrefsUtil.alwaysGenerateUA = !withContext(Dispatchers.IO) { doBlockTests() } else PrefsUtil.alwaysGenerateUA = false installSecurityProvider() DesignUtils.change(this@SplashActivity, start = false) AdsUtils.remoteConfigs.ensureInitialized().addOnCompleteListener { var initializated = false AdsUtils.setUp(this@SplashActivity) { if (!initializated) { initializated = true startActivity(Intent(this@SplashActivity, DesignUtils.mainClass)) finish() } } } } } } ================================================ FILE: app/src/main/java/knf/kuma/achievements/AchievementActivity.kt ================================================ package knf.kuma.achievements import androidx.activity.addCallback import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.Menu import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.viewpager.widget.ViewPager import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.tabs.TabLayout import com.mikhaellopez.circularprogressbar.CircularProgressBar import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.AdsUtils import knf.kuma.ads.FullscreenAdLoader import knf.kuma.ads.getFAdLoaderInterstitial import knf.kuma.ads.getFAdLoaderRewarded import knf.kuma.ads.implBanner import knf.kuma.ads.showRandomInterstitial import knf.kuma.backup.Backups import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.Economy import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.diceOf import knf.kuma.commons.getPackage import knf.kuma.commons.safeShow import knf.kuma.custom.AchievementUnlocked import knf.kuma.custom.BannerContainerView import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.pojos.Achievement import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.toast import xdroid.toaster.Toaster import java.text.NumberFormat import java.util.Locale import androidx.core.net.toUri class AchievementActivity : GenericActivity() { private val toolbar: Toolbar by bind(R.id.toolbar) private val tabs: TabLayout by bind(R.id.tabs) private val pager: ViewPager by bind(R.id.pager) private val progress: CircularProgressBar by bind(R.id.progress) private val level: TextView by bind(R.id.level) private val countDown: TextView by bind(R.id.countdown) private val cardView: MaterialCardView by bind(R.id.sheet) private val buyButton: MaterialButton by bind(R.id.buyButton) private val icon: ImageView by bind(R.id.achievement_icon) private val xpIndicator: TextView by bind(R.id.achievement_xp) private val state: TextView by bind(R.id.achievement_state) private val progressIndicator: View by bind(R.id.progress_indicator) private val progressBar: ProgressBar by bind(R.id.progress_bar) private val progressText: TextView by bind(R.id.progress_text) private val progressIndText: TextView by bind(R.id.progress_ind_text) private val name: TextView by bind(R.id.achievement_name) private val description: TextView by bind(R.id.achievement_description) private val adContainer: BannerContainerView by bind(R.id.adContainer) private lateinit var bottomSheet: BottomSheetBehavior private var syncButton: MenuItem? = null private val levelCalculator = LevelCalculator() private val rewardedAd: FullscreenAdLoader by lazy { getFAdLoaderRewarded(this) } private var interstitial: FullscreenAdLoader = getFAdLoaderInterstitial(this) override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(R.layout.activity_achievement_profile) setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.title = "Logros" toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } bottomSheet = BottomSheetBehavior.from(cardView) bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN val backCallback = onBackPressedDispatcher.addCallback(this, false) { bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN } bottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { backCallback.isEnabled = newState == BottomSheetBehavior.STATE_EXPANDED } override fun onSlide(bottomSheet: View, slideOffset: Float) { } }) pager.offscreenPageLimit = 2 pager.adapter = AchievementsFragmentsPagerAdapter(supportFragmentManager) { onMoreInfo(it) } tabs.setupWithViewPager(pager) rewardedAd.load() interstitial.load() if (!PrefsUtil.isNativeAdsEnabled) adContainer.implBanner(AdsType.ACHIEVEMENT_BANNER) showRandomInterstitial(this,PrefsUtil.fullAdsExtraProbability) } override fun onAttachedToWindow() { super.onAttachedToWindow() if (!PrefsUtil.isAchievementsOmitted && !Settings.canDrawOverlays(this)) MaterialDialog(this).safeShow { message(text = "Para mostrar una mejor animacion al desbloquear logros, la app necesita un permiso especial, ¿Deseas activarlo?") positiveButton(text = "Activar") { try { startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).setData( "package:${getPackage()}".toUri()), 5879) } catch (_: Exception) { Toaster.toast("No se pudo abrir la configuracion") } } negativeButton(text = "Omitir") { PrefsUtil.isAchievementsOmitted = true } } } @SuppressLint("SetTextI18n") override fun onResume() { super.onResume() CacheDB.INSTANCE.achievementsDAO().totalUnlockedPoints.observe(this, Observer { lifecycleScope.launch(Dispatchers.Main) { levelCalculator.calculate(it ?: 0) if (it != withContext(Dispatchers.IO){ CacheDB.INSTANCE.achievementsDAO().totalPoints }) { progress.progressMax = levelCalculator.max.toFloat() progress.progress = levelCalculator.progress.toFloat() progressIndText.visibility = View.VISIBLE countDown.text = "${NumberFormat.getNumberInstance(Locale.US).format(levelCalculator.toLvlUp)} XP" } else { progress.progressMax = 100f progress.progress = 100f progressIndText.visibility = View.GONE countDown.text = "MAXIMO NIVEL" } level.text = levelCalculator.level.toString() } }) } @SuppressLint("SetTextI18n") private fun onMoreInfo(achievement: Achievement) { if (achievement.isSecret && !achievement.isRevealed && !achievement.isUnlocked) { val cost = ((achievement.points / 1000) * 25) buyButton.text = cost.toString() buyButton.visibility = View.VISIBLE buyButton.setOnClickListener { if (Economy.buy(cost)) { achievement.isRevealed = true doAsync { CacheDB.INSTANCE.achievementsDAO().update(achievement) syncData { achievements() } } onMoreInfo(achievement) } else toast("Loli-coins insuficientes") } } else buyButton.visibility = View.GONE icon.setImageResource(achievement.usableIcon()) xpIndicator.text = "${NumberFormat.getNumberInstance(Locale.US).format(achievement.points)} XP" state.text = achievement.getState() if (!achievement.isSecret && !achievement.isUnlocked && achievement.goal > 0) { progressIndicator.visibility = View.VISIBLE progressBar.apply { max = achievement.goal progress = achievement.count } progressText.text = "${achievement.count} / ${achievement.goal}" } else progressIndicator.visibility = View.GONE name.text = achievement.usableName() description.text = achievement.usableDescription() bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { if (ev?.action == MotionEvent.ACTION_DOWN) if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { val rect = Rect() cardView.getGlobalVisibleRect(rect) return if (!rect.contains(ev.rawX.toInt(), ev.rawY.toInt())) { bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN true } else super.dispatchTouchEvent(ev) } return super.dispatchTouchEvent(ev) } private fun showAd() { diceOf<() -> Unit> { put({ rewardedAd.show() }, AdsUtils.remoteConfigs.getDouble("rewarded_percent")) put({ interstitial.show() }, AdsUtils.remoteConfigs.getDouble("interstitial_percent")) }() } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_achievements, menu) if (Backups.type != Backups.Type.NONE && Backups.type != Backups.Type.FIRESTORE) { syncButton = menu.findItem(R.id.sync) } else menu.findItem(R.id.sync)?.isVisible = false return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.backup -> { syncButton?.isEnabled = false AchievementManager.backup { invalidateOptionsMenu() } } R.id.restore -> { syncButton?.isEnabled = false AchievementManager.restore { invalidateOptionsMenu() } } R.id.coins -> { Economy.showWallet(this) { showAd() } } } return super.onOptionsItemSelected(item) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 5879) { if (Settings.canDrawOverlays(this)) { Toaster.toast("Logros mejorados!") val achievementUnlocked = AchievementUnlocked(this).apply { setRounded(false) setLarge(true) setDismissible(true) } lifecycleScope.launch(Dispatchers.IO) { val list = CacheDB.INSTANCE.achievementsDAO().completedAchievements val achievementList = mutableListOf() list.forEach { achievementList.add(it.achievementData(this@AchievementActivity)) } launch(Dispatchers.Main) { achievementUnlocked.show(achievementList) } } } else Toaster.toast("Permiso no concedido") } } companion object { fun open(context: Context) { context.startActivity(Intent(context, AchievementActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/achievements/AchievementActivityMaterial.kt ================================================ package knf.kuma.achievements import androidx.activity.addCallback import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.graphics.Rect import android.net.Uri import android.os.Bundle import android.provider.Settings import android.view.Menu import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope import androidx.viewpager.widget.ViewPager import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.tabs.TabLayout import com.mikhaellopez.circularprogressbar.CircularProgressBar import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.AdsUtils import knf.kuma.ads.FullscreenAdLoader import knf.kuma.ads.getFAdLoaderInterstitial import knf.kuma.ads.getFAdLoaderRewarded import knf.kuma.ads.implBanner import knf.kuma.ads.showRandomInterstitial import knf.kuma.backup.Backups import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.Economy import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.diceOf import knf.kuma.commons.getPackage import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.AchievementUnlocked import knf.kuma.custom.BannerContainerView import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.pojos.Achievement import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.toast import xdroid.toaster.Toaster import java.text.NumberFormat import java.util.Locale import androidx.core.net.toUri class AchievementActivityMaterial : GenericActivity() { private val toolbar: Toolbar by bind(R.id.toolbar) private val tabs: TabLayout by bind(R.id.tabs) private val pager: ViewPager by bind(R.id.pager) private val progress: CircularProgressBar by bind(R.id.progress) private val level: TextView by bind(R.id.level) private val countDown: TextView by bind(R.id.countdown) private val cardView: MaterialCardView by bind(R.id.sheet) private val buyButton: MaterialButton by bind(R.id.buyButton) private val icon: ImageView by bind(R.id.achievement_icon) private val xpIndicator: TextView by bind(R.id.achievement_xp) private val state: TextView by bind(R.id.achievement_state) private val progressIndicator: View by bind(R.id.progress_indicator) private val progressBar: ProgressBar by bind(R.id.progress_bar) private val progressText: TextView by bind(R.id.progress_text) private val progressIndText: TextView by bind(R.id.progress_ind_text) private val name: TextView by bind(R.id.achievement_name) private val description: TextView by bind(R.id.achievement_description) private val adContainer: BannerContainerView by bind(R.id.adContainer) private lateinit var bottomSheet: BottomSheetBehavior private var syncButton: MenuItem? = null private val levelCalculator = LevelCalculator() private val rewardedAd: FullscreenAdLoader by lazy { getFAdLoaderRewarded(this) } private var interstitial: FullscreenAdLoader = getFAdLoaderInterstitial(this) override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(R.layout.activity_achievement_profile_material) setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.title = "Logros" toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } bottomSheet = BottomSheetBehavior.from(cardView) bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN val backCallback = onBackPressedDispatcher.addCallback(this, false) { bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN } bottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { backCallback.isEnabled = newState == BottomSheetBehavior.STATE_EXPANDED } override fun onSlide(bottomSheet: View, slideOffset: Float) { } }) pager.offscreenPageLimit = 2 pager.adapter = AchievementsFragmentsPagerAdapter(supportFragmentManager) { onMoreInfo(it) } tabs.setupWithViewPager(pager) rewardedAd.load() interstitial.load() if (!PrefsUtil.isNativeAdsEnabled) adContainer.implBanner(AdsType.ACHIEVEMENT_BANNER) showRandomInterstitial(this,PrefsUtil.fullAdsExtraProbability) } override fun onAttachedToWindow() { super.onAttachedToWindow() if (!PrefsUtil.isAchievementsOmitted && !Settings.canDrawOverlays(this)) MaterialDialog(this).safeShow { message(text = "Para mostrar una mejor animacion al desbloquear logros, la app necesita un permiso especial, ¿Deseas activarlo?") positiveButton(text = "Activar") { try { startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).setData( "package:${getPackage()}".toUri()), 5879) } catch (_: Exception) { Toaster.toast("No se pudo abrir la configuracion") } } negativeButton(text = "Omitir") { PrefsUtil.isAchievementsOmitted = true } } } @SuppressLint("SetTextI18n") override fun onResume() { super.onResume() CacheDB.INSTANCE.achievementsDAO().totalUnlockedPoints.observe(this) { lifecycleScope.launch(Dispatchers.Main) { levelCalculator.calculate(it ?: 0) if (it != withContext(Dispatchers.IO) { CacheDB.INSTANCE.achievementsDAO().totalPoints }) { progress.progressMax = levelCalculator.max.toFloat() progress.progress = levelCalculator.progress.toFloat() progressIndText.visibility = View.VISIBLE countDown.text = "${NumberFormat.getNumberInstance(Locale.US).format(levelCalculator.toLvlUp)} XP" } else { progress.progressMax = 100f progress.progress = 100f progressIndText.visibility = View.GONE countDown.text = "MAXIMO NIVEL" } level.text = levelCalculator.level.toString() } } } @SuppressLint("SetTextI18n") private fun onMoreInfo(achievement: Achievement) { if (achievement.isSecret && !achievement.isRevealed && !achievement.isUnlocked) { val cost = ((achievement.points / 1000) * 25) buyButton.text = cost.toString() buyButton.visibility = View.VISIBLE buyButton.setOnClickListener { if (Economy.buy(cost)) { achievement.isRevealed = true doAsync { CacheDB.INSTANCE.achievementsDAO().update(achievement) syncData { achievements() } } onMoreInfo(achievement) } else toast("Loli-coins insuficientes") } } else buyButton.visibility = View.GONE icon.setImageResource(achievement.usableIcon()) xpIndicator.text = "${NumberFormat.getNumberInstance(Locale.US).format(achievement.points)} XP" state.text = achievement.getState() if (!achievement.isSecret && !achievement.isUnlocked && achievement.goal > 0) { progressIndicator.visibility = View.VISIBLE progressBar.apply { max = achievement.goal progress = achievement.count } progressText.text = "${achievement.count} / ${achievement.goal}" } else progressIndicator.visibility = View.GONE name.text = achievement.usableName() description.text = achievement.usableDescription() bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { if (ev?.action == MotionEvent.ACTION_DOWN) if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { val rect = Rect() cardView.getGlobalVisibleRect(rect) return if (!rect.contains(ev.rawX.toInt(), ev.rawY.toInt())) { bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN true } else super.dispatchTouchEvent(ev) } return super.dispatchTouchEvent(ev) } private fun showAd() { diceOf<() -> Unit> { put({ rewardedAd.show() }, AdsUtils.remoteConfigs.getDouble("rewarded_percent")) put({ interstitial.show() }, AdsUtils.remoteConfigs.getDouble("interstitial_percent")) }() } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_achievements, menu) if (Backups.type != Backups.Type.NONE && Backups.type != Backups.Type.FIRESTORE) { syncButton = menu.findItem(R.id.sync) } else menu.findItem(R.id.sync)?.isVisible = false return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.backup -> { syncButton?.isEnabled = false AchievementManager.backup { invalidateOptionsMenu() } } R.id.restore -> { syncButton?.isEnabled = false AchievementManager.restore { invalidateOptionsMenu() } } R.id.coins -> { Economy.showWallet(this) { showAd() } } } return super.onOptionsItemSelected(item) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 5879) { if (Settings.canDrawOverlays(this)) { Toaster.toast("Logros mejorados!") val achievementUnlocked = AchievementUnlocked(this).apply { setRounded(false) setLarge(true) setDismissible(true) } lifecycleScope.launch(Dispatchers.IO) { val list = CacheDB.INSTANCE.achievementsDAO().completedAchievements val achievementList = mutableListOf() list.forEach { achievementList.add(it.achievementData(this@AchievementActivityMaterial)) } launch(Dispatchers.Main) { achievementUnlocked.show(achievementList) } } } else Toaster.toast("Permiso no concedido") } } companion object { fun open(context: Context) { context.startActivity(Intent(context, AchievementActivityMaterial::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/achievements/AchievementAdapter.kt ================================================ package knf.kuma.achievements import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.ads.AdCallback import knf.kuma.ads.AdCardItemHolder import knf.kuma.ads.AdsUtilsMob import knf.kuma.ads.implAdsAchievement import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashLet import knf.kuma.pojos.Achievement import knf.kuma.pojos.AchievementAd import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.text.NumberFormat import java.util.Locale class AchievementAdapter(private val onClick: (achievement: Achievement) -> Unit) : RecyclerView.Adapter() { private var list: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { if (viewType == 1) return AdCardItemHolder(parent, AdCardItemHolder.TYPE_ACHIEVEMENT).also { it.loadAd(GlobalScope, object : AdCallback { override fun getID(): String = AdsUtilsMob.ACHIEVEMENT_NATIVE }, 500) } return ItemHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_achievements, parent, false)) } override fun getItemCount(): Int { return list.size } override fun getItemViewType(position: Int): Int { return noCrashLet { if (list[position] is AchievementAd) 1 else 0 } ?: 1 } @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val achievement = list[position] if (holder is ItemHolder) { holder.icon.setImageResource(achievement.usableIcon()) holder.name.text = achievement.usableName() holder.state.text = achievement.getState() holder.exp.text = "${NumberFormat.getNumberInstance(Locale.US).format(achievement.points)} XP" holder.root.setOnClickListener { noCrash { onClick.invoke(achievement) } } } } fun setAchievements(list: MutableList) { GlobalScope.launch(Dispatchers.IO) { this@AchievementAdapter.list = list if (PrefsUtil.isNativeAdsEnabled) this@AchievementAdapter.list.implAdsAchievement() launch(Dispatchers.Main) { notifyDataSetChanged() } } } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.root) val icon: ImageView by itemView.bind(R.id.icon) val name: TextView by itemView.bind(R.id.name) val state: TextView by itemView.bind(R.id.state) val exp: TextView by itemView.bind(R.id.exp) } } ================================================ FILE: app/src/main/java/knf/kuma/achievements/AchievementFragment.kt ================================================ package knf.kuma.achievements import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.database.CacheDB import knf.kuma.pojos.Achievement import nl.dionsegijn.konfetti.KonfettiView import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.jetbrains.anko.find class AchievementFragment : Fragment() { private lateinit var recyclerView: RecyclerView private lateinit var error: View private lateinit var errorText: TextView private lateinit var onClick: OnClick private lateinit var konfetti: KonfettiView private val adapter = AchievementAdapter { onClick.invoke(it) } private val isLockedScreen: Boolean by lazy { arguments?.getInt(isUnlockedKey, 0) ?: 0 == 0 } private var isListEmpty: Boolean = false private var isFirst = true override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) CacheDB.INSTANCE.achievementsDAO().achievementList(arguments?.getInt(isUnlockedKey, 0) ?: 0) .observe(viewLifecycleOwner, Observer { isListEmpty = it.isEmpty() adapter.setAchievements(it) error.visibility = if (it.isEmpty()) View.VISIBLE else View.GONE if (isFirst) { recyclerView.scheduleLayoutAnimation() isFirst = false } }) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_achievements, container, false).also { error = it.find(R.id.error) errorText = it.find(R.id.error_text) konfetti = it.find(R.id.konfetti) errorText.text = if (arguments?.getInt(isUnlockedKey, 0) == 0) "Has completado todos los logros" else "No has completado ningun logro" recyclerView = it.find(R.id.recycler) recyclerView.addItemDecoration(DividerItemDecoration(context, LinearLayout.VERTICAL)) recyclerView.adapter = adapter } } override fun onResume() { super.onResume() if (isLockedScreen && isListEmpty) konfetti.build().apply { addColors(Color.BLUE, Color.RED, Color.YELLOW, Color.GREEN, Color.MAGENTA) setDirection(0.0, 359.0) setSpeed(4f, 7f) setFadeOutEnabled(true) setTimeToLive(2000) addShapes(Shape.RECT, Shape.CIRCLE) addSizes(Size(12, 6f), Size(16, 6f)) setPosition(-50f, konfetti.width + 50f, -50f, -50f) }.streamFor(200, 10000L) } override fun onPause() { super.onPause() if (isLockedScreen && isListEmpty) konfetti.reset() } fun setCallback(onClick: OnClick) { this.onClick = onClick } companion object { private const val isUnlockedKey = "isUnlocked" fun get(isUnlocked: Int, onClick: OnClick): AchievementFragment { val achievementFragment = AchievementFragment() val bundle = Bundle().apply { this.putInt(isUnlockedKey, isUnlocked) } achievementFragment.arguments = bundle achievementFragment.setCallback(onClick) return achievementFragment } } } typealias OnClick = (achievement: Achievement) -> Unit ================================================ FILE: app/src/main/java/knf/kuma/achievements/AchievementManager.kt ================================================ package knf.kuma.achievements import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager import android.os.Build import android.provider.Settings import androidx.annotation.DrawableRes import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import knf.kuma.R import knf.kuma.backup.Backups import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.distinct import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.isTV import knf.kuma.commons.noCrash import knf.kuma.commons.toast import knf.kuma.custom.AchievementUnlocked import knf.kuma.database.CacheDB import knf.kuma.database.EADB import knf.kuma.pojos.Achievement import knf.kuma.pojos.FavoriteObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @SuppressLint("StaticFieldLeak") object AchievementManager { private lateinit var context: Context private lateinit var achievementUnlocked: AchievementUnlocked private lateinit var completionLiveData: LiveData> private val achievementsDAO = CacheDB.INSTANCE.achievementsDAO() private val liveList = arrayListOf>() private const val VERSION = 3 fun init(context: Context) { this.context = context achievementUnlocked = AchievementUnlocked(context).apply { setRounded(false) setLarge(true) setDismissible(true) } preloadAchievements() achievementsDAO.completionListener.also { completionLiveData = it }.distinct.observeForever(Observer { if (it.isEmpty()) return@Observer val list: List = it.map { achievement -> achievement.key.toInt() } unlock(list) }) CacheDB.INSTANCE.seenDAO().countLive.also { liveList.add(it) }.distinct.observeForever { updateCount(it, listOf(33, 39)) } CacheDB.INSTANCE.favsDAO().countLive.also { liveList.add(it) }.distinct.observeForever { updateCount(it, listOf(11, 1, 2, 3, 4, 5)) } CacheDB.INSTANCE.seeingDAO().countLive.also { liveList.add(it) }.distinct.observeForever { updateCount(it, listOf(16, 17, 18, 19)) } CacheDB.INSTANCE.seeingDAO().countCompletedLive.also { liveList.add(it) }.distinct.observeForever { updateCount(it, listOf(20, 21, 22, 23)) } CacheDB.INSTANCE.seeingDAO().countDroppedLive.also { liveList.add(it) }.distinct.observeForever { updateCount(it, listOf(24, 25, 26, 27)) } CacheDB.INSTANCE.seeingDAO().isAnimeCompleted(listOf("363", "1706", "2950", "1182", "2479", "2478")).also { liveList.add(it) }.distinct.observeForever { if (it == 6) unlock(listOf(38)) } CacheDB.INSTANCE.seeingDAO().isAnimeCompleted(listOf("1487", "1488", "1019", "460", "1493", "1494")).also { liveList.add(it) }.distinct.observeForever { if (it == 6) unlock(listOf(45)) } } private fun resetIndicator() { achievementUnlocked.apply { setRounded(false) setLarge(true) setDismissible(true) } } private fun preloadAchievements() { doAsync { if (VERSION > PrefsUtil.achievementsVersion) { achievementsDAO.nuke() PrefsUtil.achievementsVersion = VERSION } achievementsDAO.insert( Achievement(0, "Primeros pasos", "Abre la app por primera vez", points = 1000), Achievement(1, "Pathetic", "Agrega 10 favoritos", points = 1000, goal = 10), Achievement(2, "¿Lo estas intentando?", "Agrega 100 favoritos", 2000, goal = 100), Achievement(3, "Ya nos vamos entendiendo", "Agrega 200 favoritos", 3000, goal = 200), Achievement(4, "Oye tranquilo viejo", "Agrega 500 favoritos", 4000, goal = 500), Achievement(5, "Estas demente parker", "Agrega 1000 favoritos", 6000, goal = 1000, isSecret = true), Achievement(6, "Remoto", "Usa Cast por primera vez", points = 1000, isSecret = true), Achievement(7, "Veterano", "Ten instalado Animeflv App", points = 3000, isSecret = true), Achievement(8, "Iniciado", "Ten instalada la app por 3 meses", points = 2000, isSecret = true), Achievement(9, "Empezando a cultivar", "Ten instalada la app por 6 meses", points = 3000, isSecret = true), Achievement(10, "Feliz cumpleaños", "Ten instalada la app por 1 año", points = 6000, isSecret = true), Achievement(11, "Primer amor", "Añade tu primer favorito", points = 1000, goal = 1), Achievement(12, "Algo fácil para iniciar", "Inicia el misterio", points = 1000, isSecret = true), Achievement(13, "El mejor escondite esta a la vista", "Descubre la secuencia", points = 2000, isSecret = true), Achievement(14, "Va para el curriculum", "Descubre para que sirve US", points = 6000, isSecret = true), Achievement(15, "Cuna del manga", "Encuentra Akihabara", points = 2000, isSecret = true), Achievement(16, "Por algo se empieza", "Sigue 5 animes", points = 1000, goal = 5), Achievement(17, "Se prendió esta mierda", "Sigue 15 animes", points = 2000, goal = 15), Achievement(18, "Esto se va a descontrolar", "Sigue 40 animes", points = 3000, goal = 40), Achievement(19, "Con el Rinnegan lo veo todo", "Sigue 100 animes", points = 4000, goal = 100), Achievement(20, "El inicio del camino", "Marca 1 anime como completado", points = 1000, goal = 1), Achievement(21, "Te está gustando?", "Marca 5 animes como completados", points = 2000, goal = 5), Achievement(22, "Ya no hay vuelta atrás", "Marca 20 animes como completados", points = 3000, goal = 20), Achievement(23, "Otaku", "Marca 50 animes como completados", points = 4000, goal = 50), Achievement(24, "Mala elección", "Dropea 1 anime", points = 1000, goal = 1, isSecret = true), Achievement(25, "Algo anda mal...", "Dropea 5 animes", points = 2000, goal = 5, isSecret = true), Achievement(26, "No te gusta nada", "Dropea 15 animes", points = 3000, goal = 15, isSecret = true), Achievement(27, "Antes eras chido...", "Dropea 30 animes", points = 4000, goal = 30, isSecret = true), Achievement(28, "Lo has logrado!", "Completa el easter egg", points = 12000, isSecret = true), Achievement(29, "Viviendo al limite", "Reproduce un episodio con poca batería", points = 2500, isSecret = true), Achievement(30, "Informado", "Lee 20 noticias", points = 1500, goal = 20), Achievement(31, "Vampiro", "Ve anime pasada la media noche", points = 2000, isSecret = true), Achievement(32, "Estas aburrido?", "Refresca la pantalla random 15 veces", points = 2500, isSecret = true), Achievement(33, "Otaku definitivo", "Ve 15k episodios", points = 15000, goal = 15000, isSecret = true), Achievement(34, "Bien hecho puerco", "Agrega 10 ecchis a favoritos", points = 2000, goal = 10), Achievement(35, "Viajero", "Descarga un anime completo", points = 2000), Achievement(36, "Que milagro verte por aquí", "No uses la app por una semana", points = 3000, isSecret = true), Achievement(37, "La aventura comienza", "Agrega un shounen a favoritos", points = 2000), Achievement(38, "1.048596", "Completa toda la saga de Steins;Gate", points = 10000, isSecret = true), Achievement(39, "Sabio de los 6 caminos", "Ve 5000 episodios", points = 5000, goal = 5000), Achievement(40, "Que haces?", "Presiona el botón de configuracion 20 veces", points = 5000, isSecret = true), Achievement(41, "Mas vale prevenir", "Respalda tus datos en la nube", points = 2000), Achievement(42, "Compartiendo sabiduria", "Comparte 20 animes", points = 2000, goal = 20), Achievement(43, "Boku no pico?", "Busca boku no hero", points = 2000, isSecret = true), Achievement(44, "Alzheimer?", "Abre el historial 20 veces", points = 2000, goal = 20), Achievement(45, "A Sam le gusta esto", "Completa todo Evangelion", points = 6000, isSecret = true), Achievement(46, "Tu primera loli", "Obtén 1 loli-coin", points = 1000, goal = 1, isSecret = true), Achievement(47, "Nyanpasu", "Obtén 10 loli-coins", points = 1500, goal = 10, isSecret = true), Achievement(48, "Al dev le gusta esto", "Obtén 50 loli-coins", points = 3000, goal = 50, isSecret = true), Achievement(49, "Eso muerde el cebo", "Obtén 100 loli-coins", points = 4500, goal = 100, isSecret = true), Achievement(50, "La ONU te busca", "Obtén 500 loli-coins", points = 6000, goal = 500, isSecret = true), Achievement(51, "Al Chico Loli le gusta esto", "Obtén 1000 loli-coins", points = 7500, goal = 1000, isSecret = true) ) } } @DrawableRes fun getIcon(key: Long): Int { return when (key) { 0L -> R.drawable.ic_achievement_start in 1..5, 11L -> R.drawable.ic_achievement_fav 6L -> R.drawable.ic_achievement_cast 7L -> R.drawable.ic_umaru_simple in 8..10 -> R.drawable.ic_achievement_calendar in 12..15, 28L -> R.drawable.ic_achievement_egg in 16..19 -> R.drawable.ic_achievement_following in 20..23 -> R.drawable.ic_achievement_completed in 24..27 -> R.drawable.ic_achievement_droped 29L -> R.drawable.ic_achievement_battery 30L -> R.drawable.ic_achievement_news 31L -> R.drawable.ic_achievement_vampire 32L -> R.drawable.ic_achievement_bored 33L, 39L -> R.drawable.ic_achievement_otaku 34L -> R.drawable.ic_achievement_pig 35L -> R.drawable.ic_achievement_airplane 36L -> R.drawable.ic_achievement_sad 37L -> R.drawable.ic_achievement_onepiece 38L -> R.drawable.ic_achievement_clock 40L -> R.drawable.ic_achievement_question 41L -> R.drawable.ic_achievement_cloud 42L -> R.drawable.ic_achievement_share 43L -> R.drawable.ic_achievement_midoriya 44L -> R.drawable.ic_achievement_memory 45L -> R.drawable.ic_achievement_evangelion in 46..51 -> R.drawable.ic_cash_multi else -> R.drawable.ic_umaru_simple } } fun backup(callback: () -> Unit = {}) = Backups.backup(id = Backups.keyAchievements) { callback() } fun restore(callback: () -> Unit = {}) { GlobalScope.launch(Dispatchers.IO) { val service = Backups.createService() if (service?.isLoggedIn == true) service.search(Backups.keyAchievements, true)?.let { backupObj -> CacheDB.INSTANCE.achievementsDAO().update((backupObj.data?.filterIsInstance() ?: arrayListOf()).filter { it.isUnlocked }) callback() } } } private fun updateCount(count: Int, keys: List) { doAsync { val list = achievementsDAO.find(keys) list.forEach { it.count = count } achievementsDAO.update(list) } } fun incrementCount(by: Int, keys: List) { doAsync { val list = achievementsDAO.find(keys) list.forEach { it.count += by } achievementsDAO.update(list) } } fun isUnlocked(key: Int): Boolean { return achievementsDAO.isUnlocked(key) } fun unlock(keys: Collection) { if (context.resources.getBoolean(R.bool.isTv)) return doAsync { val list = mutableListOf() keys.forEach { noCrash { if (!achievementsDAO.isUnlocked(it) && !isTV) { val achievement = achievementsDAO.find(it) if (achievement != null) { list.add(achievement.apply { isUnlocked = true time = System.currentTimeMillis() }) } } } } achievementsDAO.update(list) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context)) { val achievementList = mutableListOf() list.forEach { achievementList.add(it.achievementData(context)) } resetIndicator() doOnUIGlobal { achievementUnlocked.show(achievementList) } } else for (achievement in list) Toaster.toast("${achievement.name} | Desbloqueado") } } fun onAppStart() { doAsync { val list = mutableListOf() list.add(0) if (PrefsUtil.firstStart == 0L) PrefsUtil.firstStart = System.currentTimeMillis() val time = (System.currentTimeMillis() - PrefsUtil.firstStart) if (time >= 7776000000) list.add(8) if (time >= 15552000000) list.add(9) if (time >= 31104000000) list.add(10) if (System.currentTimeMillis() - PrefsUtil.lastStart >= 604800000) list.add(36) PrefsUtil.lastStart = System.currentTimeMillis() if (EADB.INSTANCE.eaDAO().isUnlocked(0)) list.add(12) if (EADB.INSTANCE.eaDAO().isUnlocked(1)) list.add(13) if (EADB.INSTANCE.eaDAO().isUnlocked(2)) list.add(14) if (EADB.INSTANCE.eaDAO().isUnlocked(3)) list.add(15) if (EAHelper.isPart0Unlocked && EAHelper.isPart1Unlocked && EAHelper.isPart2Unlocked && EAHelper.isPart3Unlocked) list.add(28) unlock(list) } } fun onPhaseUnlocked(phase: Int) { when (phase) { 0 -> unlock(listOf(12)) 1 -> unlock(listOf(13)) 2 -> unlock(listOf(14)) 3 -> unlock(listOf(15)) } if (EAHelper.isAllUnlocked) unlock(listOf(28)) } fun onNewsOpened() { incrementCount(1, listOf(30)) } fun onBackup() { unlock(listOf(41)) } fun onShare() { incrementCount(1, listOf(42)) } fun onSearch(query: String) { when (query.lowercase(Locale.getDefault())) { "boku no hero" -> unlock(listOf(43)) } } fun onRecordsOpened() { incrementCount(1, listOf(44)) } fun onFavAdded(fav: FavoriteObject) { doAsync { if (CacheDB.INSTANCE.animeDAO().hasGenre(fav.aid, "Ecchi".like)) incrementCount(1, listOf(34)) if (CacheDB.INSTANCE.animeDAO().hasGenre(fav.aid, "Shounen".like)) unlock(listOf(37)) } } private val String.like: String get() = "%$this%" fun onPlayQueue(count: Int) { if (count == 0) return incrementCount(count - 1, listOf(33)) onPlayChapter() } fun onPlayChapter() { doAsync { noCrash { val batteryStatus = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) val batteryLevel = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 val batteryScale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1 val batteryPct = batteryLevel / batteryScale.toFloat() val isLivingAtLimit = (batteryPct * 100).toInt() <= 10 && batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) != BatteryManager.BATTERY_STATUS_CHARGING && batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) != BatteryManager.BATTERY_STATUS_FULL if (isLivingAtLimit) unlock(listOf(29)) val timeFormat = SimpleDateFormat("HH", Locale.getDefault()) val current = timeFormat.format(Calendar.getInstance().time).toInt() if (current in 0..3) unlock(listOf(31)) }.toast() } } } ================================================ FILE: app/src/main/java/knf/kuma/achievements/AchievementsFragmentsPagerAdapter.kt ================================================ package knf.kuma.achievements import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter class AchievementsFragmentsPagerAdapter(fragmentManager: FragmentManager, private val onClick: OnClick) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getItem(position: Int): Fragment = AchievementFragment.get(if (position == 0) 1 else 0, onClick) override fun getCount(): Int { return 2 } override fun getPageTitle(position: Int): CharSequence? { return when (position) { 1 -> "Bloqueados" else -> "Desbloqueados" } } } ================================================ FILE: app/src/main/java/knf/kuma/achievements/AchievementsPagerAdapter.kt ================================================ package knf.kuma.achievements import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.viewpager.widget.PagerAdapter class AchievementsPagerAdapter(private val fragmentManager: FragmentManager, private val onClick: OnClick) : PagerAdapter() { private val fragments = arrayOfNulls(2) override fun destroyItem(container: ViewGroup, position: Int, any: Any) { assert(0 <= position && position < fragments.size) val fragment = fragments[position] fragment?.let { val transaction = fragmentManager.beginTransaction() transaction.remove(it) transaction.commit() fragments[position] = null } } override fun instantiateItem(container: ViewGroup, position: Int): Any { val fragment = getItem(position) fragment?.let { val transaction = fragmentManager.beginTransaction() transaction.add(container.id, fragment, "fragment:$position") transaction.commit() } return fragment ?: Any() } override fun isViewFromObject(view: View, any: Any): Boolean { return (any as? Fragment)?.view == view } private fun getItem(position: Int): Fragment? { assert(0 <= position && position < fragments.size) if (fragments[position] == null) fragments[position] = AchievementFragment.get(if (position == 0) 1 else 0, onClick) return fragments[position] } override fun getCount(): Int { return 2 } override fun getPageTitle(position: Int): CharSequence? { return when (position) { 1 -> "Bloqueados" else -> "Desbloqueados" } } } ================================================ FILE: app/src/main/java/knf/kuma/achievements/LevelCalculator.kt ================================================ package knf.kuma.achievements class LevelCalculator { var level: Int = 0 var toLvlUp: Int = 400 var progress: Int = 0 var max: Int = 400 lateinit var levels: List init { createLevels() } fun calculate(points: Int) { if (points < 400) { level = 0 toLvlUp = 400 - points progress = points max = 400 } else { var index = 0 for (value in levels) { if (points < value) { level = index max = value - (400 * index) progress = points - levels[index - 1] toLvlUp = /*max - progress*/ value - points break } index++ } } } private fun createLevels() { val lvls = mutableListOf() var last = 0 for (i in 1..60) { val xp = (last + (400 + (175 * (i - 1)))).also { last = it } lvls.add(xp) } levels = lvls } } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdAnimeObject.kt ================================================ package knf.kuma.ads import knf.kuma.pojos.AnimeObject data class AdAnimeObject(private val adID: String) : AnimeObject(), AdCallback { override fun getID(): String { return adID } } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdCallback.kt ================================================ package knf.kuma.ads interface AdCallback { fun getID(): String } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdCardItemHolder.kt ================================================ package knf.kuma.ads import android.view.LayoutInflater import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.annotation.UiThread import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.find class AdCardItemHolder(parent: ViewGroup, @LayoutRes type: Int = TYPE_NORMAL) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(type, parent, false)) { private val container = itemView.find(R.id.container) @UiThread fun loadAd(ad: AdCallback?) { if (ad == null) return container.implBanner(ad.getID(), true) } fun loadAd(scope: CoroutineScope, ad: AdCallback?, delayed: Long = 0) { if (ad == null) return scope.launch(Dispatchers.IO) { delay(delayed) container.implBanner(ad.getID(), true) } } companion object { const val TYPE_NORMAL = R.layout.item_ad const val TYPE_FAV = R.layout.item_ad_fav const val TYPE_ACHIEVEMENT = R.layout.item_ad_achievements const val TYPE_NEWS = R.layout.item_ad_news } } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdFavoriteObject.kt ================================================ package knf.kuma.ads import knf.kuma.pojos.FavoriteObject data class AdFavoriteObject(private val adID: String) : FavoriteObject(), AdCallback { override fun getID(): String { return adID } } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdRecentObject.kt ================================================ package knf.kuma.ads import knf.kuma.pojos.RecentObject data class AdRecentObject(private val adID: String) : RecentObject(), AdCallback { override fun getID(): String { return adID } } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdsUtils.kt ================================================ package knf.kuma.ads import android.app.Activity import android.util.Log import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings import knf.kuma.BuildConfig import knf.kuma.commons.PrefsUtil import knf.kuma.commons.diceOf import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashLet import knf.kuma.news.NewsObject import knf.kuma.pojos.Achievement import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.RecentObject import org.nield.kotlinstatistics.weightedCoinFlip enum class AdsType { RECENT_BANNER, RECENT_BANNER2, FAVORITE_BANNER, FAVORITE_BANNER2, DIRECTORY_BANNER, HOME_BANNER, HOME_BANNER2, EMISSION_BANNER, SEEING_BANNER, RECOMMEND_BANNER, QUEUE_BANNER, RECORD_BANNER, NEWS_BANNER, RANDOM_BANNER, INFO_BANNER, ACHIEVEMENT_BANNER, EXPLORER_BANNER, CAST_BANNER, REWARDED, INTERSTITIAL } object AdsUtils { val remoteConfigs = FirebaseRemoteConfig.getInstance().apply { if (BuildConfig.DEBUG) setConfigSettingsAsync( FirebaseRemoteConfigSettings.Builder().apply { minimumFetchIntervalInSeconds = 0 } .build() ) setDefaultsAsync( mapOf( "admob_enabled" to false, "appbrains_enabled" to false, "startapp_enabled" to false, "appodeal_enabled" to false, "applovin_enabled" to true, "admob_use_fallback" to false, "ads_forced" to false, "ads_remote_banner" to true, "ads_remote_full" to true, "ads_remote" to 1.0, "admob_percent" to 100.0, "appodeal_percent" to 100.0, "applovin_percent" to 100.0, "appbrains_percent" to 0.0, "startapp_percent" to 0.0, "appodeal_fullscreen_percent" to 100.0, "applovin_fullscreen_percent" to 100.0, "admob_fullscreen_percent" to 100.0, "appbrains_fullscreen_percent" to 100.0, "startappp_fullscreen_percent" to 100.0, "appodeal_fullscreen_percent" to 100.0, "rewarded_percent" to 10.0, "interstitial_percent" to 90.0, "disqus_version" to "9e3da5ae8d7caf8389087c4c35a6ca1b", "min_version" to 169L, "samsung_disable_foreground" to false, "bypass_show_reload" to false, "bypass_clear_cookies" to false, "bypass_max_tries" to 3L, "bypass_skip_captcha" to true, "bypass_use_dialog" to true, "bypass_dialog_style" to 1L, "full_show_extra_probability" to 60.0, "full_show_probability" to 70.0 ) ) fetch().addOnCompleteListener { it.exception?.printStackTrace() if (it.isSuccessful) { FirebaseRemoteConfig.getInstance().activate() } } } val isRemoteAdsEnabled by lazy { weightedCoinFlip(remoteConfigs.getDouble("ads_remote")) } val isRemoteBannerEnabled get() = remoteConfigs.getBoolean("ads_remote_banner") val isRemoteFullEnabled get() = remoteConfigs.getBoolean("ads_remote_full") val isAdmobEnabled get() = remoteConfigs.getBoolean("admob_enabled") val isApplovinEnabled get() = remoteConfigs.getBoolean("applovin_enabled") fun setUp(context: Activity, callback: () -> Unit) { if (!isRemoteAdsEnabled || !listOf(isAdmobEnabled, isApplovinEnabled).any { it }) { Log.e("ADS", "All disabled") callback() return } if (isApplovinEnabled) { Log.e("ADS", "Applovin enabled") AdsUtilsLovin.setUp(context, callback) } if (isAdmobEnabled) { Log.e("ADS", "Admob enabled") AdsUtilsMob.setUp(context, callback) } } } fun MutableList.implAdsRecent() { if (AdsUtils.isRemoteAdsEnabled && AdsUtils.isRemoteBannerEnabled && PrefsUtil.isAdsEnabled) noCrash { diceOf({ implAdsRecentBrains() }) { if (AdsUtils.isAdmobEnabled) put({ implAdsRecentMob() }, AdsUtils.remoteConfigs.getDouble("admob_percent")) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put({ implAdsRecentBrains() }, AdsUtils.remoteConfigs.getDouble("appbrains_percent")) if (AdsUtils.isApplovinEnabled) put({ implAdsRecentLovin() }, AdsUtils.remoteConfigs.getDouble("applovin_percent")) }() } } fun MutableList.implAdsFavorite() { if (AdsUtils.isRemoteAdsEnabled && AdsUtils.isRemoteBannerEnabled && PrefsUtil.isAdsEnabled) noCrash { diceOf({ implAdsFavoriteBrains() }) { if (AdsUtils.isAdmobEnabled) put({ implAdsFavoriteMob() }, AdsUtils.remoteConfigs.getDouble("admob_percent")) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put({ implAdsFavoriteBrains() }, AdsUtils.remoteConfigs.getDouble("appbrains_percent")) if (AdsUtils.isApplovinEnabled) put({ implAdsFavoriteLovin() }, AdsUtils.remoteConfigs.getDouble("applovin_percent")) }() } } fun MutableList.implAdsNews() { if (AdsUtils.isRemoteAdsEnabled && AdsUtils.isRemoteBannerEnabled && PrefsUtil.isAdsEnabled) noCrash { diceOf({ implAdsNewsBrain() }) { if (AdsUtils.remoteConfigs.getBoolean("admob_enabled")) put({ implAdsNewsMob() }, AdsUtils.remoteConfigs.getDouble("admob_percent")) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put({ implAdsNewsBrain() }, AdsUtils.remoteConfigs.getDouble("appbrains_percent")) if (AdsUtils.isApplovinEnabled) put({ implAdsNewsLovin() }, AdsUtils.remoteConfigs.getDouble("applovin_percent")) }() } } fun MutableList.implAdsAchievement() { if (AdsUtils.isRemoteAdsEnabled && AdsUtils.isRemoteBannerEnabled && PrefsUtil.isAdsEnabled) noCrash { diceOf({ implAdsAchievementBrain() }) { if (AdsUtils.remoteConfigs.getBoolean("admob_enabled")) put({ implAdsAchievementMob() }, AdsUtils.remoteConfigs.getDouble("admob_percent")) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put({ implAdsAchievementBrain() }, AdsUtils.remoteConfigs.getDouble("appbrains_percent")) if (AdsUtils.isApplovinEnabled) put({ implAdsAchievementLovin() }, AdsUtils.remoteConfigs.getDouble("applovin_percent")) }() } } fun ViewGroup.implBannerCast() { if (AdsUtils.isRemoteAdsEnabled && AdsUtils.isRemoteBannerEnabled && PrefsUtil.isAdsEnabled) noCrash { diceOf({ implBannerCastBrains() }) { if (AdsUtils.remoteConfigs.getBoolean("admob_enabled")) put({ implBannerCastMob() }, AdsUtils.remoteConfigs.getDouble("admob_percent")) if (AdsUtils.remoteConfigs.getBoolean("applovin_enabled")) put({ implBannerCastLovin() }, AdsUtils.remoteConfigs.getDouble("applovin_percent")) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put({ implBannerCastBrains() }, AdsUtils.remoteConfigs.getDouble("appbrains_percent")) }() } } fun ViewGroup.implBanner(unitID: String, isSmart: Boolean = false) { if (AdsUtils.isRemoteAdsEnabled && AdsUtils.isRemoteBannerEnabled && PrefsUtil.isAdsEnabled) noCrash { diceOf({ implBannerBrains(unitID, isSmart) }) { if (AdsUtils.isAdmobEnabled) put( { implBannerMob(unitID, isSmart) }, AdsUtils.remoteConfigs.getDouble("admob_percent") ) if (AdsUtils.isApplovinEnabled) put( { implBannerLovin(AdsType.RECENT_BANNER) }, AdsUtils.remoteConfigs.getDouble("applovin_percent") ) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put( { implBannerBrains(unitID, isSmart) }, AdsUtils.remoteConfigs.getDouble("appbrains_percent") ) }() } } fun ViewGroup.implBanner(unitID: AdsType, isSmart: Boolean = false) { if (AdsUtils.isRemoteAdsEnabled && AdsUtils.isRemoteBannerEnabled && PrefsUtil.isAdsEnabled) noCrash { diceOf({ implBannerBrains(unitID, isSmart) }) { if (AdsUtils.isAdmobEnabled) put( { implBannerMob(unitID, isSmart) }, AdsUtils.remoteConfigs.getDouble("admob_percent") ) if (AdsUtils.isApplovinEnabled) put( { implBannerLovin(unitID) }, AdsUtils.remoteConfigs.getDouble("applovin_percent") ) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put( { implBannerBrains(unitID, isSmart) }, AdsUtils.remoteConfigs.getDouble("appbrains_percent") ) }() } } fun getFAdLoaderRewarded(context: Activity, onUpdate: () -> Unit = {}): FullscreenAdLoader = noCrashLet(getFAdLoaderBrains(context, onUpdate)) { diceOf({ getFAdLoaderBrains(context, onUpdate) }) { if (AdsUtils.isAdmobEnabled) put( { getFAdLoaderRewardedMob(context, onUpdate) }, AdsUtils.remoteConfigs.getDouble("admob_fullscreen_percent") ) if (AdsUtils.isApplovinEnabled) put( { getFAdLoaderRewardedLovin(context, onUpdate) }, AdsUtils.remoteConfigs.getDouble("applovin_fullscreen_percent") ) }() } fun getFAdLoaderInterstitial(context: Activity, onUpdate: () -> Unit = {}): FullscreenAdLoader = noCrashLet(getFAdLoaderBrains(context, onUpdate)) { diceOf({ getFAdLoaderBrains(context, onUpdate) }) { if (AdsUtils.isAdmobEnabled) put( { getFAdLoaderInterstitialMob(context, onUpdate) }, AdsUtils.remoteConfigs.getDouble("admob_fullscreen_percent") ) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put( { getFAdLoaderBrains(context, onUpdate) }, AdsUtils.remoteConfigs.getDouble("appbrains_fullscreen_percent") ) if (AdsUtils.isApplovinEnabled) put( { getFAdLoaderInterstitialLovin(context, onUpdate) }, AdsUtils.remoteConfigs.getDouble("applovin_fullscreen_percent") ) }() } fun showRandomInterstitial( context: AppCompatActivity, probability: Float = PrefsUtil.fullAdsProbability ) { if (AdsUtils.isRemoteAdsEnabled && PrefsUtil.isAdsEnabled && PrefsUtil.isFullAdsEnabled && probability > 0) { val probDefault = 100f - probability diceOf<() -> Unit> { if (AdsUtils.isAdmobEnabled) put( { FAdLoaderInterstitialLazyMob(context).show() }, probability.toDouble() ) if (AdsUtils.remoteConfigs.getBoolean("appbrains_enabled")) put( { FAdLoaderInterstitialLazyBrains(context).show() }, probability.toDouble() ) if (AdsUtils.isApplovinEnabled) put( { FAdLoaderInterstitialLazyLovin(context).show() }, probability.toDouble() ) put({}, probDefault.toDouble()) }() } } interface FullscreenAdLoader { fun load() fun show() } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdsUtilsBrains.kt ================================================ package knf.kuma.ads //import com.appbrain.* import android.content.Context import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.news.AdNewsObject import knf.kuma.news.NewsObject import knf.kuma.pojos.Achievement import knf.kuma.pojos.AchievementAd import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.RecentObject import xdroid.toaster.Toaster object AdsUtilsBrains { const val RECENT_BANNER = "recent_banner" const val RECENT_BANNER2 = "recent_banner_2" const val FAVORITE_BANNER = "favorite_banner" const val FAVORITE_BANNER2 = "favorite_banner_2" const val DIRECTORY_BANNER = "directory_banner" const val HOME_BANNER = "home_banner" const val HOME_BANNER2 = "home_banner_2" const val EMISSION_BANNER = "emission_banner" const val SEEING_BANNER = "seeing_banner" const val RECOMMEND_BANNER = "recommend_banner" const val QUEUE_BANNER = "queue_banner" const val RECORD_BANNER = "record_banner" const val RANDOM_BANNER = "random_banner" const val NEWS_BANNER = "news_banner" const val INFO_BANNER = "info_banner" const val ACHIEVEMENT_BANNER = "achievement_banner" const val EXPLORER_BANNER = "explorer_banner" const val CAST_BANNER = "cast_banner" const val REWARDED = "rewarded" const val INTERSTITIAL = "interstitial" } fun MutableList.implAdsRecentBrains() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 ArrayList(this).forEachIndexed { index, _ -> if (index % 5 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsBrains.RECENT_BANNER } else -> { adIndex = 0 AdsUtilsBrains.RECENT_BANNER2 } } add(index, AdRecentObject(adID)) } } } fun MutableList.implAdsFavoriteBrains() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 ArrayList(this).apply { forEachIndexed { index, _ -> if (index % 8 == 0 && index > 0 && !this[index - 1].isSection) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsBrains.FAVORITE_BANNER } else -> { adIndex = 0 AdsUtilsBrains.FAVORITE_BANNER2 } } this@implAdsFavoriteBrains.add(index, AdFavoriteObject(adID)) } } } } fun MutableList.implAdsNewsBrain() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 ArrayList(this).forEachIndexed { index, _ -> if (index % 5 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsBrains.NEWS_BANNER } else -> { adIndex = 0 AdsUtilsBrains.NEWS_BANNER } } add(index, AdNewsObject(adID)) } } } fun MutableList.implAdsAchievementBrain() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 ArrayList(this).forEachIndexed { index, _ -> if (index % 8 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsBrains.ACHIEVEMENT_BANNER } else -> { adIndex = 0 AdsUtilsBrains.ACHIEVEMENT_BANNER } } add(index, AchievementAd(adID)) } } } fun ViewGroup.implBannerCastBrains() { this.implBannerBrains(AdsUtilsBrains.CAST_BANNER) } fun ViewGroup.implBannerBrains(unitID: AdsType, isSmart: Boolean = false) { val id = when (unitID) { AdsType.RECENT_BANNER -> AdsUtilsBrains.RECENT_BANNER AdsType.RECENT_BANNER2 -> AdsUtilsBrains.RECENT_BANNER2 AdsType.FAVORITE_BANNER -> AdsUtilsBrains.FAVORITE_BANNER AdsType.FAVORITE_BANNER2 -> AdsUtilsBrains.FAVORITE_BANNER2 AdsType.DIRECTORY_BANNER -> AdsUtilsBrains.DIRECTORY_BANNER AdsType.HOME_BANNER -> AdsUtilsBrains.HOME_BANNER AdsType.HOME_BANNER2 -> AdsUtilsBrains.HOME_BANNER2 AdsType.EMISSION_BANNER -> AdsUtilsBrains.EMISSION_BANNER AdsType.SEEING_BANNER -> AdsUtilsBrains.SEEING_BANNER AdsType.RECOMMEND_BANNER -> AdsUtilsBrains.RECOMMEND_BANNER AdsType.QUEUE_BANNER -> AdsUtilsBrains.QUEUE_BANNER AdsType.RECORD_BANNER -> AdsUtilsBrains.RECORD_BANNER AdsType.RANDOM_BANNER -> AdsUtilsBrains.RANDOM_BANNER AdsType.NEWS_BANNER -> AdsUtilsBrains.NEWS_BANNER AdsType.INFO_BANNER -> AdsUtilsBrains.INFO_BANNER AdsType.ACHIEVEMENT_BANNER -> AdsUtilsBrains.ACHIEVEMENT_BANNER AdsType.EXPLORER_BANNER -> AdsUtilsBrains.EXPLORER_BANNER AdsType.CAST_BANNER -> AdsUtilsBrains.CAST_BANNER AdsType.REWARDED -> AdsUtilsBrains.REWARDED AdsType.INTERSTITIAL -> AdsUtilsBrains.INTERSTITIAL } implBannerBrains(id, isSmart) } fun ViewGroup.implBannerBrains(unitID: String, isSmart: Boolean = false) { } fun getFAdLoaderBrains(context: Context, onUpdate: () -> Unit): FullscreenAdLoader = FAdLoaderBrains(context, onUpdate) class FAdLoaderBrains(private val context: Context, onUpdate: () -> Unit) : FullscreenAdLoader { var isAdClicked = false /*private val builder: InterstitialBuilder by lazy { InterstitialBuilder.create().apply { adId = AdId.DEFAULT setOnDoneCallback { builder.preload(context) } listener = object : InterstitialListener { override fun onClick() { FirebaseAnalytics.getInstance(App.context).logEvent("Interstitial_Ad_clicked", Bundle()) isAdClicked = true } override fun onDismissed(p0: Boolean) { FirebaseAnalytics.getInstance(App.context).logEvent("Interstitial_Ad_watched", Bundle()) Economy.reward(isAdClicked) onUpdate() } override fun onAdFailedToLoad(p0: InterstitialListener.InterstitialError?) { } override fun onPresented() { } override fun onAdLoaded() { } } }.also { it.preload(context) } }*/ override fun load() { ///builder.preload(context) } override fun show() { if (Network.isAdsBlocked) Toaster.toast("Anuncios bloqueados por host") /*else builder.show(context)*/ } } class FAdLoaderInterstitialLazyBrains(val context: AppCompatActivity) : FullscreenAdLoader { var isAdClicked = false /*private val builder: InterstitialBuilder by lazy { InterstitialBuilder.create().apply { adId = AdId.DEFAULT setOnDoneCallback { builder.preload(context) } listener = object : InterstitialListener { override fun onClick() { FirebaseAnalytics.getInstance(App.context).logEvent("Interstitial_Ad_clicked", Bundle()) isAdClicked = true } override fun onDismissed(p0: Boolean) { FirebaseAnalytics.getInstance(App.context).logEvent("Interstitial_Ad_watched", Bundle()) Economy.reward(isAdClicked) } override fun onAdFailedToLoad(p0: InterstitialListener.InterstitialError?) { } override fun onPresented() { } override fun onAdLoaded() { } } } }*/ init { load() } override fun load() { //builder.preload(context) } override fun show() { if (Network.isAdsBlocked) Toaster.toast("Anuncios bloqueados por host") } } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdsUtilsLovin.kt ================================================ package knf.kuma.ads import android.app.Activity import android.view.ViewGroup import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.applovin.mediation.MaxAd import com.applovin.mediation.MaxAdListener import com.applovin.mediation.MaxError import com.applovin.mediation.MaxReward import com.applovin.mediation.MaxRewardedAdListener import com.applovin.mediation.ads.MaxAdView import com.applovin.mediation.ads.MaxInterstitialAd import com.applovin.mediation.ads.MaxRewardedAd import com.applovin.sdk.AppLovinMediationProvider import com.applovin.sdk.AppLovinPrivacySettings import com.applovin.sdk.AppLovinSdk import com.applovin.sdk.AppLovinSdkInitializationConfiguration import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.commons.Economy import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.asPx import knf.kuma.custom.BannerContainerView import knf.kuma.news.AdNewsObject import knf.kuma.news.NewsObject import knf.kuma.pojos.Achievement import knf.kuma.pojos.AchievementAd import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.RecentObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.collections.forEachReversedWithIndex import xdroid.toaster.Toaster.toast object AdsUtilsLovin { fun setUp(context: Activity, callback: () -> Unit) { val initConfig = AppLovinSdkInitializationConfiguration.builder("QHQI9Sl_Fltmz6OzT9WBg6sTUG3SlJOaLf6E7G4xMGsOake13NQHoHFK6dAUnG0u_18dllB1Q7mGheTwmEl8AD") .setMediationProvider(AppLovinMediationProvider.MAX) .build() AppLovinSdk.getInstance(context).initialize(initConfig) { if (!AppLovinPrivacySettings.hasUserConsent()) { AppLovinPrivacySettings.setHasUserConsent(true) AppLovinPrivacySettings.setDoNotSell(false) } callback() } } } fun MutableList.implAdsRecentLovin() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 5 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.RECENT_BANNER } else -> { adIndex = 0 AdsUtilsMob.RECENT_BANNER } } add(index, AdRecentObject(adID)) } } add(0, AdRecentObject(AdsUtilsMob.RECENT_BANNER)) } fun MutableList.implAdsFavoriteLovin() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 8 == 0 && index > 0 && !this[index - 1].isSection) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.FAVORITE_BANNER } else -> { adIndex = 0 AdsUtilsMob.FAVORITE_BANNER } } this@implAdsFavoriteLovin.add(index, AdFavoriteObject(adID)) } } this@implAdsFavoriteLovin.add(0, AdFavoriteObject(AdsUtilsMob.FAVORITE_BANNER)) } fun MutableList.implAdsNewsLovin() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 5 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.NEWS_BANNER } else -> { adIndex = 0 AdsUtilsMob.NEWS_BANNER } } add(index, AdNewsObject(adID)) } } add(0, AdNewsObject(AdsUtilsMob.NEWS_BANNER)) } fun MutableList.implAdsAchievementLovin() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 8 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.ACHIEVEMENT_BANNER } else -> { adIndex = 0 AdsUtilsMob.ACHIEVEMENT_BANNER } } add(index, AchievementAd(adID)) } } add(0, AchievementAd(AdsUtilsMob.ACHIEVEMENT_BANNER)) } fun ViewGroup.implBannerCastLovin() { this.implBannerMob(AdsUtilsMob.CAST_BANNER) } fun ViewGroup.implBannerLovin(unitID: AdsType, isSmart: Boolean = false) { implBannerLovin() } fun ViewGroup.implBannerLovin() { if (PrefsUtil.isAdsEnabled) { GlobalScope.launch g@{ if (this@implBannerLovin.tag == "AdView added") return@g if (this@implBannerLovin !is BannerContainerView) { GlobalScope.launch { val adView = MaxAdView("91d782c7eb7efc75") adView.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 50.asPx) adView.setBackgroundColor(ContextCompat.getColor(App.context, R.color.cardview_background)) launch(Dispatchers.Main) { addView(adView) this@implBannerLovin.tag = "AdView added" adView.loadAd() } } } else { val adView = MaxAdView("91d782c7eb7efc75") adView.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 50.asPx) adView.setBackgroundColor(ContextCompat.getColor(App.context, R.color.cardview_background)) launch(Dispatchers.Main) { show(adView) adView.loadAd() this@implBannerLovin.tag = "AdView added" } } } } } fun getFAdLoaderInterstitialLovin(context: Activity, onUpdate: () -> Unit): FullscreenAdLoader = FAdLoaderInterstitialLovin(context, onUpdate) fun getFAdLoaderRewardedLovin(context: Activity, onUpdate: () -> Unit): FullscreenAdLoader = FAdLoaderRewardedLovin(context, onUpdate) class FAdLoaderInterstitialLovin(val context: Activity, private val onUpdate: () -> Unit) : FullscreenAdLoader { private var interstitialAd: MaxInterstitialAd = MaxInterstitialAd("e5f776a3ccb9282e") private var isLoading = false init { isLoading = true interstitialAd.setListener(object : MaxAdListener { var isClicked = false override fun onAdLoaded(p0: MaxAd) { isLoading = false } override fun onAdLoadFailed(p0: String, p1: MaxError) { GlobalScope.launch { delay(2000) interstitialAd.loadAd() } } override fun onAdHidden(p0: MaxAd) { isLoading = true interstitialAd.loadAd() Economy.reward(isClicked) isClicked = false onUpdate() } override fun onAdDisplayed(p0: MaxAd) {} override fun onAdClicked(p0: MaxAd) { isClicked = true } override fun onAdDisplayFailed(p0: MaxAd, p1: MaxError) {} }) interstitialAd.loadAd() } override fun load() { if (!isLoading && !interstitialAd.isReady) { isLoading = true interstitialAd.loadAd() } } override fun show() { when { !AdsUtils.isRemoteAdsEnabled || !AdsUtils.isRemoteFullEnabled || BuildConfig.DEBUG -> return interstitialAd.isReady -> { interstitialAd.showAd(context) } Network.isAdsBlocked -> toast("Anuncios bloqueados por host") else -> toast("Anuncio aún cargando...") } } } class FAdLoaderRewardedLovin(val context: Activity, private val onUpdate: () -> Unit) : FullscreenAdLoader { private var rewardedAd: MaxRewardedAd = MaxRewardedAd.getInstance("e3b2506478ae074c") private var isLoading = false init { isLoading = true rewardedAd.setListener(object : MaxRewardedAdListener { var isClicked = false override fun onAdLoaded(p0: MaxAd) { isLoading = false } override fun onAdDisplayed(p0: MaxAd) {} override fun onAdHidden(p0: MaxAd) { isClicked = false isLoading = true rewardedAd.loadAd() onUpdate() } override fun onAdClicked(p0: MaxAd) { isClicked = true } override fun onAdLoadFailed(p0: String, p1: MaxError) {} override fun onAdDisplayFailed(p0: MaxAd, p1: MaxError) {} override fun onUserRewarded(p0: MaxAd, p1: MaxReward) { Economy.reward(isClicked) } }) rewardedAd.loadAd() } override fun load() { if (!isLoading && !rewardedAd.isReady) { isLoading = true rewardedAd.loadAd() } } override fun show() { when { !AdsUtils.isRemoteAdsEnabled || !AdsUtils.isRemoteFullEnabled || BuildConfig.DEBUG -> return rewardedAd.isReady -> { rewardedAd.showAd(context) } Network.isAdsBlocked -> toast("Anuncios bloqueados por host") else -> toast("Anuncio aún cargando...") } } } class FAdLoaderInterstitialLazyLovin(val context: AppCompatActivity) : FullscreenAdLoader { private var interstitialAd: MaxInterstitialAd = MaxInterstitialAd("e5f776a3ccb9282e") private var isLoading = false init { isLoading = true interstitialAd.setListener(object : MaxAdListener { var isClicked = false override fun onAdLoaded(p0: MaxAd) { isLoading = false } override fun onAdLoadFailed(p0: String, p1: MaxError) { GlobalScope.launch { delay(2000) interstitialAd.loadAd() } } override fun onAdHidden(p0: MaxAd) { isLoading = true interstitialAd.loadAd() Economy.reward(isClicked) isClicked = false } override fun onAdDisplayed(p0: MaxAd) {} override fun onAdClicked(p0: MaxAd) { isClicked = true } override fun onAdDisplayFailed(p0: MaxAd, p1: MaxError) {} }) interstitialAd.loadAd() } override fun load() { if (!isLoading && !interstitialAd.isReady) { isLoading = true interstitialAd.loadAd() } } override fun show() { when { !AdsUtils.isRemoteAdsEnabled || !AdsUtils.isRemoteFullEnabled || BuildConfig.DEBUG -> return interstitialAd.isReady -> { interstitialAd.showAd(context) } Network.isAdsBlocked -> toast("Anuncios bloqueados por host") else -> context.lifecycleScope.launch(Dispatchers.Main) { var tryCount = 11 while (!interstitialAd.isReady && tryCount > 0) { delay(250) tryCount-- } if (interstitialAd.isReady) show() } } } } ================================================ FILE: app/src/main/java/knf/kuma/ads/AdsUtilsMob.kt ================================================ package knf.kuma.ads /*import com.google.android.gms.ads.AdListener import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.AdSize import com.google.android.gms.ads.AdView import com.google.android.gms.ads.FullScreenContentCallback import com.google.android.gms.ads.LoadAdError import com.google.android.gms.ads.MobileAds import com.google.android.gms.ads.RequestConfiguration import com.google.android.gms.ads.interstitial.InterstitialAd import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback import com.google.android.gms.ads.rewarded.RewardedAd import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback*/ import android.app.Activity import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import knf.kuma.BuildConfig import knf.kuma.commons.PrefsUtil import knf.kuma.news.AdNewsObject import knf.kuma.news.NewsObject import knf.kuma.pojos.Achievement import knf.kuma.pojos.AchievementAd import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.RecentObject import org.jetbrains.anko.collections.forEachReversedWithIndex object AdsUtilsMob { val RECENT_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.RECENT_BANNER val RECENT_BANNER2 get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.RECENT_BANNER2 val FAVORITE_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.FAVORITE_BANNER val FAVORITE_BANNER2 get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.FAVORITE_BANNER2 val DIRECTORY_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.DIRECTORY_BANNER val HOME_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.HOME_BANNER val HOME_BANNER2 get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.HOME_BANNER2 val EMISSION_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.EMISSION_BANNER val SEEING_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.SEEING_BANNER val RECOMMEND_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.RECOMMEND_BANNER val QUEUE_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.QUEUE_BANNER val RECORD_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.RECORD_BANNER val RANDOM_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.RANDOM_BANNER val NEWS_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.NEWS_BANNER val INFO_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.INFO_BANNER val ACHIEVEMENT_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.ACHIEVEMENT_BANNER val EXPLORER_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.EXPLORER_BANNER val CAST_BANNER get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/6300978111" else AdmobID.CAST_BANNER val LIST_NATIVE get() = if (BuildConfig.DEBUG) "ca-app-pub-5390653757953587/5447863415" else AdmobID.LIST_NATIVE val REWARDED get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/5224354917" else AdmobID.REWARDED val INTERSTITIAL get() = if (BuildConfig.DEBUG) "ca-app-pub-3940256099942544/1033173712" else AdmobID.INTERSTITIAL //val adRequest: AdRequest get() = AdRequest.Builder().build() val ACHIEVEMENT_NATIVE = "achievement_native" fun setUp(context: Activity, callback: () -> Unit) { /*MobileAds.initialize(context) { NativeManager callback() } if (!BuildConfig.DEBUG) return val builder = RequestConfiguration.Builder().setTestDeviceIds(listOf("FE18DCEC5EE5755C3927E4EC30CD4F9D")) MobileAds.setRequestConfiguration(builder.build())*/ } } fun MutableList.implAdsRecentMob() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 5 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.RECENT_BANNER } else -> { adIndex = 0 AdsUtilsMob.RECENT_BANNER } } add(index, AdRecentObject(adID)) } } add(0, AdRecentObject(AdsUtilsMob.RECENT_BANNER)) } fun MutableList.implAdsFavoriteMob() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 8 == 0 && index > 0 && !this[index - 1].isSection) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.FAVORITE_BANNER } else -> { adIndex = 0 AdsUtilsMob.FAVORITE_BANNER } } this@implAdsFavoriteMob.add(index, AdFavoriteObject(adID)) } } this@implAdsFavoriteMob.add(0, AdFavoriteObject(AdsUtilsMob.FAVORITE_BANNER)) } fun MutableList.implAdsNewsMob() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 5 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.NEWS_BANNER } else -> { adIndex = 0 AdsUtilsMob.NEWS_BANNER } } add(index, AdNewsObject(adID)) } } add(0, AdNewsObject(AdsUtilsMob.NEWS_BANNER)) } fun MutableList.implAdsAchievementMob() { if (!PrefsUtil.isAdsEnabled || isEmpty()) return var adIndex = 0 forEachReversedWithIndex { index, _ -> if (index % 8 == 0 && index > 0) { val adID: String = when (adIndex) { 0 -> { adIndex = 1 AdsUtilsMob.ACHIEVEMENT_BANNER } else -> { adIndex = 0 AdsUtilsMob.ACHIEVEMENT_BANNER } } add(index, AchievementAd(adID)) } } add(0, AchievementAd(AdsUtilsMob.ACHIEVEMENT_BANNER)) } fun ViewGroup.implBannerCastMob() { this.implBannerMob(AdsUtilsMob.CAST_BANNER) } fun ViewGroup.implBannerMob(unitID: AdsType, isSmart: Boolean = false) { val id = when (unitID) { AdsType.RECENT_BANNER -> AdsUtilsMob.RECENT_BANNER AdsType.RECENT_BANNER2 -> AdsUtilsMob.RECENT_BANNER2 AdsType.FAVORITE_BANNER -> AdsUtilsMob.FAVORITE_BANNER AdsType.FAVORITE_BANNER2 -> AdsUtilsMob.FAVORITE_BANNER2 AdsType.DIRECTORY_BANNER -> AdsUtilsMob.DIRECTORY_BANNER AdsType.HOME_BANNER -> AdsUtilsMob.HOME_BANNER AdsType.HOME_BANNER2 -> AdsUtilsMob.HOME_BANNER2 AdsType.EMISSION_BANNER -> AdsUtilsMob.EMISSION_BANNER AdsType.SEEING_BANNER -> AdsUtilsMob.SEEING_BANNER AdsType.RECOMMEND_BANNER -> AdsUtilsMob.RECOMMEND_BANNER AdsType.QUEUE_BANNER -> AdsUtilsMob.QUEUE_BANNER AdsType.RECORD_BANNER -> AdsUtilsMob.RECORD_BANNER AdsType.RANDOM_BANNER -> AdsUtilsMob.RANDOM_BANNER AdsType.NEWS_BANNER -> AdsUtilsMob.NEWS_BANNER AdsType.INFO_BANNER -> AdsUtilsMob.INFO_BANNER AdsType.ACHIEVEMENT_BANNER -> AdsUtilsMob.ACHIEVEMENT_BANNER AdsType.EXPLORER_BANNER -> AdsUtilsMob.EXPLORER_BANNER AdsType.CAST_BANNER -> AdsUtilsMob.CAST_BANNER AdsType.REWARDED -> AdsUtilsMob.REWARDED AdsType.INTERSTITIAL -> AdsUtilsMob.INTERSTITIAL } implBannerMob(id, isSmart) } fun ViewGroup.implBannerMob(unitID: String, isSmart: Boolean = false) { /*if (PrefsUtil.isAdsEnabled) { GlobalScope.launch g@{ if (this@implBannerMob.tag == "AdView added") return@g if (this@implBannerMob !is BannerContainerView) { NativeManager.take(GlobalScope, 1) { if (it.isEmpty()) GlobalScope.launch { val adView = AdView(context) adView.setAdSize(getAdSize(width.toFloat())) adView.adUnitId = unitID adView.adListener = object : AbsAdListener() { override fun onAdClicked() { FirebaseAnalytics.getInstance(App.context).logEvent("Ad_clicked", Bundle()) } } launch(Dispatchers.Main) { addView(adView) this@implBannerMob.tag = "AdView added" adView.loadAd(AdsUtilsMob.adRequest) } } else GlobalScope.launch { val adView = when (unitID) { AdsUtilsMob.RECENT_BANNER, AdsUtilsMob.FAVORITE_BANNER -> { asyncInflate(context, R.layout.admob_ad_card).apply { Log.e("Ad", "On recent") find(R.id.admobAd).setNativeAd(it[0]) } } AdsUtilsMob.NEWS_BANNER -> { asyncInflate(context, R.layout.admob_ad_news).apply { Log.e("Ad", "On news") find(R.id.admobAd).setNativeAd(it[0]) } } AdsUtilsMob.ACHIEVEMENT_BANNER, AdsUtilsMob.ACHIEVEMENT_NATIVE -> { asyncInflate(context, R.layout.admob_ad_plain).apply { Log.e("Ad", "On Achievement") find(R.id.admobAd).setNativeAd(it[0]) } } AdsUtilsMob.CAST_BANNER -> { asyncInflate(context, R.layout.admob_ad_alone).apply { Log.e("Ad", "On Cast") find(R.id.admobAd).setNativeAd(it[0]) } } else -> return@launch } launch(Dispatchers.Main) { addView(adView) this@implBannerMob.tag = "AdView added" } } } } else { val adView = AdView(context) adView.setAdSize(getAdSize(width.toFloat())) adView.adUnitId = unitID adView.adListener = object : AbsAdListener() { override fun onAdClicked() { FirebaseAnalytics.getInstance(App.context).logEvent("Ad_clicked", Bundle()) } } launch(Dispatchers.Main) { show(adView) adView.loadAd(AdsUtilsMob.adRequest) this@implBannerMob.tag = "AdView added" } } } }*/ } fun getFAdLoaderRewardedMob(context: Activity, onUpdate: () -> Unit): FullscreenAdLoader = FAdLoaderRewardedMob(context, onUpdate) fun getFAdLoaderInterstitialMob(context: Activity, onUpdate: () -> Unit): FullscreenAdLoader = FAdLoaderInterstitialMob(context, onUpdate) class FAdLoaderRewardedMob(val context: Activity, private val onUpdate: () -> Unit) : FullscreenAdLoader { /*private var rewardedAd: RewardedAd? = null private fun createAndLoadRewardAd() { rewardedAd = null RewardedAd.load(context, AdsUtilsMob.REWARDED, AdsUtilsMob.adRequest, object : RewardedAdLoadCallback() { override fun onAdLoaded(p0: RewardedAd) { rewardedAd = p0 } override fun onAdFailedToLoad(p0: LoadAdError) { Log.e("Ad", "Ad failed to load, code: ${p0.code}") GlobalScope.launch(Dispatchers.Main) { delay(2000) if (!context.isFinishing) { createAndLoadRewardAd() } } } }) }*/ private fun showRewarded() { /*rewardedAd?.fullScreenContentCallback = object : FullScreenContentCallback() { override fun onAdDismissedFullScreenContent() { createAndLoadRewardAd() } } rewardedAd?.show(context) { FirebaseAnalytics.getInstance(App.context).logEvent("Rewarded_Ad_watched", Bundle()) Economy.reward(baseReward = 2) onUpdate() }*/ } override fun load() { //if (rewardedAd == null) createAndLoadRewardAd() } override fun show() { /*when { !AdsUtils.isRemoteAdsEnabled || !AdsUtils.isRemoteFullEnabled -> return rewardedAd != null -> showRewarded() Network.isAdsBlocked -> toast("Anuncios bloqueados por host") else -> toast("Anuncio aún cargando...") }*/ } } class FAdLoaderInterstitialMob(val context: Activity, private val onUpdate: () -> Unit) : FullscreenAdLoader { /*private var interstitialAd: InterstitialAd? = null private fun createAndLoad() { interstitialAd = null InterstitialAd.load(context, AdsUtilsMob.INTERSTITIAL, AdsUtilsMob.adRequest, object : InterstitialAdLoadCallback() { override fun onAdLoaded(p0: InterstitialAd) { interstitialAd = p0 } override fun onAdFailedToLoad(p0: LoadAdError) { Log.e("Ad", "Ad failed to load, code: ${p0.code}") GlobalScope.launch(Dispatchers.Main) { delay(2000) if (!context.isFinishing) { createAndLoad() } } } }) }*/ override fun load() { //if (interstitialAd == null) createAndLoad() } override fun show() { /* when { !AdsUtils.isRemoteAdsEnabled || !AdsUtils.isRemoteFullEnabled -> return interstitialAd != null -> { interstitialAd?.fullScreenContentCallback = object : FullScreenContentCallback() { override fun onAdDismissedFullScreenContent() { FirebaseAnalytics.getInstance(App.context).logEvent("Interstitial_Ad_watched", Bundle()) createAndLoad() Economy.reward(false) onUpdate() } } interstitialAd?.show(context) } Network.isAdsBlocked -> toast("Anuncios bloqueados por host") else -> toast("Anuncio aún cargando...") }*/ } } class FAdLoaderInterstitialLazyMob(val context: AppCompatActivity) : FullscreenAdLoader { //private var interstitialAd: InterstitialAd? = null init { //createAndLoad() } /*private fun createAndLoad() { interstitialAd = null InterstitialAd.load(context, AdsUtilsMob.INTERSTITIAL, AdsUtilsMob.adRequest, object : InterstitialAdLoadCallback() { override fun onAdLoaded(p0: InterstitialAd) { interstitialAd = p0 } override fun onAdFailedToLoad(p0: LoadAdError) { Log.e("Ad", "Ad failed to load, code: ${p0.code}") GlobalScope.launch(Dispatchers.Main) { delay(3000) if (!context.isFinishing) { createAndLoad() } } } }) }*/ override fun load() { //if (interstitialAd == null) createAndLoad() } override fun show() { /*when { interstitialAd != null -> { interstitialAd?.fullScreenContentCallback = object : FullScreenContentCallback() { override fun onAdDismissedFullScreenContent() { FirebaseAnalytics.getInstance(App.context).logEvent("Interstitial_Ad_watched", Bundle()) createAndLoad() Economy.reward(false) } } interstitialAd?.show(context) } Network.isAdsBlocked -> toast("Anuncios bloqueados por host") else -> context.lifecycleScope.launch(Dispatchers.Main) { var tryCount = 11 while (interstitialAd == null && tryCount > 0) { delay(250) tryCount-- } if (interstitialAd != null) show() } }*/ } } /*fun getAdSize(width: Float): AdSize { val metrics = App.context.resources.displayMetrics val density = metrics.density var adWidthPixels = width if (adWidthPixels == 0f) { adWidthPixels = metrics.widthPixels.toFloat() } val adWidth = (adWidthPixels / density).toInt() return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(App.context, adWidth) }*/ //abstract class AbsAdListener : AdListener() object AdmobID { const val RECENT_BANNER = "ca-app-pub-8896186846999141/5663284088" const val RECENT_BANNER2 = "ca-app-pub-8896186846999141/5663284088" const val FAVORITE_BANNER = "ca-app-pub-8896186846999141/5663284088" const val FAVORITE_BANNER2 = "ca-app-pub-8896186846999141/5663284088" const val DIRECTORY_BANNER = "ca-app-pub-8896186846999141/5663284088" const val HOME_BANNER = "ca-app-pub-8896186846999141/5663284088" const val HOME_BANNER2 = "ca-app-pub-8896186846999141/5663284088" const val EMISSION_BANNER = "ca-app-pub-8896186846999141/5663284088" const val SEEING_BANNER = "ca-app-pub-8896186846999141/5663284088" const val RECOMMEND_BANNER = "ca-app-pub-8896186846999141/5663284088" const val QUEUE_BANNER = "ca-app-pub-8896186846999141/5663284088" const val RECORD_BANNER = "ca-app-pub-8896186846999141/5663284088" const val RANDOM_BANNER = "ca-app-pub-8896186846999141/5663284088" const val NEWS_BANNER = "ca-app-pub-8896186846999141/5663284088" const val INFO_BANNER = "ca-app-pub-8896186846999141/5663284088" const val ACHIEVEMENT_BANNER = "ca-app-pub-8896186846999141/5663284088" const val EXPLORER_BANNER = "ca-app-pub-8896186846999141/5663284088" const val CAST_BANNER = "ca-app-pub-8896186846999141/5663284088" const val LIST_NATIVE = "ca-app-pub-8896186846999141/5113437989" const val REWARDED = "ca-app-pub-8896186846999141/9219385713" const val INTERSTITIAL = "ca-app-pub-8896186846999141/7231764047" } ================================================ FILE: app/src/main/java/knf/kuma/ads/NativeManager.kt ================================================ package knf.kuma.ads /*import com.google.android.gms.ads.AdListener import com.google.android.gms.ads.AdLoader import com.google.android.gms.ads.LoadAdError import com.google.android.gms.ads.nativead.NativeAd*/ /*object NativeManager { private var isLoading = false private var internalSize = 0 private val adsChannel = Channel(Int.MAX_VALUE) init { cacheAds() } suspend fun take(scope: CoroutineScope, size: Int, tryCount: Int = 0, callback: TakeCallback) { val operation = suspend { scope.launch { if (internalSize >= size) { launch(Dispatchers.Main) { callback(suspendCoroutine { launch { val pendingList = mutableListOf() repeat(size) { internalSize-- pendingList.add(adsChannel.receive()) } it.resume(pendingList) } }) } if (internalSize <= 5) cacheAds() }else { take(scope, size, tryCount + 1, callback) } } Unit } when { withContext(Dispatchers.IO) {Network.isAdsBlocked } -> callback(emptyList()) internalSize >= size -> operation() isLoading -> { withContext(Dispatchers.IO) { while (isLoading) delay(500) if (tryCount > 3) callback(emptyList()) else take(scope, size, tryCount + 1, callback) } } else -> { cacheAds(scope, size, operation) } } } private fun cacheAds(scope: CoroutineScope = GlobalScope, pendingSize: Int = 5, pending: PendingCallback = {}) { if (isLoading) return GlobalScope.launch(Dispatchers.Main) { isLoading = true var loader: AdLoader? = null var isCallbackCalled = false loader = AdLoader.Builder(App.context, AdsUtilsMob.LIST_NATIVE) .forNativeAd { GlobalScope.launch { internalSize++ adsChannel.send(it) if (internalSize >= pendingSize) { isCallbackCalled = true scope.launch { pending() } } if (loader?.isLoading == false) { isLoading = false if (!isCallbackCalled) scope.launch { pending() } } } } .withAdListener(object : AdListener() { override fun onAdFailedToLoad(p0: LoadAdError) { super.onAdFailedToLoad(p0) if (loader?.isLoading == false) { isLoading = false launch { if (!isCallbackCalled) scope.launch { pending() } } } } }).build() loader.loadAds(AdsUtilsMob.adRequest, 5) } } }*/ typealias PendingCallback = suspend () -> Unit typealias TakeCallback = (List) -> Unit ================================================ FILE: app/src/main/java/knf/kuma/ads/SubscriptionReceiver.kt ================================================ package knf.kuma.ads import android.content.Intent import knf.kuma.App import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.json.JSONObject import java.net.URL object SubscriptionReceiver { data class VerifyStatus(val isVerified: Boolean = false, val isActive: Boolean = false) data class SubscriptionInfo( val token: String = "", val purchaseTime: Long = 0L ) fun check(intent: Intent) { GlobalScope.launch(Dispatchers.IO) { if (intent.hasExtra("token")) check(intent.getStringExtra("token")) else check(PrefsUtil.subscriptionToken) } } private suspend fun check(token: String?) { if (token == null || !Network.isConnected) return val status = checkStatus(token) if (status.isVerified) { PrefsUtil.subscriptionToken = token } else { PrefsUtil.subscriptionToken = null if (!PrefsUtil.isAdsEnabled) FirestoreManager.doSignOut(App.context) } } suspend fun checkStatus(token: String): VerifyStatus = withContext(Dispatchers.IO) { try { val json = JSONObject(URL("https://us-central1-nu-client.cloudfunctions.net/checkSub?token=${token}").readText()) VerifyStatus(json.getBoolean("isVerified"), json.getBoolean("isActive")) } catch (e: Exception) { e.printStackTrace() VerifyStatus(isVerified = true, isActive = true) } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/ActivityAnime.kt ================================================ package knf.kuma.animeinfo import androidx.activity.addCallback import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.InflateException import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageView import androidx.activity.viewModels import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.google.android.material.floatingactionbutton.FloatingActionButton import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.showRandomInterstitial import knf.kuma.animeinfo.img.ActivityImgFull import knf.kuma.animeinfo.viewholders.AnimeActivityHolder import knf.kuma.backup.firestore.syncData import knf.kuma.commons.CastUtil import knf.kuma.commons.DesignUtils import knf.kuma.commons.EAHelper import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.noCrash import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.directory.DirObject import knf.kuma.directory.DirObjectCompact import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.NotificationObj import knf.kuma.pojos.QueueObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeeingObject import knf.kuma.recommended.AnimeShortObject import knf.kuma.recommended.RankType import knf.kuma.recommended.RecommendHelper import knf.kuma.search.SearchObject import knf.kuma.search.SearchObjectFav import knf.kuma.widgets.emision.WEListItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onLongClick import org.jetbrains.anko.toast import xdroid.toaster.Toaster import java.util.Locale import kotlin.random.Random class ActivityAnime : GenericActivity(), AnimeActivityHolder.Interface { private var isEdited = false private val viewModel: AnimeViewModel by viewModels() private val holder: AnimeActivityHolder by lazy { AnimeActivityHolder(this) } private var favoriteObject: FavoriteObject? = null private val dao = CacheDB.INSTANCE.favsDAO() private var chapters: MutableList = ArrayList() private var genres: MutableList = ArrayList() private val aidOnly get() = intent?.getBooleanExtra(keyAidOnly, false) ?: false override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getThemeNA()) super.onCreate(savedInstanceState) try { setContentView(R.layout.activity_anime_info) } catch (e: InflateException) { setContentView(R.layout.activity_anime_info_nwv) } setSupportActionBar(holder.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) holder.toolbar.setNavigationOnClickListener { closeActivity() } if (aidOnly) viewModel.init(intent.getStringExtra(keyAid)) else viewModel.init(this@ActivityAnime, intent.dataString, intent.getBooleanExtra(keyPersist, true)) if (intent.getBooleanExtra(keyNotification, false)) sendBroadcast(NotificationObj.fromIntent(intent).getBroadcast(this@ActivityAnime)) onBackPressedDispatcher.addCallback(this) { closeActivity() } load() checkBypass() showRandomInterstitial(this) } private fun load() { viewModel.liveData.observe(this, Observer { animeObject -> if (animeObject != null) { doOnUI { chapters = animeObject.chapters ?: mutableListOf() genres = animeObject.genres ?: mutableListOf() if (PrefsUtil.isFamilyFriendly && genres.map { it.lowercase(Locale.getDefault()) }.contains("ecchi")) { toast("Anime no familiar") onBackPressedDispatcher.onBackPressed() } favoriteObject = FavoriteObject(animeObject) favoriteObject?.let { fav -> holder.imageView.onLongClick(returnValue = true) { doAsync { val isFav = dao.isFav(fav.key) if (isFav) { holder.setFABState(false) dao.deleteFav(fav) RecommendHelper.registerAll(genres, RankType.UNFAV) doOnUI { toast("Removido de favoritos") } } else { holder.setFABState(true) dao.addFav(fav) RecommendHelper.registerAll(genres, RankType.FAV) AchievementManager.onFavAdded(fav) doOnUI { toast("Añadido a favoritos") } } syncData { favs() } } } dao.isFavLive(fav.key).observe(this, Observer { holder.setFABState(it) }) } holder.setTitle(animeObject.name) holder.loadImg(PatternUtil.getCover(animeObject.aid), View.OnClickListener { startActivity( Intent(this@ActivityAnime, ActivityImgFull::class.java) .setData(Uri.parse(PatternUtil.getCover(animeObject.aid))) .putExtra(keyTitle, animeObject.name), ActivityOptionsCompat.makeSceneTransitionAnimation(this@ActivityAnime, holder.imageView, "img") .toBundle() ) }) lifecycleScope.launch(Dispatchers.Main){ holder.setFABState(withContext(Dispatchers.IO) { dao.isFav(favoriteObject?.key ?: 0) }) holder.showFAB() } invalidateOptionsMenu() RecommendHelper.registerAll(genres, RankType.CHECK) } } else { Toaster.toast("Error al cargar información del anime") onBackPressedDispatcher.onBackPressed() } }) } private fun setResult() { isEdited = true } override fun onFabClicked(actionButton: FloatingActionButton) { lifecycleScope.launch(Dispatchers.Main) { setResult() favoriteObject?.let { val isFav = withContext(Dispatchers.IO) { dao.isFav(it.key) } if (isFav) { holder.setFABState(false) withContext(Dispatchers.IO) { dao.deleteFav(it) } RecommendHelper.registerAll(genres, RankType.UNFAV) } else { holder.setFABState(true) withContext(Dispatchers.IO) { dao.addFav(it) } RecommendHelper.registerAll(genres, RankType.FAV) AchievementManager.onFavAdded(it) } syncData { favs() } } } } override fun onImgClicked(imageView: ImageView) { } override fun onBypassUpdated() { try { if (!aidOnly) viewModel.reload(this, intent.dataString, intent.getBooleanExtra(keyPersist, true)) } catch (e: Exception) { e.printStackTrace() } } override fun getSnackbarAnchor(): View? { return findViewById(R.id.coordinator) } override fun onCreateOptionsMenu(menu: Menu): Boolean { if (favoriteObject != null) { menuInflater.inflate(R.menu.menu_anime_info, menu) CastUtil.registerActivity(this, menu, R.id.castMenu) } return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_share -> share() } return true } private fun share() { try { startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND) .setType("text/plain") .putExtra(Intent.EXTRA_TEXT, favoriteObject?.name + "\n" + favoriteObject?.link), "Compartir")) AchievementManager.onShare() } catch (e: ActivityNotFoundException) { Toaster.toast("No se encontraron aplicaciones para enviar") } } private fun closeActivity() { holder.hideFABForce() if (intent.getBooleanExtra(keyFromFav, false) && isEdited) { finish() } else if (intent.getBooleanExtra(keyNoTransition, false)) { finish() } else { supportFinishAfterTransition() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) noCrash { if (requestCode == FileAccessHelper.SD_REQUEST && resultCode == RESULT_OK) { val validation = FileAccessHelper.isUriValid(data?.data) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") FileAccessHelper.openTreeChooser(this) } } } } companion object { private var REQUEST_CODE = 558 private const val keyTitle = "title" private const val keyAid = "aid" private const val keyImg = "img" private const val keyPosition = "persist" private const val keyPersist = "persist" private const val keyNoTransition = "noTransition" private const val keyIsRecord = "isRecord" private const val keyFromFav = "from_fav" private const val keyAidOnly = "aid_only" private const val keyNotification = "notification" private const val sharedImg = "img" fun open(fragment: Fragment, animeObject: SearchObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: SearchObjectFav, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, recentObject: RecentObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(recentObject.url) intent.putExtra(keyTitle, recentObject.name) intent.putExtra(keyAid, recentObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(recentObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: DirObjectCompact, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: DirObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return try { val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } catch (e: Exception) { e.printStackTrace() } } fun open(activity: Activity, animeObject: SearchObject, view: ImageView, persist: Boolean, animate: Boolean) { val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity?, animeObject: AnimeShortObject, view: ImageView, persist: Boolean, animate: Boolean) { val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) activity?.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, explorerObject: ExplorerObject, view: ImageView) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(explorerObject.link) intent.putExtra(keyTitle, explorerObject.name) intent.putExtra(keyAid, explorerObject.key.toString()) intent.putExtra(keyImg, PatternUtil.getCover(explorerObject.aid)) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity, recordObject: RecordObject, view: ImageView) { val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(recordObject.animeObject.link) intent.putExtra(keyTitle, recordObject.name) intent.putExtra(keyAid, recordObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(recordObject.animeObject.aid)) intent.putExtra(keyPersist, true) intent.putExtra(keyIsRecord, true) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity?, seeingObject: SeeingObject) { activity ?: return val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(seeingObject.link) intent.putExtra(keyTitle, seeingObject.title) //intent.putExtra(keyAid, seeingObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(seeingObject.aid)) intent.putExtra(keyPersist, true) intent.putExtra(keyNoTransition, true) intent.putExtra(keyIsRecord, true) activity.startActivity(intent) } fun open(context: Context, animeObject: SearchObject) { val intent = Intent(context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) context.startActivity(intent) } fun open(fragment: Fragment, favoriteObject: FavoriteObject, view: ImageView) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(favoriteObject.link) intent.putExtra(keyTitle, favoriteObject.name) intent.putExtra(keyAid, favoriteObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(favoriteObject.aid)) intent.putExtra(keyFromFav, true) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity, queueObject: QueueObject, view: ImageView) { val intent = Intent(activity, DesignUtils.infoClass) intent.putExtra(keyTitle, queueObject.chapter.name) intent.putExtra(keyAid, queueObject.chapter.aid) intent.putExtra(keyImg, PatternUtil.getCover(queueObject.chapter.aid)) intent.putExtra(keyAidOnly, true) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeRelated: AnimeObject.WebInfo.AnimeRelated) { val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse("https://www3.animeflv.net/" + animeRelated.link) intent.putExtra(keyTitle, animeRelated.name) intent.putExtra(keyAid, animeRelated.aid) fragment.startActivityForResult(intent, REQUEST_CODE) } fun open(fragment: Fragment, animeObject: AnimeObject.WebInfo.AnimeRelated, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse("https://www3.animeflv.net/" + animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(context: Context, url: String) { val intent = Intent(context, DesignUtils.infoClass) intent.data = Uri.parse(url) context.startActivity(intent) } fun getSimpleIntent(context: Context, item: WEListItem): Intent { val intent = Intent(context, DesignUtils.infoClass) intent.data = Uri.parse(item.link) intent.action = "${Random.nextInt(1, 9000)}" intent.putExtra(keyTitle, item.title) intent.putExtra(keyAid, item.aid) intent.putExtra(keyImg, PatternUtil.getCover(item.aid)) return intent } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/ActivityAnimeMaterial.kt ================================================ package knf.kuma.animeinfo import androidx.activity.addCallback import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.InflateException import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageView import androidx.activity.viewModels import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.google.android.material.floatingactionbutton.FloatingActionButton import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.showRandomInterstitial import knf.kuma.animeinfo.img.ActivityImgFull import knf.kuma.animeinfo.viewholders.AnimeActivityMaterialHolder import knf.kuma.backup.firestore.syncData import knf.kuma.commons.CastUtil import knf.kuma.commons.DesignUtils import knf.kuma.commons.EAHelper import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.noCrash import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.directory.DirObject import knf.kuma.directory.DirObjectCompact import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.NotificationObj import knf.kuma.pojos.QueueObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeeingObject import knf.kuma.recommended.AnimeShortObject import knf.kuma.recommended.RankType import knf.kuma.recommended.RecommendHelper import knf.kuma.search.SearchAdvObject import knf.kuma.search.SearchObject import knf.kuma.search.SearchObjectFav import knf.kuma.widgets.emision.WEListItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onLongClick import org.jetbrains.anko.toast import xdroid.toaster.Toaster import java.util.Locale import kotlin.random.Random class ActivityAnimeMaterial : GenericActivity(), AnimeActivityMaterialHolder.Interface { private var isEdited = false private val viewModel: AnimeViewModel by viewModels() private val holder: AnimeActivityMaterialHolder by lazy { AnimeActivityMaterialHolder(this) } private var favoriteObject: FavoriteObject? = null private val dao = CacheDB.INSTANCE.favsDAO() private var chapters: MutableList = ArrayList() private var genres: MutableList = ArrayList() private val aidOnly get() = intent?.getBooleanExtra(keyAidOnly, false) ?: false override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getThemeNA()) super.onCreate(savedInstanceState) try { setContentView(R.layout.activity_anime_info_material) } catch (e: InflateException) { setContentView(R.layout.activity_anime_info_nwv) } setSupportActionBar(holder.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) holder.toolbar.setNavigationOnClickListener { closeActivity() } if (aidOnly) viewModel.init(intent.getStringExtra(keyAid)) else viewModel.init(this@ActivityAnimeMaterial, intent.dataString, intent.getBooleanExtra(keyPersist, true)) if (intent.getBooleanExtra(keyNotification, false)) sendBroadcast(NotificationObj.fromIntent(intent).getBroadcast(this@ActivityAnimeMaterial)) onBackPressedDispatcher.addCallback(this) { closeActivity() } load() checkBypass() showRandomInterstitial(this) } private fun load() { viewModel.liveData.observe(this, Observer { animeObject -> if (animeObject != null) { doOnUI { chapters = animeObject.chapters ?: mutableListOf() genres = animeObject.genres ?: mutableListOf() if (PrefsUtil.isFamilyFriendly && genres.map { it.lowercase(Locale.getDefault()) }.contains("ecchi")) { toast("Anime no familiar") onBackPressedDispatcher.onBackPressed() } favoriteObject = FavoriteObject(animeObject) favoriteObject?.let { fav -> holder.imageView.onLongClick(returnValue = true) { doAsync { val isFav = dao.isFav(fav.key) if (isFav) { holder.setFABState(false) dao.deleteFav(fav) RecommendHelper.registerAll(genres, RankType.UNFAV) doOnUI { toast("Removido de favoritos") } } else { holder.setFABState(true) dao.addFav(fav) RecommendHelper.registerAll(genres, RankType.FAV) AchievementManager.onFavAdded(fav) doOnUI { toast("Añadido a favoritos") } } syncData { favs() } } } dao.isFavLive(fav.key).observe(this, Observer { holder.setFABState(it) }) } holder.setTitle(animeObject.name) holder.loadImg(PatternUtil.getCover(animeObject.aid), View.OnClickListener { startActivity( Intent(this@ActivityAnimeMaterial, ActivityImgFull::class.java) .setData(Uri.parse(PatternUtil.getCover(animeObject.aid))) .putExtra(keyTitle, animeObject.name), ActivityOptionsCompat.makeSceneTransitionAnimation(this@ActivityAnimeMaterial, holder.imageView, "img") .toBundle() ) }) lifecycleScope.launch(Dispatchers.Main) { holder.setFABState(withContext(Dispatchers.IO) { dao.isFav(favoriteObject?.key ?: 0) }) holder.showFAB() } invalidateOptionsMenu() RecommendHelper.registerAll(genres, RankType.CHECK) } } else { Toaster.toast("Error al cargar información del anime") onBackPressedDispatcher.onBackPressed() } }) } private fun setResult() { isEdited = true } override fun onFabClicked(actionButton: FloatingActionButton) { lifecycleScope.launch(Dispatchers.Main) { setResult() favoriteObject?.let { val isFav = withContext(Dispatchers.IO) { dao.isFav(it.key) } if (isFav) { holder.setFABState(false) withContext(Dispatchers.IO) { dao.deleteFav(it) } RecommendHelper.registerAll(genres, RankType.UNFAV) } else { holder.setFABState(true) withContext(Dispatchers.IO) { dao.addFav(it) } RecommendHelper.registerAll(genres, RankType.FAV) AchievementManager.onFavAdded(it) } syncData { favs() } } } } override fun onImgClicked(imageView: ImageView) { } override fun onBypassUpdated() { try { if (!aidOnly) viewModel.reload(this, intent.dataString, intent.getBooleanExtra(keyPersist, true)) } catch (e: Exception) { e.printStackTrace() } } override fun getSnackbarAnchor(): View? { return findViewById(R.id.coordinator) } override fun onCreateOptionsMenu(menu: Menu): Boolean { if (favoriteObject != null) { menuInflater.inflate(R.menu.menu_anime_info, menu) CastUtil.registerActivity(this, menu, R.id.castMenu) } return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_share -> share() } return true } private fun share() { try { startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND) .setType("text/plain") .putExtra(Intent.EXTRA_TEXT, favoriteObject?.name + "\n" + favoriteObject?.link), "Compartir")) AchievementManager.onShare() } catch (e: ActivityNotFoundException) { Toaster.toast("No se encontraron aplicaciones para enviar") } } private fun closeActivity() { holder.hideFABForce() if (intent.getBooleanExtra(keyFromFav, false) && isEdited) { finish() } else if (intent.getBooleanExtra(keyNoTransition, false)) { finish() } else { supportFinishAfterTransition() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) noCrash { if (requestCode == FileAccessHelper.SD_REQUEST && resultCode == RESULT_OK) { val validation = FileAccessHelper.isUriValid(data?.data) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") FileAccessHelper.openTreeChooser(this) } } } } companion object { private var REQUEST_CODE = 558 private const val keyTitle = "title" private const val keyAid = "aid" private const val keyImg = "img" private const val keyPosition = "persist" private const val keyPersist = "persist" private const val keyNoTransition = "noTransition" private const val keyIsRecord = "isRecord" private const val keyFromFav = "from_fav" private const val keyAidOnly = "aid_only" private const val keyNotification = "notification" private const val sharedImg = "img" fun open(fragment: Fragment, recentObject: RecentObject, view: ImageView, position: Int) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(recentObject.anime) intent.putExtra(keyTitle, recentObject.name) intent.putExtra(keyAid, recentObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(recentObject.aid)) intent.putExtra(keyPosition, position) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } @JvmOverloads fun open(fragment: Fragment, animeObject: AnimeObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: SearchObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: SearchAdvObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun openGeneric(fragment: Fragment, animeObject: SearchAdvObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: SearchObjectFav, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: AnimeObject.WebInfo.AnimeRelated, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse("https://www3.animeflv.net/" + animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, recentObject: RecentObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(recentObject.url) intent.putExtra(keyTitle, recentObject.name) intent.putExtra(keyAid, recentObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(recentObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun openGeneric(fragment: Fragment, recentObject: RecentObject, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(recentObject.url) intent.putExtra(keyTitle, recentObject.name) intent.putExtra(keyAid, recentObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(recentObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: DirObjectCompact, view: ImageView, persist: Boolean = true, animate: Boolean = true) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeObject: DirObject, persist: Boolean = false) { fragment.startActivity(Intent(fragment.requireContext(),DesignUtils.infoClass).apply { data = Uri.parse(animeObject.link) putExtra(keyTitle, animeObject.name) putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) putExtra(keyPersist, persist) }) } fun open(fragment: Fragment, dirObject: DirObjectCompact, persist: Boolean = false) { fragment.startActivity(Intent(fragment.requireContext(),DesignUtils.infoClass).apply { data = Uri.parse(dirObject.link) putExtra(keyTitle, dirObject.name) putExtra(keyImg, PatternUtil.getCover(dirObject.aid)) putExtra(keyPersist, persist) }) } fun open(activity: Activity, animeObject: AnimeObject, view: ImageView, persist: Boolean, animate: Boolean) { val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity, animeObject: SearchObject, view: ImageView, persist: Boolean, animate: Boolean) { val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity?, animeObject: AnimeShortObject, view: ImageView, persist: Boolean, animate: Boolean) { val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) intent.putExtra(keyPersist, persist) intent.putExtra(keyNoTransition, !animate) activity?.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, explorerObject: ExplorerObject, view: ImageView) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(explorerObject.link) intent.putExtra(keyTitle, explorerObject.name) intent.putExtra(keyAid, explorerObject.key.toString()) intent.putExtra(keyImg, PatternUtil.getCover(explorerObject.aid)) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity, recordObject: RecordObject, view: ImageView) { val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse("https://www3.animeflv.net/" + recordObject.animeObject.link) intent.putExtra(keyTitle, recordObject.name) intent.putExtra(keyAid, recordObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(recordObject.animeObject.aid)) intent.putExtra(keyPersist, true) intent.putExtra(keyIsRecord, true) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity?, seeingObject: SeeingObject) { activity ?: return val intent = Intent(activity, DesignUtils.infoClass) intent.data = Uri.parse(seeingObject.link) intent.putExtra(keyTitle, seeingObject.title) //intent.putExtra(keyAid, seeingObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(seeingObject.aid)) intent.putExtra(keyPersist, true) intent.putExtra(keyNoTransition, true) intent.putExtra(keyIsRecord, true) activity.startActivity(intent) } fun open(context: Context, animeObject: SearchObject) { val intent = Intent(context, DesignUtils.infoClass) intent.data = Uri.parse(animeObject.link) intent.putExtra(keyTitle, animeObject.name) intent.putExtra(keyAid, animeObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(animeObject.aid)) context.startActivity(intent) } fun open(fragment: Fragment, favoriteObject: FavoriteObject, view: ImageView) { val activity = fragment.activity ?: return val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse(favoriteObject.link) intent.putExtra(keyTitle, favoriteObject.name) //intent.putExtra(keyAid, favoriteObject.aid) intent.putExtra(keyImg, PatternUtil.getCover(favoriteObject.aid)) intent.putExtra(keyFromFav, true) fragment.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(activity: Activity, queueObject: QueueObject, view: ImageView) { val intent = Intent(activity, DesignUtils.infoClass) intent.putExtra(keyTitle, queueObject.chapter.name) intent.putExtra(keyAid, queueObject.chapter.aid) intent.putExtra(keyImg, PatternUtil.getCover(queueObject.chapter.aid)) intent.putExtra(keyAidOnly, true) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, sharedImg).toBundle()) } fun open(fragment: Fragment, animeRelated: AnimeObject.WebInfo.AnimeRelated) { val intent = Intent(fragment.context, DesignUtils.infoClass) intent.data = Uri.parse("https://www3.animeflv.net/" + animeRelated.link) intent.putExtra(keyTitle, animeRelated.name) intent.putExtra(keyAid, animeRelated.aid) fragment.startActivityForResult(intent, REQUEST_CODE) } fun open(context: Context, url: String) { val intent = Intent(context, DesignUtils.infoClass) intent.data = Uri.parse(url) context.startActivity(intent) } fun getSimpleIntent(context: Context, item: WEListItem): Intent { val intent = Intent(context, DesignUtils.infoClass) intent.data = Uri.parse(item.link) intent.action = "${Random.nextInt(1, 9000)}" intent.putExtra(keyTitle, item.title) intent.putExtra(keyAid, item.aid) intent.putExtra(keyImg, PatternUtil.getCover(item.aid)) return intent } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeBroadcast.kt ================================================ package knf.kuma.animeinfo import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import knf.kuma.database.CacheDB class AnimeBroadcast : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val animeObject = CacheDB.INSTANCE.animeDAO().getByAid(intent.getStringExtra("aid") ?: "") if (animeObject != null) ActivityAnimeMaterial.open(context, animeObject) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeChaptersAdapter.kt ================================================ package knf.kuma.animeinfo import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.net.Uri import android.os.Build import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.card.MaterialCardView import com.michaelflisar.dragselectrecyclerview.DragSelectTouchListener import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import com.squareup.picasso.Callback import knf.kuma.App import knf.kuma.R import knf.kuma.ads.AdsUtils import knf.kuma.animeinfo.fragments.ChaptersFragment import knf.kuma.animeinfo.ktx.epTitle import knf.kuma.animeinfo.ktx.fileName import knf.kuma.animeinfo.ktx.filePath import knf.kuma.backup.firestore.syncData import knf.kuma.cast.CastMedia import knf.kuma.commons.CastUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.FileWrapper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.distinct import knf.kuma.commons.doOnUI import knf.kuma.commons.isFullMode import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashSuspend import knf.kuma.commons.safeShow import knf.kuma.database.CacheDB import knf.kuma.download.DownloadManagerCentral import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeeingObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import knf.kuma.videoservers.FileActions import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster import java.net.URL import java.util.Locale import java.util.concurrent.atomic.AtomicReference class AnimeChaptersAdapter(private val fragment: Fragment, private val recyclerView: RecyclerView, val chapters: List, private val touchListener: DragSelectTouchListener) : RecyclerView.Adapter(), FastScrollRecyclerView.SectionedAdapter { private val context: Context? = fragment.context private val chaptersDAO = CacheDB.INSTANCE.seenDAO() private val recordsDAO = CacheDB.INSTANCE.recordsDAO() private val seeingDAO = CacheDB.INSTANCE.seeingDAO() private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() private val isNetworkAvailable = Network.isConnected val selection = HashSet() private var seeingObject: SeeingObject? = null var isImporting = false private var processingPosition = -1 init { setHasStableIds(true) if (chapters.isNotEmpty()) { noCrash { seeingObject = seeingDAO.getByAid(chapters[0].chapter.aid) doAsync { if (CacheDB.INSTANCE.animeDAO().isCompleted(chapters[0].chapter.aid)) DownloadedObserver.observe(fragment.lifecycleScope, chapters.size, chapters[0].chapter.fileWrapper()) } } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChapterImgHolder { return ChapterImgHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_chapter_preview, parent, false)) } override fun onBindViewHolder(holder: ChapterImgHolder, position: Int, payloads: MutableList) { if (context != null && payloads.isNotEmpty() && chapters.isNotEmpty()) { holder.setSeen(chapters[position].isSeen) if (selection.contains(position)) { holder.cardView.setBackgroundColor(ContextCompat.getColor(context, EAHelper.getThemeColorLight())) } else { holder.cardView.setBackgroundColor(ContextCompat.getColor(context, R.color.cardview_background)) } } if (payloads.isEmpty()) super.onBindViewHolder(holder, position, payloads) } override fun onBindViewHolder(holder: ChapterImgHolder, position: Int) { if (context == null) return val chapter = chapters[position] if (selection.contains(position)) holder.cardView.setBackgroundColor(ContextCompat.getColor(context, EAHelper.getThemeColorLight())) else holder.cardView.setBackgroundColor(ContextCompat.getColor(context, R.color.cardview_background)) if (processingPosition == holder.adapterPosition) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE } else holder.progressBarRoot.visibility = View.GONE if (!Network.isConnected || chapter.chapter.img == null) holder.imageView.visibility = View.GONE if (chapter.chapter.img != null) holder.imageView.load(chapter.chapter.img, object : Callback { override fun onSuccess() { holder.imageView.visibility = View.VISIBLE } override fun onError(e: Exception?) { } }) val downloadObject = AtomicReference() holder.apply { fileWrapperJob?.cancel() fileWrapperJob = fragment.lifecycleScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { chapter.chapter.fileWrapper() downloadObject.set(downloadsDAO.getByEid(chapter.chapter.eid)) } if (!isActive) return@launch setQueueObserver(CacheDB.INSTANCE.queueDAO().isInQueueLive(chapter.chapter.eid), fragment) { setQueue( it, isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject.get()) ) } setDownloadObserver(downloadsDAO.getLiveByEid(chapter.chapter.eid).distinct, fragment) { downloadObject1 -> setDownloadState(downloadObject1) val casting = CastUtil.get().casting.value val isCasting = casting != null && casting == chapter.chapter.eid if (!isCasting) fragment.lifecycleScope.launch(Dispatchers.IO) { setQueue( QueueManager.isInQueue(chapter.chapter.eid), isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject1) ) } else setDownloaded( isPlayAvailable( chapter.chapter.fileWrapper(), downloadObject1 ), true ) downloadObject.set(downloadObject1) } setCastingObserver(fragment) { s -> if (chapter.chapter.eid != s) fragment.lifecycleScope.launch(Dispatchers.IO) { setQueue( QueueManager.isInQueue(chapter.chapter.eid), isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject.get()) ) } else setDownloaded( isPlayAvailable( chapter.chapter.fileWrapper(), downloadObject.get() ), chapter.chapter.eid == s ) } } } holder.chapter.setTextColor(ContextCompat.getColor(context, if (chapter.isSeen) EAHelper.getThemeColor() else R.color.textPrimary)) holder.separator.visibility = if (position == 0) View.GONE else View.VISIBLE holder.chapter.text = chapter.chapter.number if (!isFullMode) holder.actions.visibility = View.GONE else holder.actions.setOnClickListener { view -> fragment.lifecycleScope.launch(Dispatchers.Main) { val menu = PopupMenu(context, view) if (CastUtil.get().casting.value == chapter.chapter.eid) { menu.inflate(R.menu.chapter_casting_menu) if (canPlay(chapter.chapter.fileWrapper())) menu.menu.findItem(R.id.download).isVisible = false } else if (isPlayAvailable( chapter.chapter.fileWrapper(), downloadObject.get() ) ) { menu.inflate(R.menu.chapter_downloaded_menu) if (!CastUtil.get().connected()) menu.menu.findItem(R.id.cast).isVisible = false } else if (isNetworkAvailable) menu.inflate(R.menu.chapter_menu) else menu.inflate(R.menu.chapter_menu_offline) if (QueueManager.isInQueue(chapter.chapter.eid) && menu.menu.findItem(R.id.queue) != null) menu.menu.findItem(R.id.queue).isVisible = false if (!PrefsUtil.showImport() || isImporting) menu.menu.findItem(R.id.import_file).isVisible = false menu.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.play -> if (canPlay(chapter.chapter.fileWrapper())) { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) recordsDAO.add(RecordObject.fromChapter(chapter.chapter)) } chapter.isSeen = true updateSeeing(chapter.chapter.number) holder.setSeen(true) ServersFactory.startPlay(context, chapter.chapter.epTitle, chapter.chapter.fileWrapper().name()) syncData { history() seen() } } else { Toaster.toast("Aun no se está descargando") } R.id.cast -> if (canPlay(chapter.chapter.fileWrapper())) { CastUtil.get().play(recyclerView, CastMedia.create(chapter.chapter)) fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) recordsDAO.add(RecordObject.fromChapter(chapter.chapter)) } chapter.isSeen = true syncData { history() seen() } updateSeeing(chapter.chapter.number) holder.setSeen(true) } R.id.casting -> CastUtil.get().openControls() R.id.delete -> MaterialDialog(context).safeShow { message( text = "¿Eliminar el ${ chapter.chapter.number.lowercase( Locale.getDefault() ) }?" ) positiveButton(text = "CONFIRMAR") { fragment.lifecycleScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { FileAccessHelper.deletePath( chapter.chapter.filePath, false ) } downloadObject.get()?.state = -8 chapter.chapter.fileWrapper().exist = false holder.setDownloaded(false, false) } DownloadManagerCentral.cancel(chapter.chapter.eid) QueueManager.remove(chapter.chapter.eid) } negativeButton(text = "CANCELAR") } R.id.download -> { setOrientation(true) FileActions.download(fragment, chapter.chapter) { state, _ -> when (state) { FileActions.CallbackState.START_DOWNLOAD -> { fragment.lifecycleScope.launch(Dispatchers.Main) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE holder.setQueue(withContext(Dispatchers.IO) { CacheDB.INSTANCE.queueDAO().isInQueue(chapter.chapter.eid) }, true) chapter.chapter.fileWrapper().exist = true } } else -> { fragment.doOnUI { holder.progressBarRoot.visibility = View.GONE } } } setOrientation(false) } } R.id.streaming -> { setOrientation(true) FileActions.stream(fragment, chapter.chapter) { state, extra -> when (state) { FileActions.CallbackState.START_STREAM, FileActions.CallbackState.START_CAST -> { if (state == FileActions.CallbackState.START_CAST) { CastUtil.get().play(recyclerView, CastMedia.create(chapter.chapter, extra as? String)) } fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) recordsDAO.add(RecordObject.fromChapter(chapter.chapter)) } chapter.isSeen = true syncData { history() seen() } updateSeeing(chapter.chapter.number) holder.setSeen(true) } else -> { // } } setOrientation(false) } } R.id.queue -> if (isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject.get())) { QueueManager.add(chapter.chapter.fileWrapper(), downloadObject.get(), true, chapter.chapter) holder.setQueue(true, true) } else { setOrientation(true) ServersFactory.start(context, chapter.chapter.link, chapter.chapter, true, true, object : ServersFactory.ServersInterface { override fun onFinish(started: Boolean, success: Boolean) { if (success) { holder.setQueue(true, false) } setOrientation(false) } override fun onCast(url: String?) {} override fun onProgressIndicator(boolean: Boolean) { fragment.doOnUI { if (boolean) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE } else holder.progressBarRoot.visibility = View.GONE } } override fun getView(): View { return recyclerView } }) } R.id.share -> fragment.activity?.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND) .setType("text/plain") .putExtra(Intent.EXTRA_TEXT, chapter.chapter.epTitle + "\n" + chapter.chapter.link), "Compartir")) R.id.import_file -> (fragment as ChaptersFragment).onMove(chapter.chapter.fileName) R.id.commentaries -> { fragment.lifecycleScope.launch(Dispatchers.Main) { try { val version = withContext(Dispatchers.IO) { try { Regex("load\\.(\\w+)\\.js").find(URL("https://https-myanimelist-net-2.disqus.com/embed.js").readText())?.destructured?.component1()!! } catch (e: Exception) { e.printStackTrace() AdsUtils.remoteConfigs.getString( "disqus_version" ) } } CommentariesDialog.show( fragment, chapter.chapter.link, version ) } catch (_: ActivityNotFoundException) { noCrashSuspend { context.startActivity( Intent( Intent.ACTION_VIEW, Uri.parse(withContext(Dispatchers.IO) { chapter.chapter.commentariesLink( AdsUtils.remoteConfigs.getString( "disqus_version" ) ) }) ) ) } } } } } true } menu.show() } } holder.cardView.setOnClickListener { if (chapter.isSeen) { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.deleteChapter(chapter.chapter.aid, chapter.chapter.number) } chapter.isSeen = false holder.chapter.setTextColor(ContextCompat.getColor(context, R.color.textPrimary)) } else { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) } chapter.isSeen = true holder.chapter.setTextColor(ContextCompat.getColor(context, EAHelper.getThemeColor())) } syncData { seen() } updateSeeing(chapter.chapter.number) } holder.cardView.setOnLongClickListener { touchListener.startDragSelection(holder.adapterPosition) true } } override fun getSectionName(position: Int): String { return chapters[position].chapter.number.trim().substring(chapters[position].chapter.number.trim().lastIndexOf(" ") + 1) } private fun updateSeeing(chapter: String) { doAsync { seeingObject?.let { it.chapter = chapter seeingDAO.update(it) syncData { seeing() } } } } private fun setOrientation(block: Boolean) { noCrash { if (block) (fragment.activity as? AppCompatActivity)?.requestedOrientation = when { fragment.context?.resources?.getBoolean(R.bool.isLandscape) == true -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } else (fragment.activity as? AppCompatActivity)?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } private fun isPlayAvailable(fileWrapper: FileWrapper<*>, downloadObject: DownloadObject?): Boolean { return fileWrapper.exist || downloadObject != null && downloadObject.isDownloading } private fun canPlay(fileWrapper: FileWrapper<*>): Boolean { return fileWrapper.exist } override fun getItemViewType(position: Int): Int { return chapters[position].chapter.chapterType?.value ?: 0 } override fun getItemCount(): Int { return chapters.size } override fun getItemId(position: Int): Long { return position.toLong() } fun select(pos: Int, sel: Boolean) { if (sel) { selection.add(pos) } else { selection.remove(pos) } notifyItemChanged(pos, 0) } fun selectRange(start: Int, end: Int, sel: Boolean) { if (sel) selection.add(start) else selection.remove(start) fragment.doOnUI { notifyItemChanged(start, true) } } fun deselectAll() { selection.clear() notifyDataSetChanged() } override fun onViewRecycled(holder: ChapterImgHolder) { holder.unsetCastingObserver() holder.unsetDownloadObserver() holder.unsetQueueObserver() super.onViewRecycled(holder) } inner class ChapterImgHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val separator: View by itemView.bind(R.id.separator) val imageView: ImageView by itemView.bind(R.id.img) val chapter: TextView by itemView.bind(R.id.chapter) private val inDown: ImageView by itemView.bind(R.id.in_down) val actions: ImageButton by itemView.bind(R.id.actions) val progressBar: ProgressBar by itemView.bind(R.id.progress) val progressBarRoot: View by itemView.bind(R.id.progress_root) private var downloadLiveData: LiveData = MutableLiveData() private var queueLiveData: LiveData = MutableLiveData() private var downloadObserver: Observer? = null private var castingObserver: Observer? = null private var queueObserver: Observer? = null var fileWrapperJob: Job? = null fun setDownloadObserver(downloadLiveData: LiveData, owner: LifecycleOwner?, observer: Observer) { if (owner == null) return this.downloadLiveData = downloadLiveData this.downloadObserver = observer this.downloadLiveData.observe(owner, observer) } fun unsetDownloadObserver() { downloadObserver?.let { downloadLiveData.removeObserver(it) downloadObserver = null } } fun setCastingObserver(owner: LifecycleOwner?, observer: Observer) { if (owner == null) return this.castingObserver = observer CastUtil.get().casting.observe(owner, observer) } fun unsetCastingObserver() { castingObserver?.let { CastUtil.get().casting.removeObserver(it) castingObserver = null } } fun setQueueObserver(queueLivedata: LiveData, owner: LifecycleOwner?, observer: Observer) { if (owner == null) return this.queueLiveData = queueLivedata this.queueObserver = observer this.queueLiveData.observe(owner, observer) } fun unsetQueueObserver() { queueObserver?.let { queueLiveData.removeObserver(it) queueObserver = null } } fun setDownloaded(downloaded: Boolean, isCasting: Boolean) { inDown.post { if (downloaded) inDown.setImageResource(R.drawable.ic_chap_down) if (isCasting) inDown.setImageResource(R.drawable.ic_casting) inDown.visibility = if (downloaded || isCasting) View.VISIBLE else View.GONE } } fun setQueue(isInQueue: Boolean, isDownloaded: Boolean) { inDown.post { if (!isInQueue) setDownloaded(isDownloaded, false) else { inDown.setImageResource(if (isDownloaded) R.drawable.ic_queue_file else R.drawable.ic_queue_normal) inDown.visibility = View.VISIBLE } } } fun setSeen(seen: Boolean) { chapter.post { chapter.setTextColor(ContextCompat.getColor(App.context, if (seen) EAHelper.getThemeColor() else R.color.textPrimary)) } } fun setDownloadState(downloadObject: DownloadObject?) { progressBar.post { if (downloadObject != null && PrefsUtil.showProgress()) when (downloadObject.state) { DownloadObject.PENDING -> { progressBarRoot.visibility = View.VISIBLE progressBar.isIndeterminate = true } DownloadObject.PAUSED, DownloadObject.DOWNLOADING -> { progressBarRoot.visibility = View.VISIBLE progressBar.isIndeterminate = false if (downloadObject.getEta() == -2L || PrefsUtil.downloaderType == 0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(downloadObject.progress, true) else progressBar.progress = downloadObject.progress else { progressBar.progress = 0 progressBar.secondaryProgress = downloadObject.progress } } else -> progressBarRoot.visibility = View.GONE } else progressBarRoot.visibility = View.GONE } } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeChaptersAdapterMaterial.kt ================================================ package knf.kuma.animeinfo import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.net.Uri import android.os.Build import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.michaelflisar.dragselectrecyclerview.DragSelectTouchListener import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import com.squareup.picasso.Callback import knf.kuma.App import knf.kuma.R import knf.kuma.ads.AdsUtils import knf.kuma.animeinfo.fragments.ChaptersFragmentMaterial import knf.kuma.animeinfo.ktx.epTitle import knf.kuma.animeinfo.ktx.fileName import knf.kuma.animeinfo.ktx.filePath import knf.kuma.backup.firestore.syncData import knf.kuma.cast.CastMedia import knf.kuma.commons.CastUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.FileWrapper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.distinct import knf.kuma.commons.doOnUI import knf.kuma.commons.getSurfaceColor import knf.kuma.commons.isFullMode import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashSuspend import knf.kuma.commons.safeShow import knf.kuma.database.CacheDB import knf.kuma.download.DownloadManagerCentral import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeeingObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import knf.kuma.videoservers.FileActions import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster import java.net.URL import java.util.Locale import java.util.concurrent.atomic.AtomicReference class AnimeChaptersAdapterMaterial(private val fragment: Fragment, private val recyclerView: RecyclerView, val chapters: List, private val touchListener: DragSelectTouchListener) : RecyclerView.Adapter(), FastScrollRecyclerView.SectionedAdapter { private val context: Context? = fragment.context private val chaptersDAO = CacheDB.INSTANCE.seenDAO() private val recordsDAO = CacheDB.INSTANCE.recordsDAO() private val seeingDAO = CacheDB.INSTANCE.seeingDAO() private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() private val isNetworkAvailable = Network.isConnected val selection = HashSet() private var seeingObject: SeeingObject? = null var isImporting = false private var processingPosition = -1 init { setHasStableIds(true) if (chapters.isNotEmpty()) { noCrash { doAsync { seeingObject = seeingDAO.getByAid(chapters[0].chapter.aid) if (CacheDB.INSTANCE.animeDAO().isCompleted(chapters[0].chapter.aid)) DownloadedObserver.observe(fragment.lifecycleScope, chapters.size, chapters[0].chapter.fileWrapper()) } } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChapterImgHolder { return ChapterImgHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_chapter_preview_material, parent, false)) } override fun onBindViewHolder(holder: ChapterImgHolder, position: Int, payloads: MutableList) { if (context != null) if (selection.contains(position)) holder.cardView.setBackgroundColor(ContextCompat.getColor(context, EAHelper.getThemeColorLight())) else holder.cardView.setBackgroundColor(fragment.getSurfaceColor()) if (payloads.isEmpty()) super.onBindViewHolder(holder, position, payloads) } override fun onBindViewHolder(holder: ChapterImgHolder, position: Int) { if (context == null) return val chapter = chapters[position] if (selection.contains(position)) holder.cardView.setBackgroundColor(ContextCompat.getColor(context, EAHelper.getThemeColorLight())) else holder.cardView.setBackgroundColor(fragment.getSurfaceColor()) if (processingPosition == holder.adapterPosition) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE } else holder.progressBarRoot.visibility = View.GONE if (!Network.isConnected || chapter.chapter.img == null) holder.imageView.visibility = View.GONE if (chapter.chapter.img != null) { holder.imageView.load(chapter.chapter.img, object : Callback { override fun onSuccess() { holder.imageView.visibility = View.VISIBLE } override fun onError(e: Exception?) { } }) } val downloadObject = AtomicReference() holder.apply { fileWrapperJob?.cancel() fileWrapperJob = fragment.lifecycleScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { chapter.chapter.fileWrapper() downloadObject.set(downloadsDAO.getByEid(chapter.chapter.eid)) } if (!isActive) return@launch setQueueObserver(CacheDB.INSTANCE.queueDAO().isInQueueLive(chapter.chapter.eid), fragment, Observer { setQueue(it, isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject.get())) }) setDownloadObserver(downloadsDAO.getLiveByEid(chapter.chapter.eid).distinct, fragment, Observer { downloadObject1 -> setDownloadState(downloadObject1) val casting = CastUtil.get().casting.value val isCasting = casting != null && casting == chapter.chapter.eid if (!isCasting) fragment.lifecycleScope.launch(Dispatchers.IO){ setQueue(QueueManager.isInQueue(chapter.chapter.eid), isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject1)) } else setDownloaded(isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject1), true) downloadObject.set(downloadObject1) }) setCastingObserver(fragment, Observer { s -> if (chapter.chapter.eid != s) fragment.lifecycleScope.launch(Dispatchers.IO){ setQueue(QueueManager.isInQueue(chapter.chapter.eid), isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject.get())) } else setDownloaded(isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject.get()), chapter.chapter.eid == s) }) } } holder.chapter.setTextColor(ContextCompat.getColor(context, if (chapter.isSeen) EAHelper.getThemeColor() else R.color.textPrimary)) holder.separator.visibility = if (position == 0) View.GONE else View.VISIBLE holder.chapter.text = chapter.chapter.number if (!isFullMode) holder.actions.visibility = View.GONE else holder.actions.setOnClickListener { view -> fragment.lifecycleScope.launch(Dispatchers.Main) { val menu = PopupMenu(context, view) if (CastUtil.get().casting.value == chapter.chapter.eid) { menu.inflate(R.menu.chapter_casting_menu) if (canPlay(chapter.chapter.fileWrapper())) menu.menu.findItem(R.id.download).isVisible = false } else if (isPlayAvailable( chapter.chapter.fileWrapper(), downloadObject.get() ) ) { menu.inflate(R.menu.chapter_downloaded_menu) if (!CastUtil.get().connected()) menu.menu.findItem(R.id.cast).isVisible = false } else if (isNetworkAvailable) menu.inflate(R.menu.chapter_menu) else menu.inflate(R.menu.chapter_menu_offline) if (QueueManager.isInQueue(chapter.chapter.eid) && menu.menu.findItem(R.id.queue) != null) menu.menu.findItem(R.id.queue).isVisible = false if (!PrefsUtil.showImport() || isImporting) menu.menu.findItem(R.id.import_file).isVisible = false menu.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.play -> if (canPlay(chapter.chapter.fileWrapper())) { fragment.lifecycleScope.launch(Dispatchers.IO){ chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) recordsDAO.add(RecordObject.fromChapter(chapter.chapter)) } chapter.isSeen = true updateSeeing(chapter.chapter.number) holder.setSeen(true) ServersFactory.startPlay(context, chapter.chapter.epTitle, chapter.chapter.fileWrapper().name()) syncData { history() seen() } } else { Toaster.toast("Aun no se está descargando") } R.id.cast -> if (canPlay(chapter.chapter.fileWrapper())) { //CastUtil.get().play(fragment.activity as Activity, recyclerView, chapter.eid, SelfServer.start(chapter.fileName, true), chapter.name, chapter.number, if (chapter.img == null) chapter.aid else chapter.img, chapter.img == null) CastUtil.get().play(recyclerView, CastMedia.create(chapter.chapter)) fragment.lifecycleScope.launch(Dispatchers.IO){ chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) recordsDAO.add(RecordObject.fromChapter(chapter.chapter)) } chapter.isSeen = true syncData { history() seen() } updateSeeing(chapter.chapter.number) holder.setSeen(true) } R.id.casting -> CastUtil.get().openControls() R.id.delete -> MaterialDialog(context).safeShow { message( text = "¿Eliminar el ${ chapter.chapter.number.lowercase( Locale.getDefault() ) }?" ) positiveButton(text = "CONFIRMAR") { fragment.lifecycleScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { FileAccessHelper.deletePath( chapter.chapter.filePath, false ) } downloadObject.get()?.state = -8 chapter.chapter.fileWrapper().exist = false holder.setDownloaded(false, false) } DownloadManagerCentral.cancel(chapter.chapter.eid) QueueManager.remove(chapter.chapter.eid) } negativeButton(text = "CANCELAR") } R.id.download -> { setOrientation(true) FileActions.download(fragment, chapter.chapter) { state, _ -> when (state) { FileActions.CallbackState.START_DOWNLOAD -> { fragment.lifecycleScope.launch(Dispatchers.Main) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE holder.setQueue(withContext(Dispatchers.IO){ CacheDB.INSTANCE.queueDAO().isInQueue(chapter.chapter.eid) }, true) chapter.chapter.fileWrapper().exist = true } } else -> { fragment.doOnUI { holder.progressBarRoot.visibility = View.GONE } } } setOrientation(false) } } R.id.streaming -> { setOrientation(true) FileActions.stream(fragment, chapter.chapter) { state, extra -> when (state) { FileActions.CallbackState.START_STREAM, FileActions.CallbackState.START_CAST -> { if (state == FileActions.CallbackState.START_CAST) { CastUtil.get().play(recyclerView, CastMedia.create(chapter.chapter, extra as? String)) } fragment.lifecycleScope.launch(Dispatchers.IO){ chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) recordsDAO.add(RecordObject.fromChapter(chapter.chapter)) } chapter.isSeen = true syncData { history() seen() } updateSeeing(chapter.chapter.number) holder.setSeen(true) } else -> { // } } setOrientation(false) } } R.id.queue -> if (isPlayAvailable(chapter.chapter.fileWrapper(), downloadObject.get())) { QueueManager.add(chapter.chapter.fileWrapper(), downloadObject.get(), true, chapter.chapter) holder.setQueue(true, true) } else { setOrientation(true) ServersFactory.start(context, chapter.chapter.link, chapter.chapter, true, true, object : ServersFactory.ServersInterface { override fun onFinish(started: Boolean, success: Boolean) { if (success) { holder.setQueue(true, false) } setOrientation(false) } override fun onCast(url: String?) {} override fun onProgressIndicator(boolean: Boolean) { fragment.doOnUI { if (boolean) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE } else holder.progressBarRoot.visibility = View.GONE } } override fun getView(): View { return recyclerView } }) } R.id.share -> fragment.activity?.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND) .setType("text/plain") .putExtra(Intent.EXTRA_TEXT, chapter.chapter.epTitle + "\n" + chapter.chapter.link), "Compartir")) R.id.import_file -> (fragment as ChaptersFragmentMaterial).onMove(chapter.chapter.fileName) R.id.commentaries -> { fragment.lifecycleScope.launch(Dispatchers.Main){ try { val version = withContext(Dispatchers.IO) { Regex("load\\.(\\w+)\\.js").find(URL("https://https-myanimelist-net-2.disqus.com/embed.js").readText())?.destructured?.component1()!! } CommentariesDialog.show( fragment, chapter.chapter.link, version ) } catch (e: Exception) { noCrashSuspend { context.startActivity( Intent( Intent.ACTION_VIEW, Uri.parse(withContext(Dispatchers.IO) { chapter.chapter.commentariesLink( AdsUtils.remoteConfigs.getString( "disqus_version" ) ) }) ) ) } } } } } true } menu.show() } } holder.cardView.setOnClickListener { if (chapter.isSeen) { fragment.lifecycleScope.launch(Dispatchers.IO){ chaptersDAO.deleteChapter(chapter.chapter.aid, chapter.chapter.number) } chapter.isSeen = false holder.chapter.setTextColor(ContextCompat.getColor(context, R.color.textPrimary)) } else { fragment.lifecycleScope.launch(Dispatchers.IO){ chaptersDAO.addChapter(SeenObject.fromChapter(chapter.chapter)) } chapter.isSeen = true holder.chapter.setTextColor(ContextCompat.getColor(context, EAHelper.getThemeColor())) } syncData { seen() } updateSeeing(chapter.chapter.number) } holder.cardView.setOnLongClickListener { touchListener.startDragSelection(holder.adapterPosition) true } } override fun getSectionName(position: Int): String { return chapters[position].chapter.number.trim().substring(chapters[position].chapter.number.trim().lastIndexOf(" ") + 1) } private fun updateSeeing(chapter: String) { fragment.lifecycleScope.launch(Dispatchers.IO){ seeingObject?.let { it.chapter = chapter seeingDAO.update(it) syncData { seeing() } } } } private fun setOrientation(block: Boolean) { noCrash { if (block) (fragment.activity as? AppCompatActivity)?.requestedOrientation = when { fragment.context?.resources?.getBoolean(R.bool.isLandscape) == true -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } else (fragment.activity as? AppCompatActivity)?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } private fun isPlayAvailable(fileWrapper: FileWrapper<*>, downloadObject: DownloadObject?): Boolean { return fileWrapper.exist || downloadObject != null && downloadObject.isDownloading } private fun canPlay(fileWrapper: FileWrapper<*>): Boolean { return fileWrapper.exist } override fun getItemViewType(position: Int): Int { return chapters[position].chapter.chapterType?.value ?: 0 } override fun getItemCount(): Int { return chapters.size } override fun getItemId(position: Int): Long { return position.toLong() } fun select(pos: Int, sel: Boolean) { if (sel) { selection.add(pos) } else { selection.remove(pos) } notifyItemChanged(pos, 0) } fun selectRange(start: Int, end: Int, sel: Boolean) { for (i in start..end) { if (sel) selection.add(i) else selection.remove(i) } notifyItemRangeChanged(start, end - start + 1, 0) } fun deselectAll() { selection.clear() notifyDataSetChanged() } override fun onViewRecycled(holder: ChapterImgHolder) { holder.unsetCastingObserver() holder.unsetDownloadObserver() holder.unsetQueueObserver() super.onViewRecycled(holder) } inner class ChapterImgHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val separator: View by itemView.bind(R.id.separator) val imageView: ImageView by itemView.bind(R.id.img) val chapter: TextView by itemView.bind(R.id.chapter) private val inDown: ImageView by itemView.bind(R.id.in_down) val actions: ImageButton by itemView.bind(R.id.actions) val progressBar: ProgressBar by itemView.bind(R.id.progress) val progressBarRoot: View by itemView.bind(R.id.progress_root) private var downloadLiveData: LiveData = MutableLiveData() private var queueLiveData: LiveData = MutableLiveData() private var downloadObserver: Observer? = null private var castingObserver: Observer? = null private var queueObserver: Observer? = null var fileWrapperJob: Job? = null fun setDownloadObserver(downloadLiveData: LiveData, owner: LifecycleOwner?, observer: Observer) { if (owner == null) return this.downloadLiveData = downloadLiveData this.downloadObserver = observer this.downloadLiveData.observe(owner, observer) } fun unsetDownloadObserver() { downloadObserver?.let { downloadLiveData.removeObserver(it) downloadObserver = null } } fun setCastingObserver(owner: LifecycleOwner?, observer: Observer) { if (owner == null) return this.castingObserver = observer CastUtil.get().casting.observe(owner, observer) } fun unsetCastingObserver() { castingObserver?.let { CastUtil.get().casting.removeObserver(it) castingObserver = null } } fun setQueueObserver(queueLivedata: LiveData, owner: LifecycleOwner?, observer: Observer) { if (owner == null) return this.queueLiveData = queueLivedata this.queueObserver = observer this.queueLiveData.observe(owner, observer) } fun unsetQueueObserver() { queueObserver?.let { queueLiveData.removeObserver(it) queueObserver = null } } fun setDownloaded(downloaded: Boolean, isCasting: Boolean) { noCrash { inDown.post { if (downloaded) inDown.setImageResource(R.drawable.ic_chap_down) if (isCasting) inDown.setImageResource(R.drawable.ic_casting) inDown.visibility = if (downloaded || isCasting) View.VISIBLE else View.GONE } } } fun setQueue(isInQueue: Boolean, isDownloaded: Boolean) { noCrash { inDown.post { if (!isInQueue) setDownloaded(isDownloaded, false) else { inDown.setImageResource(if (isDownloaded) R.drawable.ic_queue_file else R.drawable.ic_queue_normal) inDown.visibility = View.VISIBLE } } } } fun setSeen(seen: Boolean) { chapter.post { chapter.setTextColor(ContextCompat.getColor(App.context, if (seen) EAHelper.getThemeColor() else R.color.textPrimary)) } } fun setDownloadState(downloadObject: DownloadObject?) { progressBar.post { if (downloadObject != null && PrefsUtil.showProgress()) when (downloadObject.state) { DownloadObject.PENDING -> { progressBarRoot.visibility = View.VISIBLE progressBar.isIndeterminate = true } DownloadObject.PAUSED, DownloadObject.DOWNLOADING -> { progressBarRoot.visibility = View.VISIBLE progressBar.isIndeterminate = false if (downloadObject.getEta() == -2L || PrefsUtil.downloaderType == 0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(downloadObject.progress, true) else progressBar.progress = downloadObject.progress else { progressBar.progress = 0 progressBar.secondaryProgress = downloadObject.progress } } else -> progressBarRoot.visibility = View.GONE } else progressBarRoot.visibility = View.GONE } } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeInfo.kt ================================================ package knf.kuma.animeinfo import knf.kuma.commons.PatternUtil import knf.kuma.pojos.AnimeObject import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale import java.util.regex.Pattern class AnimeInfo(code: String) { var aid: String? = null var title: String? = null var sid: String? = null var day: AnimeObject.Day? = null get() { try { if (date == null) return AnimeObject.Day.NONE val calendar = Calendar.getInstance() calendar.time = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date) return when (calendar.get(Calendar.DAY_OF_WEEK)) { 2 -> AnimeObject.Day.MONDAY 3 -> AnimeObject.Day.TUESDAY 4 -> AnimeObject.Day.WEDNESDAY 5 -> AnimeObject.Day.THURSDAY 6 -> AnimeObject.Day.FRIDAY 7 -> AnimeObject.Day.SATURDAY 1 -> AnimeObject.Day.SUNDAY else -> AnimeObject.Day.NONE } } catch (e: Exception) { //e.printStackTrace() return AnimeObject.Day.NONE } } var epMap: HashMap private var date: String? = null init { val matcher = Pattern.compile("\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\",?").matcher(code) var i = 0 while (matcher.find()) { when (i) { 0 -> this.aid = matcher.group(1) 1 -> this.title = PatternUtil.fromHtml(matcher.group(1)).replace("\\", "") 2 -> this.sid = matcher.group(1) 3 -> this.date = matcher.group(1) } i++ } this.day = day this.epMap = PatternUtil.getEpListMap(code) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimePagerAdapter.kt ================================================ package knf.kuma.animeinfo import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import knf.kuma.animeinfo.fragments.ChaptersFragment import knf.kuma.animeinfo.fragments.DetailsFragment class AnimePagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { private val detailsFragment = DetailsFragment.get() private val chaptersFragment = ChaptersFragment.get() override fun getPageTitle(position: Int): CharSequence? { return when (position) { 1 -> "EPISODIOS" else -> "INFO" } } fun onChaptersReselect() { chaptersFragment.onReselect() } override fun getItem(position: Int): Fragment { return when (position) { 1 -> chaptersFragment else -> detailsFragment } } override fun getCount(): Int { return 2 } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimePagerAdapterMaterial.kt ================================================ package knf.kuma.animeinfo import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import knf.kuma.animeinfo.fragments.ChaptersFragmentMaterial import knf.kuma.animeinfo.fragments.DetailsFragmentMaterial class AnimePagerAdapterMaterial(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { private val detailsFragment = DetailsFragmentMaterial.get() private val chaptersFragment = ChaptersFragmentMaterial.get() override fun getPageTitle(position: Int): CharSequence? { return when (position) { 0 -> "INFO" 1 -> "EPISODIOS" else -> "INFO" } } fun onChaptersReselect() { chaptersFragment.onReselect() } override fun getItem(position: Int): Fragment { return when (position) { 0 -> detailsFragment 1 -> chaptersFragment else -> detailsFragment } } override fun getCount(): Int { return 2 } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeRelatedAdapter.kt ================================================ package knf.kuma.animeinfo 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.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.load import knf.kuma.databinding.ItemRelatedBinding import knf.kuma.pojos.AnimeObject internal class AnimeRelatedAdapter(private val fragment: Fragment, private val list: MutableList) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RelatedHolder { return RelatedHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_related, parent, false)) } override fun onBindViewHolder(holder: RelatedHolder, position: Int) { val related = list[position] holder.textView.text = related.name holder.relation.text = related.relation if (related.aid != "null") { holder.imageView.visibility = View.VISIBLE holder.imageView.load(PatternUtil.getCover(related.aid)) holder.cardView.setOnClickListener { ActivityAnime.open(fragment, related, holder.imageView) } } else { holder.imageView.visibility = View.GONE holder.cardView.setOnClickListener { ActivityAnime.open(fragment, related) } } } override fun getItemCount(): Int { return list.size } internal class RelatedHolder(itemView: View, binding: ItemRelatedBinding = ItemRelatedBinding.bind(itemView)) : RecyclerView.ViewHolder(itemView) { val cardView: LinearLayout = binding.card val imageView: ImageView = binding.img val textView: TextView = binding.title val relation: TextView = binding.relation } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeRelatedAdapterMaterial.kt ================================================ package knf.kuma.animeinfo 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.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.load import knf.kuma.databinding.ItemRelatedBinding import knf.kuma.pojos.AnimeObject internal class AnimeRelatedAdapterMaterial(private val fragment: Fragment, private val list: MutableList) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RelatedHolder { return RelatedHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_related, parent, false)) } override fun onBindViewHolder(holder: RelatedHolder, position: Int) { val related = list[position] holder.textView.text = related.name holder.relation.text = related.relation if (related.aid != "null") { holder.imageView.visibility = View.VISIBLE holder.imageView.load(PatternUtil.getCover(related.aid)) holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, related, holder.imageView) } } else { holder.imageView.visibility = View.GONE holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, related) } } } override fun getItemCount(): Int { return list.size } internal class RelatedHolder(itemView: View, binding: ItemRelatedBinding = ItemRelatedBinding.bind(itemView)) : RecyclerView.ViewHolder(itemView) { val cardView: LinearLayout = binding.card val imageView: ImageView = binding.img val textView: TextView = binding.title val relation: TextView = binding.relation } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeTagsAdapter.kt ================================================ package knf.kuma.animeinfo import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.search.GenreActivity import org.jetbrains.anko.find internal class AnimeTagsAdapter(private val context: Context, private val list: MutableList) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagHolder { return TagHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_chip, parent, false)) } override fun onBindViewHolder(holder: TagHolder, position: Int) { holder.chip.text = list[position] holder.chip.setOnClickListener { GenreActivity.open(context, list[holder.adapterPosition]) } } override fun getItemCount(): Int { return list.size } internal class TagHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var chip: TextView = itemView.find(R.id.chip) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeTagsAdapterMaterial.kt ================================================ package knf.kuma.animeinfo import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.search.GenreActivityMaterial import org.jetbrains.anko.find internal class AnimeTagsAdapterMaterial(private val context: Context, private val list: MutableList) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagHolder { return TagHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_chip, parent, false)) } override fun onBindViewHolder(holder: TagHolder, position: Int) { holder.chip.text = list[position] holder.chip.setOnClickListener { GenreActivityMaterial.open(context, list[holder.adapterPosition]) } } override fun getItemCount(): Int { return list.size } internal class TagHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var chip: TextView = itemView.find(R.id.chip) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/AnimeViewModel.kt ================================================ package knf.kuma.animeinfo import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.jsoupCookies import knf.kuma.commons.noCrashLet import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject import knf.kuma.retrofit.Repository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync class AnimeViewModel : ViewModel() { private val repository = Repository() val liveData: MutableLiveData = MutableLiveData() fun init(context: Context, link: String?, persist: Boolean) { link?.let { if (it.contains("/ver/")) { viewModelScope.launch { val nLink = withContext(Dispatchers.IO) { "https://www3.animeflv.net" + noCrashLet { jsoupCookies(it).get().select("a[href~=/anime/]").attr("href") } } repository.getAnime(nLink, persist, liveData) } } else repository.getAnime(link, persist, liveData) } } fun init(aid: String?) { doAsync { aid?.let { val animeObject = CacheDB.INSTANCE.animeDAO().getAnimeByAid(aid) doOnUIGlobal { liveData.value = animeObject } } ?: doOnUIGlobal { liveData.value = null } } } fun reload(context: Context, link: String?, persist: Boolean) { init(context, link, persist) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/BottomActionsDialog.kt ================================================ package knf.kuma.animeinfo import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleObserver import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import knf.kuma.R import knf.kuma.commons.isFullMode import knf.kuma.databinding.LayBottomActionsBinding import org.jetbrains.anko.sdk27.coroutines.onClick import org.jetbrains.anko.support.v4.toast class BottomActionsDialog : BottomSheetDialogFragment(), LifecycleObserver { private var callback: ActionsCallback? = null private var listSize: Int = 0 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.lay_bottom_actions, container, false) val binding = LayBottomActionsBinding.bind(view) if (listSize <= 1) { binding.actionSeen.text = "Marcar como visto" binding.actionUnseen.text = "Marcar como no visto" binding.actionImportAll.text = "Importar archivo" binding.actionDownloadAll.text = "Descargar" } binding.actionSeen.onClick { callback?.onSelect(STATE_SEEN) safeDismiss() } binding.actionUnseen.onClick { callback?.onSelect(STATE_UNSEEN) safeDismiss() } binding.actionImportAll.onClick { callback?.onSelect(STATE_IMPORT_MULTIPLE) safeDismiss() } binding.actionDownloadAll.onClick { if (!isFullMode) { toast("Deshabilitado para esta version") } else { callback?.onSelect(STATE_DOWNLOAD_MULTIPLE) } safeDismiss() } binding.actionQueueAll.onClick { if (!isFullMode) { toast("Deshabilitado para esta version") } else { callback?.onSelect(STATE_QUEUE_MULTIPLE) } safeDismiss() } return view } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) dialog.setOnShowListener { dialogInterface -> try { val d = dialogInterface as? BottomSheetDialog d?.findViewById(com.google.android.material.R.id.design_bottom_sheet)?.also { BottomSheetBehavior.from(it).setState(BottomSheetBehavior.STATE_EXPANDED) } } catch (e: Exception) { // } } return dialog } fun safeShow(manager: FragmentManager, tag: String) { try { show(manager, tag) } catch (e: Exception) { // } } private fun safeDismiss() { try { dismiss() } catch (e: Exception) { // } } /*override fun onDismiss(dialog: DialogInterface) { try { super.onDismiss(dialog) callback?.onDismiss() } catch (e: IllegalArgumentException) { e.printStackTrace() } }*/ override fun onCancel(dialog: DialogInterface) { try { super.onDismiss(dialog) callback?.onDismiss() } catch (e: IllegalArgumentException) { e.printStackTrace() } } interface ActionsCallback { fun onSelect(state: Int) fun onDismiss() } companion object { const val STATE_SEEN = 0 const val STATE_UNSEEN = 1 const val STATE_IMPORT_MULTIPLE = 2 const val STATE_DOWNLOAD_MULTIPLE = 3 const val STATE_QUEUE_MULTIPLE = 4 fun newInstance(listSize: Int, callback: ActionsCallback): BottomActionsDialog { val actionsDialog = BottomActionsDialog() actionsDialog.callback = callback actionsDialog.listSize = listSize return actionsDialog } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/ChapterObjWrap.kt ================================================ package knf.kuma.animeinfo import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject class ChapterObjWrap(val chapter: AnimeObject.WebInfo.AnimeChapter) { var isSeen = CacheDB.INSTANCE.seenDAO().chapterIsSeen(chapter.aid,chapter.number) } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/CommentariesDialog.kt ================================================ package knf.kuma.animeinfo import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.webkit.WebView import android.webkit.WebViewClient import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetDialogFragment import knf.kuma.R import knf.kuma.commons.jsoupCookies import knf.kuma.commons.urlEncode import knf.kuma.databinding.LayCommentsBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class CommentariesDialog : BottomSheetDialogFragment(), LifecycleObserver { private var link: String = "about:blank" private var version: String = "1.0" @SuppressLint("SetJavaScriptEnabled") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.lay_comments, container, false) val binding = LayCommentsBinding.bind(rootView) lifecycleScope.launch { val html = withContext(Dispatchers.IO) { jsoupCookies(link).execute().body() } val regex = Regex("this.page.url = '([^']*)';\\s+this.page.identifier = '([^']*)").find(html) val (target, identifier) = regex!!.destructured val url = "https://disqus.com/embed/comments/?base=default&f=https-myanimelist-net-2&t_i=$identifier&t_u=${urlEncode(target)}&s_o=default#version=$version" binding.webview.apply { setInitialScale(1) settings.useWideViewPort = true settings.loadWithOverviewMode = true settings.javaScriptEnabled = true webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) binding.loading.visibility = View.GONE } } loadUrl(url) } } return rootView } fun setUpOwner(owner: LifecycleOwner) { owner.lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPaused() { safeDismiss() } fun safeShow(manager: FragmentManager, tag: String) { try { show(manager, tag) } catch (e: Exception) { // } } private fun safeDismiss() { try { dismiss() } catch (e: Exception) { // } } companion object { fun show(fragment: Fragment, link: String, version: String) { CommentariesDialog().apply { this.link = link this.version = version setUpOwner(fragment) }.safeShow(fragment.childFragmentManager, "BottomSheetDialog") } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/DownloadedObserver.kt ================================================ package knf.kuma.animeinfo import knf.kuma.achievements.AchievementManager import knf.kuma.commons.FileWrapper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch object DownloadedObserver { private var observer: Job = Job() fun observe(scope: CoroutineScope, size: Int, fileWrapper: FileWrapper<*>) { observer.cancel() observer = Job() scope.launch(Dispatchers.IO + observer) { if (AchievementManager.isUnlocked(35) || size == fileWrapper.parentSize()) { unlock() return@launch } while (isActive) { if (size == fileWrapper.parentSize()) { unlock() return@launch } delay(1500) } } } private fun unlock() { AchievementManager.unlock(listOf(35)) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/fragments/ChaptersFragment.kt ================================================ package knf.kuma.animeinfo.fragments import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Pair import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import knf.kuma.App import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.animeinfo.AnimeViewModel import knf.kuma.animeinfo.ktx.fileName import knf.kuma.animeinfo.viewholders.AnimeChaptersHolder import knf.kuma.commons.EAHelper import knf.kuma.commons.FileUtil import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrashLet import knf.kuma.commons.toast import knf.kuma.custom.snackbar.SnackProgressBar import knf.kuma.custom.snackbar.SnackProgressBarManager import knf.kuma.download.FileAccessHelper import knf.kuma.download.MultipleDownloadManager import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.pojos.AnimeObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import xdroid.toaster.Toaster import java.util.regex.Pattern class ChaptersFragment : BottomFragment(), AnimeChaptersHolder.ChapHolderCallback { private var holder: AnimeChaptersHolder? = null private var moveFile: String? = null private var chapters: MutableList = ArrayList() private lateinit var snackManager: SnackProgressBarManager override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) activity?.let { activity -> ViewModelProvider(activity).get(AnimeViewModel::class.java).liveData.observe(viewLifecycleOwner, Observer { animeObject -> if (animeObject != null) { val chapters = animeObject.chapters chapters?.let { viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { when { checkIntegrity(chapters) -> { if (PrefsUtil.isChapsAsc) chapters.reverse() holder?.setAdapter(this@ChaptersFragment, chapters) holder?.goToChapter() } Network.isConnected -> { DirUpdateWork.runNow() "Integridad de directorio comprometida, actualizando directorio...".toast() } } } } } }) } } private fun checkIntegrity(list: List): Boolean { return try { list.isEmpty() || (list[0].aid != null && list[0].eid != null) } catch (e: Exception) { false } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.recycler_chapters, container, false) holder = AnimeChaptersHolder(view, this, this).also { snackManager = SnackProgressBarManager(it.recyclerView) .setProgressBarColor(EAHelper.getThemeColor()) .setOverlayLayoutAlpha(0.4f) .setOverlayLayoutColor(android.R.color.background_dark) } return view } override fun onReselect() { holder?.smoothGoToChapter() } fun onMove(to: String) { try { this.moveFile = to startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType("video/mp4"), 55698) } catch (e: Exception) { Toaster.toast("Error al importar") } } override fun onImportMultiple(chapters: MutableList) { when (chapters.size) { 0 -> Toaster.toast("No se puede importar ningun episodio") 1 -> { this.moveFile = chapters[0].fileName onMove(chapters[0].fileName) } else -> { try { this.chapters = chapters startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) .setType("video/mp4"), 55698) } catch (e: Exception) { Toaster.toast("Error al importar") } } } } override fun onDownloadMultiple(addQueue: Boolean, chapters: List) { holder?.let { holder -> MultipleDownloadManager.startDownload(this, holder.recyclerView, chapters.sortedBy { noCrashLet(9999) { "(\\d+)".toRegex().findAll(it.number).last().destructured.component1().toInt() } }, addQueue) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) try { holder?.adapter?.isImporting = true if (data?.clipData == null || data.clipData?.itemCount ?: 0 == 0) { if (moveFile == null && chapters.size > 0) { val uri = data?.data val file = DocumentFile.fromSingleUri(App.context, uri ?: Uri.EMPTY) val last = getLastNumber(file?.name) moveFile = findChapter(last)?.fileName } val snackbar = SnackProgressBar(SnackProgressBar.TYPE_HORIZONTAL, "Importando...") .setIsIndeterminate(false) .setProgressMax(100) .setShowProgressPercentage(true) snackManager.show(snackbar, SnackProgressBarManager.LENGTH_INDEFINITE) FileUtil.moveFile(App.context.contentResolver, data?.data, FileAccessHelper.getOutputStream(moveFile)).observe(this, Observer { pair -> try { if (pair != null) { if (pair.second) { if (pair.first == -1) { Toaster.toast("Error al importar") FileAccessHelper.delete(moveFile) } else Toaster.toast("Importado exitosamente") holder?.adapter?.notifyDataSetChanged() moveFile = null snackManager.dismiss() holder?.adapter?.isImporting = false } else snackManager.setProgress(pair.first) } } catch (e: Exception) { Toaster.toast("Error al importar") } }) } else { val snackbar = SnackProgressBar(SnackProgressBar.TYPE_HORIZONTAL, "Importando...") .setIsIndeterminate(false) .setProgressMax(100) .setShowProgressPercentage(true) snackManager.show(snackbar, SnackProgressBarManager.LENGTH_INDEFINITE) val moveRequests = ArrayList>() val count = data.clipData?.itemCount ?: 0 for (i in 0 until count) { try { val uri = data.clipData?.getItemAt(i)?.uri ?: Uri.EMPTY val file = DocumentFile.fromSingleUri(App.context, uri) val last = getLastNumber(file?.name) moveRequests.add(Pair(uri, findChapter(last)?.fileName ?: "")) } catch (e: Exception) { // } } if (moveRequests.size == 0) { Toaster.toast("No se pudo inferir el numero de los episodios") snackManager.dismiss() } else { FileUtil.moveFiles(App.context.contentResolver, moveRequests).observe(this@ChaptersFragment, Observer { pairBooleanPair -> try { if (pairBooleanPair != null) { if (pairBooleanPair.second) { Toaster.toast("Importados ${pairBooleanPair.first.second} archivos exitosamente") holder?.adapter?.notifyDataSetChanged() chapters = ArrayList() snackManager.dismiss() holder?.adapter?.isImporting = false } else { snackbar.setMessage(pairBooleanPair.first.first) snackManager.updateTo(snackbar) snackManager.setProgress(pairBooleanPair.first.second) } } } catch (e: Exception) { Toaster.toast("Error al importar") } }) } } } catch (e: Exception) { e.printStackTrace() Toaster.toast("Error al importar") } } private fun findChapter(num: String?): AnimeObject.WebInfo.AnimeChapter? { for (c in ArrayList(chapters)) { if (c.number == "Episodio $num") { chapters.remove(c) return c } } return null } private fun getLastNumber(name: String?): String? { if (name.isNullOrEmpty()) return null val matcher = Pattern.compile(".*[_ ]0?(\\d+)[_ ].*$|0?(\\d+)$").matcher(name.replace(".mp4", "")) var last: String? = null while (matcher.find()) { try { last = matcher.group(1) if (last == null) last = matcher.group(2) } catch (e: Exception) { try { last = matcher.group(2) } catch (e1: Exception) { e1.printStackTrace() } } } return last } companion object { fun get(): ChaptersFragment { return ChaptersFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/fragments/ChaptersFragmentMaterial.kt ================================================ package knf.kuma.animeinfo.fragments import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Pair import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import knf.kuma.App import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.animeinfo.AnimeViewModel import knf.kuma.animeinfo.ktx.fileName import knf.kuma.animeinfo.viewholders.AnimeChaptersMaterialHolder import knf.kuma.commons.EAHelper import knf.kuma.commons.FileUtil import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrashLet import knf.kuma.commons.toast import knf.kuma.custom.snackbar.SnackProgressBar import knf.kuma.custom.snackbar.SnackProgressBarManager import knf.kuma.download.FileAccessHelper import knf.kuma.download.MultipleDownloadManager import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.pojos.AnimeObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import xdroid.toaster.Toaster import java.util.regex.Pattern class ChaptersFragmentMaterial : BottomFragment(), AnimeChaptersMaterialHolder.ChapHolderCallback { private var holder: AnimeChaptersMaterialHolder? = null private var moveFile: String? = null private var chapters: MutableList = ArrayList() private lateinit var snackManager: SnackProgressBarManager override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) activity?.let { activity -> ViewModelProvider(activity).get(AnimeViewModel::class.java).liveData.observe(viewLifecycleOwner, Observer { animeObject -> if (animeObject != null) { val chapters = animeObject.chapters chapters?.let { viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { when { checkIntegrity(chapters) -> { if (PrefsUtil.isChapsAsc) chapters.reverse() holder?.setAdapter(this@ChaptersFragmentMaterial, chapters) delay(1000) holder?.goToChapter() } Network.isConnected -> { DirUpdateWork.runNow() "Integridad de directorio comprometida, actualizando directorio...".toast() } } } } } }) } } private fun checkIntegrity(list: List): Boolean { return try { list.isEmpty() || (list[0].aid != null && list[0].eid != null) } catch (e: Exception) { false } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.recycler_chapters, container, false) holder = AnimeChaptersMaterialHolder(view, this, this).also { snackManager = SnackProgressBarManager(it.recyclerView) .setProgressBarColor(EAHelper.getThemeColor()) .setOverlayLayoutAlpha(0.4f) .setOverlayLayoutColor(android.R.color.background_dark) } return view } override fun onReselect() { holder?.smoothGoToChapter() } fun onMove(to: String) { this.moveFile = to startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType("video/mp4"), 55698) } override fun onImportMultiple(chapters: MutableList) { when (chapters.size) { 0 -> Toaster.toast("No se puede importar ningun episodio") 1 -> { this.moveFile = chapters[0].fileName onMove(chapters[0].fileName) } else -> { this.chapters = chapters startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) .setType("video/mp4"), 55698) } } } override fun onDownloadMultiple(addQueue: Boolean, chapters: List) { holder?.let { holder -> MultipleDownloadManager.startDownload(this, holder.recyclerView, chapters.sortedBy { noCrashLet(9999) { "(\\d+)".toRegex().findAll(it.number).last().destructured.component1().toInt() } }, addQueue) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) try { holder?.adapter?.isImporting = true if (data?.clipData == null || data.clipData?.itemCount ?: 0 == 0) { if (moveFile == null && chapters.size > 0) { val uri = data?.data val file = DocumentFile.fromSingleUri(App.context, uri ?: Uri.EMPTY) val last = getLastNumber(file?.name) moveFile = findChapter(last)?.fileName } val snackbar = SnackProgressBar(SnackProgressBar.TYPE_HORIZONTAL, "Importando...") .setIsIndeterminate(false) .setProgressMax(100) .setShowProgressPercentage(true) snackManager.show(snackbar, SnackProgressBarManager.LENGTH_INDEFINITE) FileUtil.moveFile(App.context.contentResolver, data?.data, FileAccessHelper.getOutputStream(moveFile)).observe(this, Observer { pair -> try { if (pair != null) { if (pair.second) { if (pair.first == -1) { Toaster.toast("Error al importar") FileAccessHelper.delete(moveFile) } else Toaster.toast("Importado exitosamente") holder?.adapter?.notifyDataSetChanged() moveFile = null snackManager.dismiss() holder?.adapter?.isImporting = false } else snackManager.setProgress(pair.first) } } catch (e: Exception) { Toaster.toast("Error al importar") } }) } else { val snackbar = SnackProgressBar(SnackProgressBar.TYPE_HORIZONTAL, "Importando...") .setIsIndeterminate(false) .setProgressMax(100) .setShowProgressPercentage(true) snackManager.show(snackbar, SnackProgressBarManager.LENGTH_INDEFINITE) val moveRequests = ArrayList>() val count = data.clipData?.itemCount ?: 0 for (i in 0 until count) { try { val uri = data.clipData?.getItemAt(i)?.uri ?: Uri.EMPTY val file = DocumentFile.fromSingleUri(App.context, uri) val last = getLastNumber(file?.name) moveRequests.add(Pair(uri, findChapter(last)?.fileName ?: "")) } catch (e: Exception) { // } } if (moveRequests.size == 0) { Toaster.toast("No se pudo inferir el numero de los episodios") snackManager.dismiss() } else { FileUtil.moveFiles(App.context.contentResolver, moveRequests).observe(this@ChaptersFragmentMaterial, Observer { pairBooleanPair -> try { if (pairBooleanPair != null) { if (pairBooleanPair.second) { Toaster.toast("Importados ${pairBooleanPair.first.second} archivos exitosamente") holder?.adapter?.notifyDataSetChanged() chapters = ArrayList() snackManager.dismiss() holder?.adapter?.isImporting = false } else { snackbar.setMessage(pairBooleanPair.first.first) snackManager.updateTo(snackbar) snackManager.setProgress(pairBooleanPair.first.second) } } } catch (e: Exception) { Toaster.toast("Error al importar") } }) } } } catch (e: Exception) { e.printStackTrace() Toaster.toast("Error al importar") } } private fun findChapter(num: String?): AnimeObject.WebInfo.AnimeChapter? { for (c in ArrayList(chapters)) { if (c.number == "Episodio $num") { chapters.remove(c) return c } } return null } private fun getLastNumber(name: String?): String? { if (name.isNullOrEmpty()) return null val matcher = Pattern.compile(".*[_ ]0?(\\d+)[_ ].*$|0?(\\d+)$").matcher(name.replace(".mp4", "")) var last: String? = null while (matcher.find()) { try { last = matcher.group(1) if (last == null) last = matcher.group(2) } catch (e: Exception) { try { last = matcher.group(2) } catch (e1: Exception) { e1.printStackTrace() } } } return last } companion object { fun get(): ChaptersFragmentMaterial { return ChaptersFragmentMaterial() } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/fragments/DetailsFragment.kt ================================================ package knf.kuma.animeinfo.fragments import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import knf.kuma.R import knf.kuma.animeinfo.AnimeViewModel import knf.kuma.animeinfo.viewholders.AnimeDetailsHolder class DetailsFragment : Fragment() { private var holder: AnimeDetailsHolder? = null private val viewModel: AnimeViewModel by activityViewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.liveData.observe(viewLifecycleOwner, Observer { animeObject -> if (animeObject != null) holder?.populate(this@DetailsFragment, animeObject) }) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return try { val view = inflater.inflate(R.layout.fragment_anime_details, container, false) holder = AnimeDetailsHolder(view) view } catch (e: ExceptionInInitializerError) { null } } companion object { fun get(): DetailsFragment { return DetailsFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/fragments/DetailsFragmentMaterial.kt ================================================ package knf.kuma.animeinfo.fragments import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import knf.kuma.R import knf.kuma.animeinfo.AnimeViewModel import knf.kuma.animeinfo.viewholders.AnimeDetailsMaterialHolder class DetailsFragmentMaterial : Fragment() { private var holder: AnimeDetailsMaterialHolder? = null private val viewModel: AnimeViewModel by activityViewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.liveData.observe(viewLifecycleOwner, Observer { animeObject -> if (animeObject != null) holder?.populate(this@DetailsFragmentMaterial, animeObject) }) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return try { val view = inflater.inflate(R.layout.fragment_anime_details_material, container, false) holder = AnimeDetailsMaterialHolder(view) view } catch (e: ExceptionInInitializerError) { null } } companion object { fun get(): DetailsFragmentMaterial { return DetailsFragmentMaterial() } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/img/ActivityImgFull.kt ================================================ package knf.kuma.animeinfo.img import androidx.activity.addCallback import android.os.Bundle import android.view.ViewGroup import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.preference.PreferenceManager import com.google.android.material.snackbar.Snackbar import knf.kuma.commons.doOnUI import knf.kuma.commons.httpJsoup import knf.kuma.commons.iterator import knf.kuma.commons.safeDismiss import knf.kuma.commons.showSnackbar import knf.kuma.custom.GenericActivity import knf.kuma.databinding.LayoutImgBigBaseBinding import org.jetbrains.anko.doAsync import org.json.JSONObject import java.net.URLEncoder import java.util.Locale class ActivityImgFull : GenericActivity() { private val keyTitle = "title" private val binding by lazy { LayoutImgBigBaseBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) binding.pager.adapter = ImgPagerAdapter(supportFragmentManager, intent.getStringExtra(keyTitle) ?: "", listOf(intent.dataString ?: "")) binding.indicator.setViewPager(binding.pager) ViewCompat.setOnApplyWindowInsetsListener(binding.indicator) { _, insets -> binding.indicator.updateLayoutParams { topMargin = topMargin + insets.getInsets(WindowInsetsCompat.Type.statusBars()).top } WindowInsetsCompat.CONSUMED } searchInMAL() onBackPressedDispatcher.addCallback(this) { supportFinishAfterTransition() } } private fun searchInMAL() { if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("scale_img", true)) doAsync { val snackbar = binding.pager.showSnackbar("Buscando mejores imagenes...", Snackbar.LENGTH_INDEFINITE) try { val title = intent.getStringExtra(keyTitle) val response = httpJsoup("https://api.jikan.moe/v4/anime?q=${URLEncoder.encode(title, "utf-8")}&page=1") if (response.statusCode() != 200) throw IllegalStateException("Response code: ${response.statusCode()}") val results = JSONObject( response.body() ?: "{}" ).getJSONArray("data") for (i in 0 until results.length()) { val json = results.getJSONObject(i) val name = json.getJSONArray("titles").getJSONObject(0).getString("title").lowercase(Locale.getDefault()) if (title?.lowercase(Locale.getDefault()) == name) { val list = mutableListOf() //list.add(json.getString("image_url")) try { val picturesResponse = httpJsoup("https://api.jikan.moe/v4/anime/${json.getString("mal_id")}/pictures") if (picturesResponse.statusCode() != 200) throw IllegalStateException("Response code: ${picturesResponse.statusCode()}") val picturesArray = JSONObject( picturesResponse.body() ?: "{}" ).getJSONArray("data") for (item in picturesArray) { list.add(item.getJSONObject("jpg").getString("large_image_url")) } } catch (e: Exception) { e.printStackTrace() } doOnUI { binding.pager.adapter = ImgPagerAdapter(supportFragmentManager, intent.getStringExtra(keyTitle) ?: "", list) binding.indicator.setViewPager(binding.pager) } break } } snackbar.safeDismiss() } catch (e: Exception) { e.printStackTrace() snackbar.safeDismiss() } } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/img/ImgFullFragment.kt ================================================ package knf.kuma.animeinfo.img import android.content.Intent import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.view.MenuItem import android.view.View import android.widget.ProgressBar import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.Fragment import com.bumptech.glide.Glide 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.google.android.material.snackbar.Snackbar import knf.kuma.R import knf.kuma.commons.createSnackbar import knf.kuma.commons.safeDismiss import knf.kuma.commons.showSnackbar import knf.kuma.databinding.LayoutImgBigBinding import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster import java.io.FileOutputStream class ImgFullFragment : Fragment(R.layout.layout_img_big), PopupMenu.OnMenuItemClickListener { private var bitmap: Bitmap? = null private val keyTitle = "title" private lateinit var binding: LayoutImgBigBinding override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = LayoutImgBigBinding.bind(view) Glide.with(this).asBitmap().load(arguments?.getString("img")).listener( object : RequestListener { override fun onLoadFailed( e: GlideException?, model: Any?, target: Target, isFirstResource: Boolean ): Boolean { binding.error.visibility = View.VISIBLE return false } override fun onResourceReady( resource: Bitmap, model: Any, target: Target?, dataSource: DataSource, isFirstResource: Boolean ): Boolean { bitmap = resource return false } } ).into(binding.img) binding.img.setOnLongClickListener { if (bitmap != null) { context?.let { val popupMenu = PopupMenu(it, binding.anchor) popupMenu.inflate(R.menu.menu_img) popupMenu.setOnMenuItemClickListener(this@ImgFullFragment) popupMenu.show() } } true } } override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { R.id.download -> { try { val i = Intent(Intent.ACTION_CREATE_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .setType("image/jpg") .putExtra(Intent.EXTRA_TITLE, arguments?.getString(keyTitle) + ".jpg") startActivityForResult(i, 556) } catch (e: Exception) { Toaster.toast("Error al descargar") } } R.id.share -> try { val intent = Intent(Intent.ACTION_SEND) .setType("image/*") .putExtra(Intent.EXTRA_TEXT, arguments?.getString(keyTitle)) .putExtra(Intent.EXTRA_STREAM, Uri.parse(MediaStore.Images.Media.insertImage(context?.contentResolver, bitmap, "", ""))) startActivity(Intent.createChooser(intent, "Compartir...")) } catch (e: Exception) { Toaster.toast("Error al compartir") } } return true } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val snackbar = binding.img.createSnackbar("Guardando...", Snackbar.LENGTH_INDEFINITE) val progressBar = ProgressBar(context).also { it.isIndeterminate = true } (snackbar.view as Snackbar.SnackbarLayout).addView(progressBar) snackbar.show() doAsync { try { val pfd = context?.contentResolver?.openFileDescriptor(data?.data ?: Uri.EMPTY, "w") pfd?.let { val fileOutputStream = FileOutputStream(it.fileDescriptor) bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream) fileOutputStream.close() it.close() snackbar.safeDismiss() binding.img.showSnackbar("Imagen guardada!") } } catch (e: Exception) { e.printStackTrace() snackbar.safeDismiss() binding.img.showSnackbar("Error al guardar imagen") } } } companion object { fun create(img: String, title: String): ImgFullFragment { val fragment = ImgFullFragment() fragment.arguments = Bundle().apply { putString("img", img) putString("title", title) } return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/img/ImgPagerAdapter.kt ================================================ package knf.kuma.animeinfo.img import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter class ImgPagerAdapter(fm: FragmentManager, private val title: String, private val list: List) : FragmentPagerAdapter(fm) { override fun getItem(position: Int): Fragment = ImgFullFragment.create(list[position], title) override fun getCount(): Int = list.size } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/ktx/Extensions.kt ================================================ package knf.kuma.animeinfo.ktx import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.pojos.AnimeObject val AnimeObject.WebInfo.AnimeChapter.epTitle: String get() = name + number.substring(number.lastIndexOf(" ")) val AnimeObject.WebInfo.AnimeChapter.fileName: String get() = if (PrefsUtil.saveWithName) eid + "$" + PatternUtil.getFileName(link) else eid + "$" + aid + "-" + number.substring(number.lastIndexOf(" ") + 1) + ".mp4" val AnimeObject.WebInfo.AnimeChapter.filePath: String get() = if (PrefsUtil.saveWithName) "$" + PatternUtil.getFileName(link) else "$" + aid + "-" + number.substring(number.lastIndexOf(" ") + 1) + ".mp4" ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/viewholders/AnimeActivityHolder.kt ================================================ package knf.kuma.animeinfo.viewholders import android.content.Intent import android.graphics.drawable.Drawable import android.net.Uri import android.view.View import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.app.ActivityOptionsCompat import androidx.viewpager.widget.ViewPager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.tabs.TabLayout import knf.kuma.R import knf.kuma.animeinfo.AnimePagerAdapter import knf.kuma.animeinfo.img.ActivityImgFull import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.forceHide import knf.kuma.commons.load import org.jetbrains.anko.sdk27.coroutines.onClick class AnimeActivityHolder(val activity: AppCompatActivity) { val appBarLayout: AppBarLayout by bind(activity, R.id.appBar) private val collapsingToolbarLayout: CollapsingToolbarLayout by bind(activity, R.id.collapsingToolbar) val imageView: ImageView by bind(activity, R.id.img) val toolbar: Toolbar by bind(activity, R.id.toolbar) private val tabLayout: TabLayout by bind(activity, R.id.tabs) val pager: ViewPager by bind(activity, R.id.pager) private val fab: FloatingActionButton by bind(activity, R.id.fab) private val intent: Intent = activity.intent private val animePagerAdapter: AnimePagerAdapter = AnimePagerAdapter(activity.supportFragmentManager) private val innerInterface: Interface = activity as Interface private val drawableHeartFull: Drawable by lazy { activity.getDrawable(R.drawable.heart_full) as Drawable } private val drawableHeartEmpty: Drawable by lazy { activity.getDrawable(R.drawable.heart_empty) as Drawable } init { //fab.visibility = View.INVISIBLE fab.isEnabled = false populate(activity) pager.offscreenPageLimit = 2 pager.adapter = animePagerAdapter tabLayout.setupWithViewPager(pager) pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { } override fun onPageSelected(position: Int) { appBarLayout.setExpanded(position == 0, true) } override fun onPageScrollStateChanged(state: Int) { } }) if (activity.intent.getBooleanExtra("isRecord", false)) pager.setCurrentItem(1, true) tabLayout.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(pager) { override fun onTabReselected(tab: TabLayout.Tab?) { if (tab?.position == 1) { appBarLayout.setExpanded(false, true) animePagerAdapter.onChaptersReselect() } } }) fab.onClick { innerInterface.onFabClicked(fab) } imageView.onClick { innerInterface.onImgClicked(imageView) } } fun setTitle(title: String) { collapsingToolbarLayout.post { collapsingToolbarLayout.title = title } } fun loadImg(link: String, listener: View.OnClickListener) { imageView.post { imageView.load(link) imageView.setOnClickListener(listener) } } fun setFABState(isFav: Boolean) { activity.doOnUI { fab.setImageDrawable(if (isFav) drawableHeartFull else drawableHeartEmpty) fab.invalidate() } } fun showFAB() { activity.doOnUI { fab.isEnabled = true //fab.show() } } fun hideFABForce() { fab.isEnabled = false fab.forceHide() } private fun populate(activity: AppCompatActivity) { val title = intent.getStringExtra("title") if (title != null) collapsingToolbarLayout.title = title val img = intent.getStringExtra("img") if (img != null) { imageView.load(img) imageView.setOnClickListener { activity.startActivity(Intent(activity, ActivityImgFull::class.java).setData(Uri.parse(img)).putExtra("title", title), ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, "img").toBundle()) } } } interface Interface { fun onFabClicked(actionButton: FloatingActionButton) fun onImgClicked(imageView: ImageView) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/viewholders/AnimeActivityMaterialHolder.kt ================================================ package knf.kuma.animeinfo.viewholders import android.content.Intent import android.graphics.drawable.Drawable import android.net.Uri import android.view.View import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.app.ActivityOptionsCompat import androidx.viewpager.widget.ViewPager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.tabs.TabLayout import knf.kuma.R import knf.kuma.animeinfo.AnimePagerAdapterMaterial import knf.kuma.animeinfo.img.ActivityImgFull import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.forceHide import knf.kuma.commons.load import org.jetbrains.anko.sdk27.coroutines.onClick class AnimeActivityMaterialHolder(val activity: AppCompatActivity) { val appBarLayout: AppBarLayout by bind(activity, R.id.appBar) private val collapsingToolbarLayout: CollapsingToolbarLayout by bind(activity, R.id.collapsingToolbar) val imageView: ImageView by bind(activity, R.id.img) val toolbar: Toolbar by bind(activity, R.id.toolbar) private val tabLayout: TabLayout by bind(activity, R.id.tabs) val pager: ViewPager by bind(activity, R.id.pager) private val fab: FloatingActionButton by bind(activity, R.id.fab) private val intent: Intent = activity.intent private val animePagerAdapter: AnimePagerAdapterMaterial = AnimePagerAdapterMaterial(activity.supportFragmentManager) private val innerInterface: Interface = activity as Interface private val drawableHeartFull: Drawable by lazy { activity.getDrawable(R.drawable.heart_full) as Drawable } private val drawableHeartEmpty: Drawable by lazy { activity.getDrawable(R.drawable.heart_empty) as Drawable } init { //fab.visibility = View.INVISIBLE fab.isEnabled = false populate(activity) pager.offscreenPageLimit = 2 pager.adapter = animePagerAdapter tabLayout.setupWithViewPager(pager) pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { } override fun onPageSelected(position: Int) { appBarLayout.setExpanded(position == 0, true) } override fun onPageScrollStateChanged(state: Int) { } }) if (activity.intent.getBooleanExtra("isRecord", false)) pager.setCurrentItem(1, true) tabLayout.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(pager) { override fun onTabReselected(tab: TabLayout.Tab?) { if (tab?.position == 1) { appBarLayout.setExpanded(false, true) animePagerAdapter.onChaptersReselect() } } }) fab.onClick { innerInterface.onFabClicked(fab) } imageView.onClick { innerInterface.onImgClicked(imageView) } } fun setTitle(title: String) { collapsingToolbarLayout.post { collapsingToolbarLayout.title = title } } fun loadImg(link: String, listener: View.OnClickListener) { imageView.post { imageView.load(link) imageView.setOnClickListener(listener) } } fun setFABState(isFav: Boolean) { activity.doOnUI { fab.setImageDrawable(if (isFav) drawableHeartFull else drawableHeartEmpty) fab.invalidate() } } fun showFAB() { activity.doOnUI { fab.isEnabled = true //fab.show() } } fun hideFABForce() { fab.isEnabled = false fab.forceHide() } private fun populate(activity: AppCompatActivity) { val title = intent.getStringExtra("title") if (title != null) collapsingToolbarLayout.title = title val img = intent.getStringExtra("img") if (img != null) { imageView.load(img) imageView.setOnClickListener { activity.startActivity(Intent(activity, ActivityImgFull::class.java).setData(Uri.parse(img)).putExtra("title", title), ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, "img").toBundle()) } } } interface Interface { fun onFabClicked(actionButton: FloatingActionButton) fun onImgClicked(imageView: ImageView) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/viewholders/AnimeChaptersHolder.kt ================================================ package knf.kuma.animeinfo.viewholders import android.net.Uri import android.view.View import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import com.michaelflisar.dragselectrecyclerview.DragSelectTouchListener import com.michaelflisar.dragselectrecyclerview.DragSelectionProcessor import knf.kuma.R import knf.kuma.animeinfo.AnimeChaptersAdapter import knf.kuma.animeinfo.BottomActionsDialog import knf.kuma.animeinfo.ChapterObjWrap import knf.kuma.backup.firestore.syncData import knf.kuma.commons.doOnUI import knf.kuma.commons.safeDismiss import knf.kuma.commons.showSnackbar import knf.kuma.custom.CenterLayoutManager import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jetbrains.anko.find class AnimeChaptersHolder(view: View, private val fragment: Fragment, private val callback: ChapHolderCallback) { val recyclerView: RecyclerView = view.find(R.id.recycler) private val manager: LinearLayoutManager = CenterLayoutManager(view.context) private var chapters: MutableList = ArrayList() var adapter: AnimeChaptersAdapter? = null private set private val touchListener: DragSelectTouchListener init { manager.isSmoothScrollbarEnabled = true recyclerView.layoutManager = manager touchListener = DragSelectTouchListener() .withSelectListener(DragSelectionProcessor(object : DragSelectionProcessor.ISelectionHandler { override fun getSelection(): Set { return adapter?.selection ?: setOf() } override fun isSelected(i: Int): Boolean { return adapter?.selection?.contains(i) ?: false } override fun updateSelection(i: Int, i1: Int, b: Boolean, b1: Boolean) { adapter?.selectRange(i, i1, b) } }).withStartFinishedListener(object : DragSelectionProcessor.ISelectionStartFinishedListener { override fun onSelectionStarted(i: Int, b: Boolean) { } override fun onSelectionFinished(i: Int) { BottomActionsDialog.newInstance( adapter?.selection?.size ?: 0, object : BottomActionsDialog.ActionsCallback { override fun onSelect(state: Int) { try { val snackbar = recyclerView.showSnackbar( "Procesando...", duration = Snackbar.LENGTH_INDEFINITE ) when (state) { BottomActionsDialog.STATE_SEEN -> doAsync { val dao = CacheDB.INSTANCE.seenDAO() for (i13 in ArrayList( adapter?.selection ?: arrayListOf() )) { dao.addChapter(SeenObject.fromChapter(chapters[i13])) } syncData { seen() } val seeingDAO = CacheDB.INSTANCE.seeingDAO() val seeingObject = seeingDAO.getByAid(chapters[0].aid) if (seeingObject != null) { seeingObject.chapter = chapters[0].number seeingDAO.update(seeingObject) syncData { seeing() } } fragment.doOnUI { adapter?.apply { if (selection.isNotEmpty()) { selection.forEach { this.chapters[it].isSeen = true } deselectAll() } } } fragment.doOnUI { snackbar.safeDismiss() } } BottomActionsDialog.STATE_UNSEEN -> doAsync { try { val dao = CacheDB.INSTANCE.seenDAO() for (i12 in ArrayList(adapter?.selection ?: arrayListOf())) { dao.deleteChapter(chapters[i12].aid, chapters[i12].number) } syncData { seen() } val seeingDAO = CacheDB.INSTANCE.seeingDAO() val seeingObject = seeingDAO.getByAid(chapters[0].aid) if (seeingObject != null) { seeingObject.chapter = chapters[0].number seeingDAO.update(seeingObject) syncData { seeing() } } fragment.doOnUI { adapter?.apply { if (selection.isNotEmpty()) { selection.forEach { this.chapters[it].isSeen = false } deselectAll() } } } fragment.doOnUI { snackbar.safeDismiss() } } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } BottomActionsDialog.STATE_IMPORT_MULTIPLE -> doAsync { try { val cChapters = ArrayList() val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() for (i13 in ArrayList(adapter?.selection ?: arrayListOf())) { val chapter = chapters[i13] val downloadObject = downloadsDAO.getByEid(chapter.eid) if (!chapter.fileWrapper().exist && (downloadObject == null || !downloadObject.isDownloading)) cChapters.add(chapter) } callback.onImportMultiple(cChapters) recyclerView.post { adapter?.deselectAll() } fragment.doOnUI { snackbar.safeDismiss() } } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } BottomActionsDialog.STATE_DOWNLOAD_MULTIPLE -> doAsync { try { val cChapters = mutableListOf() val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() for (i13 in ArrayList(adapter?.selection ?: arrayListOf())) { val chapter = chapters[i13] val downloadObject = downloadsDAO.getByEid(chapter.eid) if (!chapter.fileWrapper().exist && (downloadObject == null || !downloadObject.isDownloading)) cChapters.add(chapter) } recyclerView.post { adapter?.deselectAll() } fragment.doOnUI { snackbar.safeDismiss() } callback.onDownloadMultiple(false, cChapters) } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } BottomActionsDialog.STATE_QUEUE_MULTIPLE -> doAsync { try { val cChapters = mutableListOf() val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() for (i13 in ArrayList(adapter?.selection ?: arrayListOf())) { val chapter = chapters[i13] val downloadObject = downloadsDAO.getByEid(chapter.eid) if (!chapter.fileWrapper().exist && (downloadObject == null || !downloadObject.isDownloading)) cChapters.add(chapter) else if (chapter.fileWrapper().exist || downloadObject?.isDownloading == true) QueueManager.add(Uri.fromFile(chapter.fileWrapper().file()), true, chapter) } recyclerView.post { adapter?.deselectAll() } fragment.doOnUI { snackbar.safeDismiss() } callback.onDownloadMultiple(true, cChapters) } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } } } catch (e: Exception) { // } } override fun onDismiss() { recyclerView.post { adapter?.deselectAll() } } }).safeShow(fragment.childFragmentManager, "actions_dialog") } }).withMode(DragSelectionProcessor.Mode.Simple)) .withMaxScrollDistance(32) } fun setAdapter(fragment: Fragment, chapters: MutableList?) { if (chapters == null) return fragment.lifecycleScope.launch(Dispatchers.IO) { this@AnimeChaptersHolder.chapters = chapters this@AnimeChaptersHolder.adapter = AnimeChaptersAdapter(fragment, recyclerView, chapters.map { ChapterObjWrap(it) }, touchListener) recyclerView.post { recyclerView.adapter = adapter recyclerView.addOnItemTouchListener(touchListener) } } } fun refresh() { if (adapter != null) recyclerView.post { adapter?.notifyDataSetChanged() } } fun goToChapter() { fragment.lifecycleScope.launch(Dispatchers.IO) { if (chapters.isNotEmpty()) { val eids = chapters.sortedBy { it.number.substringAfterLast(" ").toFloat() }.map { it.eid } eids.chunked(50).forEach { list -> val chapter = CacheDB.INSTANCE.seenDAO().getLast(list) if (chapter != null) { val position = chapters.indexOf(chapters.find { it.eid == chapter.eid }) if (position >= 0) launch(Dispatchers.Main) { manager.scrollToPositionWithOffset(position, 150) } return@forEach } } } } } fun smoothGoToChapter() { fragment.lifecycleScope.launch(Dispatchers.IO) { if (chapters.isNotEmpty()) { val eids = chapters.sortedBy { it.number.substringAfterLast(" ").toFloat() }.map { it.eid } eids.chunked(50).forEach { list -> val chapter = CacheDB.INSTANCE.seenDAO().getLast(list) if (chapter != null) { val position = chapters.indexOf(chapters.find { it.eid == chapter.eid }) if (position >= 0) recyclerView.post { manager.smoothScrollToPosition( recyclerView, null, position ) } return@forEach } } } } } interface ChapHolderCallback { fun onImportMultiple(chapters: MutableList) fun onDownloadMultiple(addQueue: Boolean, chapters: List) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/viewholders/AnimeChaptersMaterialHolder.kt ================================================ package knf.kuma.animeinfo.viewholders import android.net.Uri import android.view.View import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import com.michaelflisar.dragselectrecyclerview.DragSelectTouchListener import com.michaelflisar.dragselectrecyclerview.DragSelectionProcessor import knf.kuma.R import knf.kuma.animeinfo.AnimeChaptersAdapterMaterial import knf.kuma.animeinfo.BottomActionsDialog import knf.kuma.animeinfo.ChapterObjWrap import knf.kuma.backup.firestore.syncData import knf.kuma.commons.doOnUI import knf.kuma.commons.safeDismiss import knf.kuma.commons.showSnackbar import knf.kuma.custom.CenterLayoutManager import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jetbrains.anko.find class AnimeChaptersMaterialHolder(view: View, private val fragment: Fragment, private val callback: ChapHolderCallback) { val recyclerView: RecyclerView = view.find(R.id.recycler) private val manager: LinearLayoutManager = CenterLayoutManager(view.context) private var chapters: MutableList = ArrayList() var adapter: AnimeChaptersAdapterMaterial? = null private set private val touchListener: DragSelectTouchListener init { manager.isSmoothScrollbarEnabled = true recyclerView.layoutManager = manager touchListener = DragSelectTouchListener() .withSelectListener(DragSelectionProcessor(object : DragSelectionProcessor.ISelectionHandler { override fun getSelection(): Set { return adapter?.selection ?: setOf() } override fun isSelected(i: Int): Boolean { return adapter?.selection?.contains(i) ?: false } override fun updateSelection(i: Int, i1: Int, b: Boolean, b1: Boolean) { adapter?.selectRange(i, i1, b) } }).withStartFinishedListener(object : DragSelectionProcessor.ISelectionStartFinishedListener { override fun onSelectionStarted(i: Int, b: Boolean) { } override fun onSelectionFinished(i: Int) { BottomActionsDialog.newInstance( adapter?.selection?.size ?: 0, object : BottomActionsDialog.ActionsCallback { override fun onSelect(state: Int) { try { val snackbar = recyclerView.showSnackbar( "Procesando...", duration = Snackbar.LENGTH_INDEFINITE ) when (state) { BottomActionsDialog.STATE_SEEN -> doAsync { val dao = CacheDB.INSTANCE.seenDAO() for (i13 in ArrayList( adapter?.selection ?: arrayListOf() )) { dao.addChapter(SeenObject.fromChapter(chapters[i13])) } syncData { seen() } val seeingDAO = CacheDB.INSTANCE.seeingDAO() val seeingObject = seeingDAO.getByAid(chapters[0].aid) if (seeingObject != null) { seeingObject.chapter = chapters[0].number seeingDAO.update(seeingObject) syncData { seeing() } } fragment.doOnUI { adapter?.apply { if (selection.isNotEmpty()) { selection.forEach { this.chapters[it].isSeen = true } deselectAll() } } } fragment.doOnUI { snackbar.safeDismiss() } } BottomActionsDialog.STATE_UNSEEN -> doAsync { try { val dao = CacheDB.INSTANCE.seenDAO() for (i12 in ArrayList(adapter?.selection ?: arrayListOf())) { dao.deleteChapter(chapters[i12].aid, chapters[i12].number) } syncData { seen() } val seeingDAO = CacheDB.INSTANCE.seeingDAO() val seeingObject = seeingDAO.getByAid(chapters[0].aid) if (seeingObject != null) { seeingObject.chapter = chapters[0].number seeingDAO.update(seeingObject) syncData { seeing() } } fragment.doOnUI { adapter?.apply { if (selection.isNotEmpty()) { selection.forEach { this.chapters[it].isSeen = false } deselectAll() } } } fragment.doOnUI { snackbar.safeDismiss() } } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } BottomActionsDialog.STATE_IMPORT_MULTIPLE -> doAsync { try { val cChapters = ArrayList() val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() for (i13 in ArrayList(adapter?.selection ?: arrayListOf())) { val chapter = chapters[i13] val downloadObject = downloadsDAO.getByEid(chapter.eid) if (!chapter.fileWrapper().exist && (downloadObject == null || !downloadObject.isDownloading)) cChapters.add(chapter) } callback.onImportMultiple(cChapters) recyclerView.post { adapter?.deselectAll() } fragment.doOnUI { snackbar.safeDismiss() } } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } BottomActionsDialog.STATE_DOWNLOAD_MULTIPLE -> doAsync { try { val cChapters = mutableListOf() val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() for (i13 in ArrayList(adapter?.selection ?: arrayListOf())) { val chapter = chapters[i13] val downloadObject = downloadsDAO.getByEid(chapter.eid) if (!chapter.fileWrapper().exist && (downloadObject == null || !downloadObject.isDownloading)) cChapters.add(chapter) } recyclerView.post { adapter?.deselectAll() } fragment.doOnUI { snackbar.safeDismiss() } callback.onDownloadMultiple(false, cChapters) } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } BottomActionsDialog.STATE_QUEUE_MULTIPLE -> doAsync { try { val cChapters = mutableListOf() val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() for (i13 in ArrayList(adapter?.selection ?: arrayListOf())) { val chapter = chapters[i13] val downloadObject = downloadsDAO.getByEid(chapter.eid) if (!chapter.fileWrapper().exist && (downloadObject == null || !downloadObject.isDownloading)) cChapters.add(chapter) else if (chapter.fileWrapper().exist || downloadObject?.isDownloading == true) QueueManager.add(Uri.fromFile(chapter.fileWrapper().file()), true, chapter) } recyclerView.post { adapter?.deselectAll() } fragment.doOnUI { snackbar.safeDismiss() } callback.onDownloadMultiple(true, cChapters) } catch (e: Exception) { e.printStackTrace() fragment.doOnUI { snackbar.safeDismiss() } } } } } catch (e: Exception) { // } } override fun onDismiss() { adapter?.deselectAll() } }).safeShow(fragment.childFragmentManager, "actions_dialog") } }).withMode(DragSelectionProcessor.Mode.Simple)) .withMaxScrollDistance(32) } fun setAdapter(fragment: Fragment, chapters: MutableList?) { if (chapters == null) return fragment.lifecycleScope.launch(Dispatchers.IO){ this@AnimeChaptersMaterialHolder.chapters = chapters this@AnimeChaptersMaterialHolder.adapter = AnimeChaptersAdapterMaterial(fragment, recyclerView, chapters.map { ChapterObjWrap(it) }, touchListener) recyclerView.post { recyclerView.adapter = adapter recyclerView.addOnItemTouchListener(touchListener) } } } fun refresh() { if (adapter != null) recyclerView.post { adapter?.notifyDataSetChanged() } } fun goToChapter() { fragment.lifecycleScope.launch(Dispatchers.IO){ if (chapters.isNotEmpty()) { val eids = chapters.sortedBy { it.number.substringAfterLast(" ").toFloat() }.map { it.eid } eids.chunked(50).forEach { list -> val chapter = CacheDB.INSTANCE.seenDAO().getLast(list) if (chapter != null) { val position = chapters.indexOf(chapters.find { it.eid == chapter.eid }) if (position >= 0) launch(Dispatchers.Main) { manager.scrollToPositionWithOffset(position, 150) } return@forEach } } } } } fun smoothGoToChapter() { fragment.lifecycleScope.launch(Dispatchers.IO) { if (chapters.isNotEmpty()) { val eids = chapters.sortedBy { it.number.substringAfterLast(" ").toFloat() }.map { it.eid } eids.chunked(50).forEach { list -> val chapter = CacheDB.INSTANCE.seenDAO().getLast(list) if (chapter != null) { val position = chapters.indexOf(chapters.find { it.eid == chapter.eid }) if (position >= 0) recyclerView.post { manager.smoothScrollToPosition( recyclerView, null, position ) } return@forEach } } } } } interface ChapHolderCallback { fun onImportMultiple(chapters: MutableList) fun onDownloadMultiple(addQueue: Boolean, chapters: List) } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/viewholders/AnimeDetailsHolder.kt ================================================ package knf.kuma.animeinfo.viewholders import android.annotation.SuppressLint import android.content.ClipData import android.view.View import android.view.animation.AnimationUtils import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.ImageButton import android.widget.LinearLayout import android.widget.Spinner import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView import ir.mahdiparastesh.chlm.ChipsLayoutManager import ir.mahdiparastesh.chlm.SpacingItemDecoration import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.animeinfo.AnimeRelatedAdapter import knf.kuma.animeinfo.AnimeTagsAdapter import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrash import knf.kuma.commons.removeAllDecorations import knf.kuma.custom.ExpandableTV import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.databinding.FragmentAnimeDetailsBinding import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.SeeingObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.clipboardManager import org.jetbrains.anko.doAsync import uz.jamshid.library.ExactRatingBar import xdroid.toaster.Toaster import androidx.core.view.isVisible class AnimeDetailsHolder(val view: View) { private val binding = FragmentAnimeDetailsBinding.bind(view) private var cardViews: MutableList = arrayListOf(binding.cardTitle, binding.cardDesc, binding.adContainer, binding.cardDetails, binding.cardGenres, binding.cardList, binding.cardRelated) internal val title: TextView = binding.title private val expandIcon: ImageButton = binding.expandIcon private val desc: ExpandableTV = binding.expandableDesc internal val type: TextView = binding.type internal val state: TextView = binding.state internal val id: TextView = binding.aid internal val followers: TextView = binding.followers private val layScore: LinearLayout = binding.layScore private val ratingCount: TextView = binding.ratingCount private val ratingBar: ExactRatingBar = binding.ratingBar private val recyclerViewGenres: RecyclerView = binding.recyclerGenres private val spinnerList: Spinner = binding.spinnerList private val recyclerViewRelated: RecyclerView = binding.recyclerRelated private val clipboardManager = view.context.applicationContext.clipboardManager private var retard = 0 private var needAnimation = true init { recyclerViewGenres.layoutManager = ChipsLayoutManager.newBuilder(view.context).build() recyclerViewGenres.addItemDecoration(SpacingItemDecoration(5, 5)) recyclerViewRelated.layoutManager = VariantLinearLayoutManager(view.context) } @SuppressLint("SetTextI18n") fun populate(fragment: Fragment, animeObject: AnimeObject) { fragment.lifecycleScope.launch(Dispatchers.Main) { title.text = animeObject.name noCrash { cardViews[0].setOnLongClickListener { try { val clip = ClipData.newPlainText("Anime title", animeObject.name) clipboardManager.setPrimaryClip(clip) Toaster.toast("Título copiado") } catch (e: Exception) { e.printStackTrace() Toaster.toast("Error al copiar título") } true } showCard(cardViews[0]) } noCrash { if (animeObject.description != null && animeObject.description?.trim() != "") { desc.setTextAndIndicator(animeObject.description?.trim() ?: "", expandIcon) desc.setAnimationDuration(300) val onClickListener = View.OnClickListener { expandIcon.setImageResource(if (desc.isExpanded) R.drawable.action_expand else R.drawable.action_shrink) desc.toggle() } desc.setOnClickListener(onClickListener) expandIcon.setOnClickListener(onClickListener) showCard(cardViews[1]) } } noCrash { if (PrefsUtil.isAdsEnabled) { showCard(cardViews[2]) } } noCrash { type.text = animeObject.type state.text = getStateString(animeObject.state, animeObject.day) id.text = animeObject.aid followers.text = animeObject.followers if (animeObject.rate_stars == null || animeObject.rate_stars == "0.0") layScore.visibility = View.GONE else { ratingCount.text = "${animeObject.rate_count} (${animeObject.rate_stars ?: "?.?"})" ratingBar.setStar(animeObject.rate_stars?.toFloat() ?: 0f) } showCard(cardViews[3]) } noCrash { fragment.context?.let { context -> if (animeObject.genres?.isNotEmpty() == true && animeObject.genresString.trim().let { it != "" && it != "Sin generos" }) { recyclerViewGenres.adapter = AnimeTagsAdapter(context, animeObject.genres) showCard(cardViews[4]) } } } noCrash { spinnerList.adapter = ArrayAdapter(view.context, android.R.layout.simple_spinner_dropdown_item, view.context.resources.getStringArray(R.array.list_states)) fragment.lifecycleScope.launch(Dispatchers.Main){ spinnerList.setSelection(withContext(Dispatchers.IO){ CacheDB.INSTANCE.seeingDAO().getByAid(animeObject.aid) }?.state ?: 0) spinnerList.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) { } override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { doAsync { if (position == 0) CacheDB.INSTANCE.seeingDAO().remove(SeeingObject.fromAnime(animeObject, position)) else CacheDB.INSTANCE.seeingDAO().add(SeeingObject.fromAnime(animeObject, position)) syncData { seeing() } } } } } showCard(cardViews[5]) } noCrash { if (animeObject.related?.isNotEmpty() == true) { recyclerViewRelated.removeAllDecorations() if (animeObject.related.size > 1) recyclerViewRelated.addItemDecoration(DividerItemDecoration(view.context, LinearLayout.VERTICAL)) recyclerViewRelated.adapter = AnimeRelatedAdapter(fragment, animeObject.related) showCard(cardViews[6]) } else { launch(Dispatchers.Main) { cardViews[6].visibility = View.GONE } } } needAnimation = false if (PrefsUtil.isAdsEnabled) { fragment.lifecycleScope.launch(Dispatchers.IO) { retard += 300 delay(retard.toLong()) binding.adContainer.implBanner(AdsType.INFO_BANNER, true) } } } } private fun showCard(view: View) { if (view.isVisible || !needAnimation) return retard += 100 GlobalScope.launch(Dispatchers.Main) { delay(retard.toLong()) view.visibility = View.VISIBLE val animation = AnimationUtils.makeInChildBottomAnimation(view.context) animation.duration = 250 view.startAnimation(animation) if (cardViews.indexOf(view) == 1) desc.checkIndicator() } } private fun getStateString(state: String?, day: AnimeObject.Day): String { return when (day) { AnimeObject.Day.MONDAY -> "$state - Lunes" AnimeObject.Day.TUESDAY -> "$state - Martes" AnimeObject.Day.WEDNESDAY -> "$state - Miércoles" AnimeObject.Day.THURSDAY -> "$state - Jueves" AnimeObject.Day.FRIDAY -> "$state - Viernes" AnimeObject.Day.SATURDAY -> "$state - Sábado" AnimeObject.Day.SUNDAY -> "$state - Domingo" else -> state ?: "" } } } ================================================ FILE: app/src/main/java/knf/kuma/animeinfo/viewholders/AnimeDetailsMaterialHolder.kt ================================================ package knf.kuma.animeinfo.viewholders //import knf.kuma.ads.NativeManager import android.annotation.SuppressLint import android.content.ClipData import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.ImageButton import android.widget.LinearLayout import android.widget.Spinner import android.widget.TextView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import ir.mahdiparastesh.chlm.ChipsLayoutManager import ir.mahdiparastesh.chlm.SpacingItemDecoration import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.AdsUtils import knf.kuma.ads.implBanner import knf.kuma.animeinfo.AnimeRelatedAdapterMaterial import knf.kuma.animeinfo.AnimeTagsAdapterMaterial import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PrefsUtil import knf.kuma.commons.isVisibleAnimate import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashSuspend import knf.kuma.commons.removeAllDecorations import knf.kuma.custom.ExpandableTV import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.databinding.FragmentAnimeDetailsMaterialBinding import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.SeeingObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.clipboardManager import org.jetbrains.anko.doAsync import uz.jamshid.library.ExactRatingBar import xdroid.toaster.Toaster class AnimeDetailsMaterialHolder(val view: View) { private val binding = FragmentAnimeDetailsMaterialBinding.bind(view) private var layouts: MutableList = arrayListOf(binding.layTitle, binding.layDescription, binding.adContainer, binding.layDetails, binding.layGenres, binding.layFollow, binding.layRelated) internal val title: TextView = binding.title private val expandIcon: ImageButton = binding.expandIcon private val desc: ExpandableTV = binding.expandableDesc internal val type: TextView = binding.type internal val state: TextView = binding.state internal val id: TextView = binding.aid internal val followers: TextView = binding.followers private val layScore: LinearLayout = binding.layScore private val ratingCount: TextView = binding.ratingCount private val ratingBar: ExactRatingBar = binding.ratingBar private val recyclerViewGenres: RecyclerView = binding.recyclerGenres private val spinnerList: Spinner = binding.spinnerList private val recyclerViewRelated: RecyclerView = binding.recyclerRelated private val clipboardManager = view.context.applicationContext.clipboardManager private var needAnimation = true init { recyclerViewGenres.layoutManager = ChipsLayoutManager.newBuilder(view.context).build() recyclerViewGenres.addItemDecoration(SpacingItemDecoration(5, 5)) recyclerViewRelated.layoutManager = VariantLinearLayoutManager(view.context) binding.layAd.isVisible = PrefsUtil.isAdsEnabled } @SuppressLint("SetTextI18n") fun populate(fragment: Fragment, animeObject: AnimeObject) { fragment.lifecycleScope.launch(Dispatchers.Main) { title.text = animeObject.name noCrash { layouts[0].setOnLongClickListener { try { val clip = ClipData.newPlainText("Anime title", animeObject.name) clipboardManager.setPrimaryClip(clip) Toaster.toast("Título copiado") } catch (e: Exception) { e.printStackTrace() Toaster.toast("Error al copiar título") } true } showLayout(layouts[0]) } noCrash { if (animeObject.description != null && animeObject.description?.isBlank() == false) { desc.setTextAndIndicator(animeObject.description?.trim() ?: "", expandIcon) desc.setAnimationDuration(300) val onClickListener = View.OnClickListener { expandIcon.setImageResource(if (desc.isExpanded) R.drawable.action_expand else R.drawable.action_shrink) desc.toggle() } desc.setOnClickListener(onClickListener) expandIcon.setOnClickListener(onClickListener) showLayout(layouts[1]) } else { binding.layDescriptionSeparator.isVisible = false } } if (PrefsUtil.isAdsEnabled) { launch { noCrashSuspend { binding.adContainer.isVisible = false binding.layAd.implBanner(AdsType.INFO_BANNER) } } showLayout(layouts[2]) } noCrash { type.text = animeObject.type state.text = getStateString(animeObject.state, animeObject.day) id.text = animeObject.aid followers.text = animeObject.followers if (animeObject.rate_stars == null || animeObject.rate_stars == "0.0") layScore.visibility = View.GONE else { ratingCount.text = "${animeObject.rate_count} (${ animeObject.rate_stars ?: "?.?" })" ratingBar.setStar(animeObject.rate_stars?.toFloat() ?: 0f) } showLayout(layouts[3]) } noCrash { fragment.context?.let { context -> if (animeObject.genres?.isNotEmpty() == true && animeObject.genresString.trim().let { it != "" && it != "Sin generos" }) { recyclerViewGenres.adapter = AnimeTagsAdapterMaterial(context, animeObject.genres) showLayout(layouts[4]) } } } if (!layouts[5].isVisible) noCrash { spinnerList.adapter = ArrayAdapter(view.context, android.R.layout.simple_spinner_dropdown_item, view.context.resources.getStringArray(R.array.list_states)) fragment.lifecycleScope.launch(Dispatchers.Main) { spinnerList.onItemSelectedListener = null spinnerList.setSelection(withContext(Dispatchers.IO) { CacheDB.INSTANCE.seeingDAO().getByAid(animeObject.aid) }?.state ?: 0) spinnerList.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) { } override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { doAsync { if (position == 0) CacheDB.INSTANCE.seeingDAO().remove(SeeingObject.fromAnime(animeObject, position)) else CacheDB.INSTANCE.seeingDAO().add(SeeingObject.fromAnime(animeObject, position)) syncData { seeing() } } } } } showLayout(layouts[5]) } noCrash { if (animeObject.related?.isNotEmpty() == true) { recyclerViewRelated.removeAllDecorations() recyclerViewRelated.adapter = AnimeRelatedAdapterMaterial(fragment, animeObject.related) showLayout(layouts[6]) } else { launch(Dispatchers.Main) { layouts[6].visibility = View.GONE } } } needAnimation = false } } private fun showLayout(view: View) { if (view.visibility == View.VISIBLE || !needAnimation) return view.isVisibleAnimate = true if (layouts.indexOf(view) == 1) desc.checkIndicator() } private fun getStateString(state: String?, day: AnimeObject.Day): String { return when (day) { AnimeObject.Day.MONDAY -> "$state - Lunes" AnimeObject.Day.TUESDAY -> "$state - Martes" AnimeObject.Day.WEDNESDAY -> "$state - Miércoles" AnimeObject.Day.THURSDAY -> "$state - Jueves" AnimeObject.Day.FRIDAY -> "$state - Viernes" AnimeObject.Day.SATURDAY -> "$state - Sábado" AnimeObject.Day.SUNDAY -> "$state - Domingo" else -> state ?: "" } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/BackUpActivity.kt ================================================ package knf.kuma.backup import android.animation.Animator import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.Rect import android.os.Bundle import android.view.View import android.view.ViewAnimationUtils import android.view.animation.AccelerateDecelerateInterpolator import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.MaterialDialog import com.dropbox.core.android.Auth import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.backup.framework.BackupService import knf.kuma.backup.framework.DropBoxService import knf.kuma.backup.framework.LocalService import knf.kuma.commons.EAHelper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.admFile import knf.kuma.commons.noCrash import knf.kuma.commons.safeShow import knf.kuma.commons.showSnackbar import knf.kuma.custom.GenericActivity import knf.kuma.custom.SyncItemView import knf.kuma.databinding.ActivityLoginBinding import kotlin.math.max class BackUpActivity : GenericActivity(), SyncItemView.OnClick { val binding by lazy { ActivityLoginBinding.inflate(layoutInflater) } private val syncItems: MutableList by lazy { with(binding.layButtons) { arrayListOf(syncFavs, syncHistory, syncFollowing, syncSeen, syncSeenNew) } } private var service: BackupService? = null private var waitingLogin = false private val backColor: Int @ColorInt get() { return when (Backups.type) { Backups.Type.NONE -> ContextCompat.getColor(this, android.R.color.transparent) Backups.Type.LOCAL -> ContextCompat.getColor(this, EAHelper.getThemeColorLight()) Backups.Type.DROPBOX -> ContextCompat.getColor(this, R.color.dropbox) Backups.Type.FIRESTORE -> ContextCompat.getColor(this, R.color.firestore) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!resources.getBoolean(R.bool.isTablet)) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT setContentView(binding.root) service = Backups.createService() binding.layMain.loginDropbox.setOnClickListener { onDropBoxLogin() } if (!PrefsUtil.isAdsEnabled && !BuildConfig.DEBUG && !admFile.exists() && !PrefsUtil.isSubscriptionEnabled) { binding.layMain.loginFirestore.isEnabled = false binding.layMain.adsRequired.visibility = View.VISIBLE } else if (PrefsUtil.isAdsEnabled && Network.isAdsBlocked && !BuildConfig.DEBUG && !admFile.exists() && !PrefsUtil.isSubscriptionEnabled) { binding.layMain.loginFirestore.isEnabled = false binding.layMain.adsRequired.text = "Anuncios bloqueados por host" binding.layMain.adsRequired.visibility = View.VISIBLE } else if (!PrefsUtil.isSecurityUpdated && PrefsUtil.spProtectionEnabled && PrefsUtil.spErrorType != null) { binding.layMain.loginFirestore.isEnabled = false binding.layMain.adsRequired.text = "Proveedor de seguridad no pudo ser actualizado (${PrefsUtil.spErrorType})" binding.layMain.adsRequired.visibility = View.VISIBLE } FirestoreManager.start() binding.layMain.loginFirestore.setOnClickListener { onFirestoreLogin() } binding.layMain.loginLocal.setOnClickListener { onLocalLogin() } binding.layButtons.logOut.setOnClickListener { onLogOut() } binding.layFirestore.logOutFirestore.setOnClickListener { onLogOut() } when { service?.isLoggedIn == true -> { setState(true) showColor(savedInstanceState == null) initSyncButtons() } FirestoreManager.isLoggedIn -> { setState(true) showColor(savedInstanceState == null) initFirestoreSync() } else -> setState(false) } } private fun initSyncButtons() { for (itemView in syncItems) { itemView.init(service, this) } } private fun initFirestoreSync() { AchievementManager.onBackup() binding.layFirestore.staticSyncAchievements.suscribe(this, FirestoreManager.achievementsLiveData) binding.layFirestore.staticSyncEA.suscribe(this, FirestoreManager.eaLiveData) binding.layFirestore.staticSyncFavs.suscribe(this, FirestoreManager.favsLiveData) binding.layFirestore.staticSyncGenres.suscribe(this, FirestoreManager.genresLiveData) binding.layFirestore.staticSyncHistory.suscribe(this, FirestoreManager.historyLiveData) binding.layFirestore.staticSyncQueue.suscribe(this, FirestoreManager.queueLiveData) binding.layFirestore.staticSyncSeeing.suscribe(this, FirestoreManager.seeingLiveData) binding.layFirestore.staticSyncSeen.suscribe(this, FirestoreManager.seenLiveData) } private fun clearSyncButtons() { for (itemView in syncItems) { itemView.clear() } } private fun onDropBoxLogin() { waitingLogin = true service = DropBoxService().also { it.logIn() } } private fun onFirestoreLogin() { FirestoreManager.doLogin(this) } private fun onLocalLogin() { MaterialDialog(this).safeShow { message(text = "Los datos se quedarán en la memoria, no se podrá sincronizar datos entre dispositivos, usar este método?") positiveButton(text = "usar") { service = LocalService().also { it.start() it.logIn() } onLogin() } negativeButton(text = "cancelar") } } override fun onAction(syncItemView: SyncItemView, id: String, isBackup: Boolean) { noCrash { if (isBackup) Backups.backup(binding.colorChanger, service, id) { noCrash { if (it == null) binding.colorChanger.showSnackbar("Error al respaldar") syncItemView.enableBackup(it, this@BackUpActivity) } } else Backups.restoreDialog(this, binding.colorChanger, id, syncItemView.backupObj) } } private fun onLogOut() { MaterialDialog(this).safeShow { message(text = "Los datos no respaldados podrian ser perdidos al borrar la app, ¿desea continuar?") positiveButton(text = "continuar") { if (Backups.type == Backups.Type.FIRESTORE) { FirestoreManager.doSignOut(this@BackUpActivity) Backups.type = Backups.Type.NONE revertColor() setState(false) } else { PreferenceManager.getDefaultSharedPreferences(this@BackUpActivity).edit().putString("auto_backup", "0").apply() service?.logOut() service = null Backups.type = Backups.Type.NONE revertColor() setState(false) clearSyncButtons() } } negativeButton(text = "cancelar") } } private fun onLogin() { if (service?.isLoggedIn == true || FirestoreManager.isLoggedIn) { setState(true) showColor(true) initSyncButtons() } else if (waitingLogin) { binding.colorChanger.showSnackbar("Error al iniciar sesión") } waitingLogin = false } private fun showColor(animate: Boolean) { binding.colorChanger.post { try { binding.colorChanger.setBackgroundColor(backColor) if (animate) { val bounds = Rect() binding.colorChanger.getDrawingRect(bounds) val centerX = bounds.centerX() val centerY = bounds.centerY() val finalRadius = max(bounds.width(), bounds.height()) val animator = ViewAnimationUtils.createCircularReveal(binding.colorChanger, centerX, centerY, 0f, finalRadius.toFloat()) animator.duration = 1000 animator.interpolator = AccelerateDecelerateInterpolator() binding.colorChanger.visibility = View.VISIBLE animator.start() } else { binding.colorChanger.visibility = View.VISIBLE } } catch (e: Exception) { e.printStackTrace() } } } private fun revertColor() { binding.colorChanger.post { val bounds = Rect() binding.colorChanger.getDrawingRect(bounds) val centerX = bounds.centerX() val centerY = bounds.centerY() val finalRadius = max(bounds.width(), bounds.height()) val animator = ViewAnimationUtils.createCircularReveal(binding.colorChanger, centerX, centerY, finalRadius.toFloat(), 0f) animator.duration = 1000 animator.interpolator = AccelerateDecelerateInterpolator() animator.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) { } override fun onAnimationEnd(animation: Animator) { binding.colorChanger.visibility = View.INVISIBLE } override fun onAnimationCancel(animation: Animator) { } override fun onAnimationRepeat(animation: Animator) { } }) animator.start() } } private fun setState(isLoggedIn: Boolean) { runOnUiThread { binding.layMain.root.visibility = if (isLoggedIn) View.GONE else View.VISIBLE when (Backups.type) { Backups.Type.LOCAL, Backups.Type.DROPBOX -> { binding.layButtons.root.visibility = if (isLoggedIn) View.VISIBLE else View.GONE binding.layFirestore.root.visibility = View.GONE } Backups.Type.FIRESTORE -> { binding.layFirestore.root.visibility = if (isLoggedIn) View.VISIBLE else View.GONE binding.layButtons.root.visibility = View.GONE } Backups.Type.NONE -> { binding.layButtons.root.visibility = if (isLoggedIn) View.VISIBLE else View.GONE binding.layFirestore.root.visibility = if (isLoggedIn) View.VISIBLE else View.GONE } } } } override fun onResume() { super.onResume() if (waitingLogin) { val token = Auth.getOAuth2Token() if (service is DropBoxService && service?.logIn(token) == true) { Backups.type = Backups.Type.DROPBOX } onLogin() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (FirestoreManager.handleLogin(this, requestCode, resultCode, data)) { initFirestoreSync() } onLogin() } companion object { fun start(context: Context) { context.startActivity(Intent(context, BackUpActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/Backups.kt ================================================ package knf.kuma.backup import android.content.Context import android.view.View import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.snackbar.Snackbar import com.google.gson.reflect.TypeToken import knf.kuma.App import knf.kuma.achievements.AchievementManager import knf.kuma.backup.framework.BackupService import knf.kuma.backup.framework.DropBoxService import knf.kuma.backup.framework.LocalService import knf.kuma.backup.objects.BackupObject import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.safeDismiss import knf.kuma.commons.safeShow import knf.kuma.commons.showSnackbar import knf.kuma.database.CacheDB import knf.kuma.pojos.Achievement import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.AutoBackupObject import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeeingObject import knf.kuma.pojos.SeenObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale object Backups { private const val keyFavs = "favs" private const val keyHistory = "history" private const val keyFollowing = "following" private const val keySeen = "seen" private const val keySeenNew = "seencompact" const val keyAchievements = "achievements" const val keyAutoBackup = "autobackup" var type: Type get() = when (PreferenceManager.getDefaultSharedPreferences(App.context).getInt("backup_type", -1)) { 2 -> Type.FIRESTORE 1 -> Type.DROPBOX 0 -> Type.LOCAL else -> Type.NONE } set(type) = PreferenceManager.getDefaultSharedPreferences(App.context).edit().putInt("backup_type", type.value).apply() fun createService(): BackupService? = when (type) { Type.DROPBOX -> DropBoxService() Type.LOCAL -> LocalService() else -> null }?.also { it.start() } fun search(backupService: BackupService? = null, id: String, onFound: (backupObject: BackupObject<*>?) -> Unit = {}) { GlobalScope.launch(Dispatchers.IO) { val service = backupService ?: createService() service?.search(id)?.let { onFound(it) } ?: onFound(null) } } fun backup(view: View? = null, backupService: BackupService? = null, id: String, onBackup: (backupObject: BackupObject<*>?) -> Unit = {}) { GlobalScope.launch(Dispatchers.IO) { val snackbar = view?.showSnackbar("Respaldando...", Snackbar.LENGTH_INDEFINITE) val service = backupService ?: createService() service?.backup(BackupObject(getList(id)), id)?.let { onBackup(it) } ?: onBackup(null) doOnUIGlobal { snackbar?.safeDismiss() } } } fun backupAll() { GlobalScope.launch(Dispatchers.IO) { val service = createService() service?.backup(BackupObject(getList(keyFavs)), keyFavs) service?.backup(BackupObject(getList(keyHistory)), keyHistory) service?.backup(BackupObject(getList(keyFollowing)), keyFollowing) service?.backup(BackupObject(getList(keySeen)), keySeen) service?.backup(BackupObject(getList(keySeenNew)), keySeenNew) } } fun restoreDialog(context: Context?, view: View, id: String, backupObject: BackupObject<*>?) { if (backupObject != null) context?.let { MaterialDialog(it).safeShow { message(text = "¿Como desea restaurar?") positiveButton(text = "mezclar") { restore(view, false, id, backupObject) } negativeButton(text = "reemplazar") { restore(view, true, id, backupObject) } } } } fun restoreAll() { GlobalScope.launch(Dispatchers.IO) { val service = createService() service?.search(keyFavs)?.let { restore(null, false, keyFavs, it) } service?.search(keyHistory)?.let { restore(null, false, keyHistory, it) } service?.search(keyFollowing)?.let { restore(null, false, keyFollowing, it) } service?.search(keySeen)?.let { restore(null, false, keySeen, it) } service?.search(keySeenNew)?.let { restore(null, false, keySeenNew, it) } } } private fun restore(view: View? = null, replace: Boolean, id: String, backupObject: BackupObject<*>) { val snackbar = view?.showSnackbar("Restaurando...", Snackbar.LENGTH_INDEFINITE) doAsync { try { when (id) { keyFavs -> { if (replace) CacheDB.INSTANCE.favsDAO().clear() (backupObject.data?.filterIsInstance() as? MutableList)?.let { CacheDB.INSTANCE.favsDAO().addAll(it) } } keyHistory -> { if (replace) CacheDB.INSTANCE.recordsDAO().clear() (backupObject.data?.filterIsInstance() as? MutableList)?.let { CacheDB.INSTANCE.recordsDAO().addAll(it) } } keyFollowing -> { if (replace) CacheDB.INSTANCE.seeingDAO().clear() (backupObject.data?.filterIsInstance() as? MutableList)?.let { CacheDB.INSTANCE.seeingDAO().addAll(it) } } keySeen -> { if (replace) { CacheDB.INSTANCE.chaptersDAO().clear() CacheDB.INSTANCE.seenDAO().clear() } (backupObject.data?.filterIsInstance() as? MutableList)?.let { CacheDB.INSTANCE.seenDAO().addAll(it.map { SeenObject.fromChapter(it) }) } } keySeenNew -> { if (replace) CacheDB.INSTANCE.seenDAO().clear() (backupObject.data?.filterIsInstance() as? MutableList)?.let { CacheDB.INSTANCE.seenDAO().addAll(it) } } } snackbar?.safeDismiss() view?.showSnackbar("Restauración completada") } catch (e: Exception) { e.printStackTrace() snackbar?.safeDismiss() view?.showSnackbar("Error al restaurar") } } } val isAnimeflvInstalled: Boolean get() = try { App.context.packageManager.getPackageInfo("knf.animeflv", 0) AchievementManager.unlock(listOf(7)) true } catch (e: Exception) { false } val isKeyInstalled: Boolean get() = try { App.context.packageManager.getPackageInfo("knf.kuma.key", 0) true } catch (e: Exception) { false } private fun getList(id: String): List<*> { return when (id) { keyFavs -> CacheDB.INSTANCE.favsDAO().allRaw keyHistory -> CacheDB.INSTANCE.recordsDAO().allRaw keyFollowing -> CacheDB.INSTANCE.seeingDAO().allRaw keySeenNew -> CacheDB.INSTANCE.seenDAO().all keyAchievements -> CacheDB.INSTANCE.achievementsDAO().all else -> mutableListOf() } } fun getType(id: String): java.lang.reflect.Type { return when (id) { keyFavs -> object : TypeToken>() { }.type keyHistory -> object : TypeToken>() { }.type keyFollowing -> object : TypeToken>() { }.type keySeenNew -> object : TypeToken>() { }.type keySeen -> object : TypeToken>() { }.type keyAchievements -> object : TypeToken>() { }.type keyAutoBackup -> object : TypeToken() { }.type else -> object : TypeToken>() { }.type } } fun saveLastBackup() { PrefsUtil.lastBackup = SimpleDateFormat("dd/MM/yyyy kk:mm", Locale.getDefault()).format(Calendar.getInstance().time) } enum class Type(var value: Int) { NONE(-1), LOCAL(0), DROPBOX(1), FIRESTORE(2) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/MigrationActivity.kt ================================================ package knf.kuma.backup import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.Fragment import knf.kuma.R import knf.kuma.backup.screens.MigrateDirectoryFragment import knf.kuma.backup.screens.MigrateSuccessFragment import knf.kuma.backup.screens.MigrateVersionFragment import knf.kuma.commons.PrefsUtil import knf.kuma.custom.GenericActivity import knf.kuma.directory.DirectoryService class MigrationActivity : GenericActivity(), DirectoryService.OnDirStatus { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_migrate) } override fun onResume() { super.onResume() if (MigrateVersionFragment.installedCode < 252) setFragment(MigrateVersionFragment()) else if (!PrefsUtil.isDirectoryFinished) { DirectoryService.run(this) setFragment(MigrateDirectoryFragment[this]) } else setFragment(MigrateSuccessFragment()) } private fun setFragment(fragment: Fragment) { val transaction = supportFragmentManager.beginTransaction() transaction.replace(R.id.root, fragment) transaction.commit() } override fun onFinished() { setFragment(MigrateSuccessFragment()) } companion object { fun start(context: Context) { context.startActivity(Intent(context, MigrationActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/FirestoreManager.kt ================================================ package knf.kuma.backup.firestore import android.app.Activity import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import androidx.lifecycle.MutableLiveData import com.afollestad.materialdialogs.MaterialDialog import com.firebase.ui.auth.AuthUI import com.firebase.ui.auth.IdpResponse import com.google.firebase.Firebase import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.UserProfileChangeRequest import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.ListenerRegistration import com.google.firebase.firestore.QuerySnapshot import com.google.firebase.firestore.firestore import com.google.firebase.firestore.toObject import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.ads.SubscriptionReceiver import knf.kuma.backup.Backups import knf.kuma.backup.firestore.data.AchievementsData import knf.kuma.backup.firestore.data.EAData import knf.kuma.backup.firestore.data.FavsData import knf.kuma.backup.firestore.data.GenresData import knf.kuma.backup.firestore.data.HistoryData import knf.kuma.backup.firestore.data.QueueData import knf.kuma.backup.firestore.data.SeeingData import knf.kuma.backup.firestore.data.SeenData import knf.kuma.backup.firestore.data.TopData import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.admFile import knf.kuma.commons.currentTime import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashExec import knf.kuma.commons.noCrashLet import knf.kuma.commons.safeShow import knf.kuma.database.CacheDB import knf.kuma.database.EADB import knf.kuma.pojos.SeenObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster.toast import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine object FirestoreManager { enum class State { IDLE, UPLOAD, SYNC } val firestoreDB by lazy { Firebase.firestore } val user: FirebaseUser? get() = FirebaseAuth.getInstance().currentUser val uid: String? get() = user?.uid private val listeners = mutableListOf() val isLoggedIn: Boolean get() = uid != null val favsLiveData = MutableLiveData(State.IDLE) val seenLiveData = MutableLiveData(State.IDLE) val eaLiveData = MutableLiveData(State.IDLE) val achievementsLiveData = MutableLiveData(State.IDLE) val genresLiveData = MutableLiveData(State.IDLE) val historyLiveData = MutableLiveData(State.IDLE) val queueLiveData = MutableLiveData(State.IDLE) val seeingLiveData = MutableLiveData(State.IDLE) private var isUpdateBlocked = false var isFirestoreEnabled = false @OptIn(ExperimentalContracts::class) fun start() { if (!isGPlayServicesEnabled() || isFirestoreEnabled) return if (isLoggedIn && ((PrefsUtil.isAdsEnabled && !Network.isAdsBlocked) || BuildConfig.DEBUG || admFile.exists() || PrefsUtil.isSubscriptionEnabled)) { isFirestoreEnabled = true QueueManager.open() doAsync { firestoreDB.document("users/$uid/backups/history").addSnapshotListener { documentSnapshot, firebaseFirestoreException -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { historyLiveData.value = State.SYNC } documentSnapshot.toObject()?.list?.let { CacheDB.INSTANCE.recordsDAO().apply { clear() addAll(it) } PrefsUtil.lsHistory = currentTime() Log.e("Firestore", "History updated") } runBlocking(Dispatchers.Main) { historyLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.document("users/$uid/backups/achievements").addSnapshotListener { documentSnapshot, firebaseFirestoreException -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { achievementsLiveData.value = State.SYNC } documentSnapshot.toObject()?.list?.let { CacheDB.INSTANCE.achievementsDAO().apply { update(it) } PrefsUtil.lsAchievements = currentTime() Log.e("Firestore", "Achievements updated") } runBlocking(Dispatchers.Main) { achievementsLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.document("users/$uid/backups/ea").addSnapshotListener { documentSnapshot, firebaseFirestoreException -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { eaLiveData.value = State.SYNC } documentSnapshot.toObject()?.list?.let { EADB.INSTANCE.eaDAO().apply { unlock(it) } PrefsUtil.lsEa = currentTime() Log.e("Firestore", "EA Updated") } runBlocking(Dispatchers.Main) { eaLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.document("users/$uid/backups/favs").addSnapshotListener { documentSnapshot, firebaseFirestoreException -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { favsLiveData.value = State.SYNC } documentSnapshot.toObject()?.list?.let { CacheDB.INSTANCE.favsDAO().apply { clear() addAll(it) } PrefsUtil.lsFavs = currentTime() Log.e("Firestore", "Favs updated") } runBlocking(Dispatchers.Main) { favsLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.document("users/$uid/backups/genres").addSnapshotListener { documentSnapshot, firebaseFirestoreException -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { genresLiveData.value = State.SYNC } documentSnapshot.toObject()?.list?.let { CacheDB.INSTANCE.genresDAO().apply { reset() insertStatus(it) } PrefsUtil.lsGenres = currentTime() Log.e("Firestore", "Genres updated") } runBlocking(Dispatchers.Main) { genresLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.document("users/$uid/backups/queue").addSnapshotListener { documentSnapshot, firebaseFirestoreException -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { queueLiveData.value = State.SYNC } documentSnapshot.toObject()?.list?.let { CacheDB.INSTANCE.queueDAO().apply { nuke() add(it) } PrefsUtil.lsQueue = currentTime() Log.e("Firestore", "Queue updated") } runBlocking(Dispatchers.Main) { queueLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.document("users/$uid/backups/seeing").addSnapshotListener { documentSnapshot, firebaseFirestoreException -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { seeingLiveData.value = State.SYNC } documentSnapshot.toObject()?.list?.let { CacheDB.INSTANCE.seeingDAO().apply { clear() addAll(it) } PrefsUtil.lsSeeing = currentTime() Log.e("Firestore", "Seeing updated") } runBlocking(Dispatchers.Main) { seeingLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.collection("users/$uid/backups/seen/data").addSnapshotListener { querySnapshot, firebaseFirestoreException -> doAsync { if (querySnapshot.needsUpdate() && !isUpdateBlocked) { runBlocking(Dispatchers.Main) { seenLiveData.value = State.SYNC } val nList = mutableListOf() querySnapshot.documents.forEach { it.toObject()?.list?.let { seenList -> nList.addAll(seenList) } } CacheDB.INSTANCE.seenDAO().apply { clear() addAll(nList) } PrefsUtil.lsSeen = currentTime() Log.e("Firestore", "Seen updated") runBlocking(Dispatchers.Main) { seenLiveData.value = State.IDLE } } else firebaseFirestoreException?.printStackTrace() } }.also { listeners.add(it) } firestoreDB.document("subscriptions/$uid").get().addOnSuccessListener { if (it.exists() && PrefsUtil.subscriptionToken == null) { noCrash { it.toObject()?.let { GlobalScope.launch(Dispatchers.IO) { val info = SubscriptionReceiver.checkStatus(it.token) if (info.isVerified) { PrefsUtil.subscriptionToken = it.token toast("Suscripción restaurada") } else { firestoreDB.document("subscriptions/$uid").delete() } } } } } } } } else if (isLoggedIn) { doSignOut(App.context) Backups.type = Backups.Type.NONE toast("Firestore deshabilitado") } doAsync { firestoreDB.document("top/${uid ?: PrefsUtil.instanceUuid}").addSnapshotListener { documentSnapshot, _ -> doAsync { if (documentSnapshot.needsUpdate() && !isUpdateBlocked) { documentSnapshot.toObject()?.let { if (it.forced) { user?.updateProfile(UserProfileChangeRequest.Builder().setDisplayName(it.name).build()) ?: { PrefsUtil.instanceName = it.name }() } PrefsUtil.userRewardedVideoCount = it.number Log.e("Firestore", "Top updated") } } } }.also { listeners.add(it) } } } fun stop() { QueueManager.close() listeners.forEach { it.remove() } } private fun uploadAllData(checkForFiles: Boolean, activity: Activity) { if (!isFirestoreEnabled) return if (checkForFiles) firestoreDB.collection("users/$uid/backups").get() .addOnSuccessListener { if (it.isEmpty) { MaterialDialog(activity).safeShow { title(text = "¿Nuevo usuario?") message(text = "Este parece ser tu primer inicio de sesion, tus datos necesitan ser subidos a la nube, primero asegurate que éste sea tu dispositivo principal!") cancelable(false) positiveButton(text = "Subir") { setDefaultDevice() uploadAllData(false, activity) } negativeButton(text = "Cerrar sesion") { doSignOut(activity) } } firestoreDB.document("top/${PrefsUtil.instanceUuid}").delete() } else { firestoreDB.document("users/$uid/backups/info") .get().addOnCompleteListener { document -> val data = document.result?.data if (data != null && data["uuid"] == PrefsUtil.instanceUuid) { MaterialDialog(activity).safeShow { title(text = "Bienvenido de nuevo") message(text = "Actualmente tienes datos en la nube y este es tu dispositivo principal, ¿que datos quieres usar?\n(Usar tus datos locales sobreescribirá lo que haya en la nube)") cancelable(false) positiveButton(text = "Datos locales") { uploadAllData(false, activity) } negativeButton(text = "Descargar de la nube") { start() } } } else { toast("Se descargarán tus datos de la nube") start() } } } } else { stop() isUpdateBlocked = true QueueManager.open() Log.e("Firestore", "On upload all data") syncData { history() seen() achievements() ea() favs() genres() queue() seeing() top() } GlobalScope.launch(Dispatchers.IO) { delay(10000) isUpdateBlocked = false start() } } } private fun setDefaultDevice() { if (!isFirestoreEnabled) return firestoreDB.document("users/$uid/backups/info").set(mapOf("uuid" to PrefsUtil.instanceUuid)) } fun updateHistory(collection: CollectionReference) = noCrash { doOnUIGlobal { historyLiveData.value = State.SYNC } collection.document("history").set(HistoryData.create()).addOnSuccessListener { Log.e("Firestore", "History upload success") PrefsUtil.lsHistory = currentTime() doOnUIGlobal { historyLiveData.value = State.IDLE } }.addOnFailureListener { Log.e("Firestore", "History upload error", it) doOnUIGlobal { historyLiveData.value = State.IDLE } } } fun updateSeen(collection: CollectionReference) = noCrash { doOnUIGlobal { seenLiveData.value = State.SYNC } val data = SeenData.create() val segments = data.list.chunked(10000).map { SeenData(it) } collection.document("seen").set(mapOf("size" to segments.size)) val subcollection = collection.document("seen").collection("data") segments.forEachIndexed { index, seenData -> subcollection.document("seen_$index").set(seenData).addOnSuccessListener { Log.e("Firestore", "Seen_$index upload success") }.addOnFailureListener { Log.e("Firestore", "Seen_$index upload error", it) } } runBlocking { var nextIndex = data.list.size var needsNext = true while (needsNext) { needsNext = suspendCoroutine { noCrashLet(false) { val reference = subcollection.document("seen_$needsNext") reference.get().addOnCompleteListener { subDocument -> noCrashExec(exec = { it.resume(false) }) { if (subDocument.result?.exists() == true) { reference.delete() it.resume(true) } else it.resume(false) } } } } nextIndex++ } } PrefsUtil.lsSeen = currentTime() doOnUIGlobal { seenLiveData.value = State.IDLE } } fun updateAchievements(collection: CollectionReference) = noCrash { doOnUIGlobal { achievementsLiveData.value = State.SYNC } collection.document("achievements").set(AchievementsData.create()).addOnSuccessListener { Log.e("Firestore", "Achievements upload success") PrefsUtil.lsAchievements = currentTime() doOnUIGlobal { achievementsLiveData.value = State.IDLE } }.addOnFailureListener { Log.e("Firestore", "Achievements upload error", it) doOnUIGlobal { achievementsLiveData.value = State.IDLE } } } fun updateEA(collection: CollectionReference) = noCrash { doOnUIGlobal { eaLiveData.value = State.SYNC } collection.document("ea").set(EAData.create()).addOnSuccessListener { Log.e("Firestore", "EA upload success") PrefsUtil.lsEa = currentTime() doOnUIGlobal { eaLiveData.value = State.IDLE } }.addOnFailureListener { Log.e("Firestore", "EA upload error", it) doOnUIGlobal { eaLiveData.value = State.IDLE } } } fun updateFavs(collection: CollectionReference) = noCrash { doOnUIGlobal { favsLiveData.value = State.SYNC } collection.document("favs").set(FavsData.create()).addOnSuccessListener { Log.e("Firestore", "Favs upload success") PrefsUtil.lsFavs = currentTime() doOnUIGlobal { favsLiveData.value = State.IDLE } }.addOnFailureListener { Log.e("Firestore", "Favs upload error", it) doOnUIGlobal { favsLiveData.value = State.IDLE } } } fun updateGenres(collection: CollectionReference) = noCrash { doOnUIGlobal { genresLiveData.value = State.SYNC } collection.document("genres").set(GenresData.create()).addOnSuccessListener { Log.e("Firestore", "Genres upload success") PrefsUtil.lsGenres = currentTime() doOnUIGlobal { genresLiveData.value = State.IDLE } }.addOnFailureListener { Log.e("Firestore", "Genres upload error", it) doOnUIGlobal { genresLiveData.value = State.IDLE } } } fun updateQueue(collection: CollectionReference) = noCrash { doOnUIGlobal { queueLiveData.value = State.SYNC } collection.document("queue").set(QueueData.create()).addOnSuccessListener { Log.e("Firestore", "Queue upload success") PrefsUtil.lsQueue = currentTime() doOnUIGlobal { queueLiveData.value = State.IDLE } }.addOnFailureListener { Log.e("Firestore", "Queue upload error", it) doOnUIGlobal { queueLiveData.value = State.IDLE } } } fun updateSeeing(collection: CollectionReference) = noCrash { doOnUIGlobal { seeingLiveData.value = State.SYNC } collection.document("seeing").set(SeeingData.create()).addOnSuccessListener { Log.e("Firestore", "Seeing upload success") PrefsUtil.lsSeeing = currentTime() doOnUIGlobal { seeingLiveData.value = State.IDLE } }.addOnFailureListener { Log.e("Firestore", "Seeing upload error", it) doOnUIGlobal { seeingLiveData.value = State.IDLE } } } fun updateTop() = doAsync { noCrash { firestoreDB.document("top/${uid ?: PrefsUtil.instanceUuid}").set(TopData.create()) Log.e("Firestore", "Top upload success") } } fun updateTopSync() = noCrash { firestoreDB.document("top/${uid ?: PrefsUtil.instanceUuid}").set(TopData.create()) Log.e("Firestore", "Top upload success") } @ExperimentalContracts fun listenTop(callback: (list: List) -> Unit): ListenerRegistration { var lastUpdate = 0L return firestoreDB.collection("top").addSnapshotListener { querySnapshot, exception -> exception?.let { Log.e("Firestore", "Top Query Error", it) } if (System.currentTimeMillis() >= lastUpdate + 5000) { lastUpdate = System.currentTimeMillis() Log.e("Firestore", "On tops update") querySnapshot?.let { callback(it.documents.mapNotNull { document -> document.toObject() }) } } } } fun doLogin(activity: Activity) { if (isLoggedIn) { MaterialDialog(activity).safeShow { message(text = "Deseas cerrar sesión?") positiveButton(text = "Cerrar sesion") { doSignOut(activity) } } } else { val providers = arrayListOf( AuthUI.IdpConfig.EmailBuilder().build(), AuthUI.IdpConfig.GoogleBuilder().build(), //AuthUI.IdpConfig.TwitterBuilder().build() ) /*if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity) != ConnectionResult.SUCCESS) { providers.removeAt(1) }*/ activity.startActivityForResult( AuthUI.getInstance() .createSignInIntentBuilder() .setAvailableProviders(providers) .setTheme(R.style.AppTheme_FirebaseUI) .setLogo(R.drawable.ic_launcher_login) .setCredentialManagerEnabled(false) .build() , 5548 ) } } fun doSignOut(context: Context) = AuthUI.getInstance().signOut(context) @OptIn(ExperimentalContracts::class) fun handleLogin(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (requestCode == 5548) { val response = IdpResponse.fromResultIntent(data) if (resultCode == Activity.RESULT_OK) { Backups.type = Backups.Type.FIRESTORE uploadAllData(true, activity) return true } else if (response != null) { val error = response.error error?.printStackTrace() toast("Error al iniciar sesion: ${error?.message}") } } return false } private fun isGPlayServicesEnabled(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || PrefsUtil.isSecurityUpdated || !PrefsUtil.spProtectionEnabled } @ExperimentalContracts fun DocumentSnapshot?.needsUpdate(): Boolean { contract { returns(true) implies (this@needsUpdate != null) } return this != null && !this.metadata.hasPendingWrites() && this.exists() } @ExperimentalContracts fun QuerySnapshot?.needsUpdate(): Boolean { contract { returns(true) implies (this@needsUpdate != null) } return this != null && !this.metadata.hasPendingWrites() } class SyncRequest(private val collection: CollectionReference) { private val syncList = mutableListOf<() -> Unit>() fun history() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateHistory(collection) } } fun seen() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateSeen(collection) } } fun achievements() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateAchievements(collection) } } fun ea() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateEA(collection) } } fun favs() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateFavs(collection) } } fun genres() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateGenres(collection) } } fun queue() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateQueue(collection) } } fun seeing() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateSeeing(collection) } } fun top() = syncList.add { runBlocking(Dispatchers.IO) { FirestoreManager.updateTop() } } fun sync() { if (FirestoreManager.isLoggedIn) QueueManager.add(syncList) } } fun syncData(uploads: SyncRequest.() -> Unit) { if (!FirestoreManager.isFirestoreEnabled) return val syncRequest = SyncRequest(FirestoreManager.firestoreDB.collection("users/${FirestoreManager.uid}/backups")) uploads(syncRequest) syncRequest.sync() } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/QueueManager.kt ================================================ package knf.kuma.backup.firestore import knf.kuma.commons.noCrash import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch object QueueManager { private var list = mutableListOf<() -> Unit>() private var isRunning = false private var isClosed = false fun add(items: List<() -> Unit>) { if (isClosed) return if (isRunning) list.addAll(items) else run(items) } fun open() { isRunning = false isClosed = false } fun close() { isClosed = true } private fun run(items: List<() -> Unit> = list) { val tlist = ArrayList(items) list = mutableListOf() isRunning = true GlobalScope.launch(Dispatchers.IO) { tlist.forEach { if (isClosed) return@launch noCrash { it() } } if (list.isNotEmpty()) run() else isRunning = false } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/AchievementsData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.CacheDB import knf.kuma.pojos.Achievement @Keep data class AchievementsData(val list: List = emptyList()) { companion object { fun create(): AchievementsData = AchievementsData(CacheDB.INSTANCE.achievementsDAO().all) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/EAData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.EADB import knf.kuma.pojos.EAObject @Keep data class EAData(val list: List = emptyList()) { companion object { fun create(): EAData = EAData(EADB.INSTANCE.eaDAO().all) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/FavsData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.CacheDB import knf.kuma.pojos.FavoriteObject @Keep data class FavsData(val list: List = emptyList()) { companion object { fun create(): FavsData = FavsData(CacheDB.INSTANCE.favsDAO().allRaw) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/GenresData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.CacheDB import knf.kuma.pojos.GenreStatusObject @Keep data class GenresData(val list: List = emptyList()) { companion object { fun create(): GenresData = GenresData(CacheDB.INSTANCE.genresDAO().all) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/HistoryData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.CacheDB import knf.kuma.pojos.RecordObject @Keep data class HistoryData(val list: List = emptyList()) { companion object { fun create(): HistoryData = HistoryData(CacheDB.INSTANCE.recordsDAO().allRaw) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/QueueData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.CacheDB import knf.kuma.pojos.QueueObject @Keep data class QueueData(val list: List = emptyList()) { companion object { fun create(): QueueData = QueueData(CacheDB.INSTANCE.queueDAO().allRaw) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/SeeingData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.CacheDB import knf.kuma.pojos.SeeingObject @Keep data class SeeingData(val list: List = emptyList()) { companion object { fun create(): SeeingData = SeeingData(CacheDB.INSTANCE.seeingDAO().allRaw) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/SeenData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.database.CacheDB import knf.kuma.pojos.SeenObject @Keep data class SeenData(val list: List = emptyList()) { companion object { fun create(): SeenData = SeenData(CacheDB.INSTANCE.seenDAO().all) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/firestore/data/TopData.kt ================================================ package knf.kuma.backup.firestore.data import androidx.annotation.Keep import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.PrefsUtil @Keep data class TopData(val uid: String = "", val name: String = "", val number: Int = 0, val forced: Boolean = false) { companion object { fun create() = FirestoreManager.user?.let { TopData(it.uid, it.displayName ?: "Anónimo", PrefsUtil.userRewardedVideoCount) } ?: TopData(PrefsUtil.instanceUuid, PrefsUtil.instanceName, PrefsUtil.userRewardedVideoCount) } } ================================================ FILE: app/src/main/java/knf/kuma/backup/framework/BackupService.kt ================================================ package knf.kuma.backup.framework import knf.kuma.backup.Backups import knf.kuma.backup.objects.BackupObject import knf.kuma.commons.decrypt import knf.kuma.commons.encrypt abstract class BackupService { abstract fun start() abstract val isLoggedIn: Boolean abstract fun logIn(token: String? = null): Boolean abstract fun logOut() abstract suspend fun search(id: String, manual: Boolean = false): BackupObject<*>? abstract suspend fun backup(backupObject: BackupObject<*>, id: String): BackupObject<*>? fun String.checkResponse(id: String): String = if (id == Backups.keyAchievements) this.decrypt() ?: this else this fun String.checkData(id: String): String = if (id == Backups.keyAchievements) this.encrypt() ?: this else this } ================================================ FILE: app/src/main/java/knf/kuma/backup/framework/DropBoxService.kt ================================================ package knf.kuma.backup.framework import androidx.preference.PreferenceManager import com.dropbox.core.DbxRequestConfig import com.dropbox.core.android.Auth import com.dropbox.core.http.OkHttp3Requestor import com.dropbox.core.v2.DbxClientV2 import com.dropbox.core.v2.files.WriteMode import com.google.gson.Gson import knf.kuma.App import knf.kuma.backup.Backups import knf.kuma.backup.objects.BackupObject import knf.kuma.commons.noCrash import knf.kuma.commons.toast import java.io.ByteArrayInputStream import java.io.InputStreamReader import java.nio.charset.StandardCharsets class DropBoxService : BackupService() { private var client: DbxClientV2? = null private var dbToken: String? get() = PreferenceManager.getDefaultSharedPreferences(App.context).getString("db_token", null) set(value) = PreferenceManager.getDefaultSharedPreferences(App.context).edit().putString("db_token", value).apply() override fun start() { if (dbToken != null) logIn(dbToken) } override val isLoggedIn: Boolean get() = client != null override fun logIn(token: String?): Boolean { return if (token != null) { dbToken = token val requestConfig = DbxRequestConfig.newBuilder("dropbox_app") .withHttpRequestor(OkHttp3Requestor(OkHttp3Requestor.defaultOkHttpClient())) .build() client = DbxClientV2(requestConfig, token) true } else { noCrash { Auth.startOAuth2Authentication(App.context, "qtjow4hsk06vt19") }?.let { "Error al iniciar sesión en dropbox".toast() } false } } override fun logOut() { client = null dbToken = null } override suspend fun search(id: String, manual: Boolean): BackupObject<*>? { return if (isLoggedIn) try { val list = client?.files()?.searchV2(id)?.matches ?: arrayListOf() if (list.size > 0) { val downloader = client?.files()?.download("/$id") val backupObject = InputStreamReader(downloader?.inputStream).use { Gson().fromJson(it.readText().checkResponse(id), Backups.getType(id)) as BackupObject<*> } downloader?.close() backupObject } else null } catch (e: Exception) { e.printStackTrace() null } else null } override suspend fun backup(backupObject: BackupObject<*>, id: String): BackupObject<*>? { return if (isLoggedIn) try { client?.files()?.uploadBuilder("/$id") ?.withMute(true) ?.withMode(WriteMode.OVERWRITE) ?.uploadAndFinish(ByteArrayInputStream(Gson().toJson(backupObject, Backups.getType(id)).checkData(id).toByteArray(StandardCharsets.UTF_8))) Backups.saveLastBackup() backupObject } catch (e: Exception) { e.printStackTrace() null } else null } } ================================================ FILE: app/src/main/java/knf/kuma/backup/framework/LocalService.kt ================================================ package knf.kuma.backup.framework import android.os.Build import android.os.Environment import android.widget.Toast import com.google.gson.Gson import knf.kuma.backup.Backups import knf.kuma.backup.objects.BackupObject import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.noCrashLet import knf.kuma.commons.safeContext import java.io.File class LocalService : BackupService() { private val baseFile by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { safeContext.getExternalFilesDir("backups") ?: File(safeContext.filesDir, "backups") } else { File(Environment.getExternalStorageDirectory(), "UKIKU/backups") } } override fun start() { if (!baseFile.exists()) baseFile.mkdirs() } override val isLoggedIn: Boolean get() = true override fun logIn(token: String?): Boolean { Backups.type = Backups.Type.LOCAL return true } override fun logOut() { } override suspend fun search(id: String, manual: Boolean): BackupObject<*>? { val file = File(baseFile, "$id.backup") return if (file.exists()) { noCrashLet { Gson().fromJson(file.readText().checkResponse(id), Backups.getType(id)) as BackupObject<*> } } else { if (manual && id != Backups.keyAutoBackup) { doOnUIGlobal { Toast.makeText(safeContext, "El archivo de respaldo necesita estar en ${file.path}", Toast.LENGTH_LONG).show() } } null } } override suspend fun backup(backupObject: BackupObject<*>, id: String): BackupObject<*>? { val file = File(baseFile, "$id.backup") return noCrashLet { file.writeText(Gson().toJson(backupObject, Backups.getType(id)).checkData(id)) backupObject } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/objects/AnimeChapters.kt ================================================ package knf.kuma.backup.objects import com.google.gson.Gson import com.google.gson.reflect.TypeToken import knf.kuma.pojos.AnimeObject class AnimeChapters { var aid = "0" var chapters = "[]" fun chaptersList(): List { val type = object : TypeToken>() { }.type return Gson().fromJson>(chapters, type) ?: listOf() } } ================================================ FILE: app/src/main/java/knf/kuma/backup/objects/BackupObject.kt ================================================ package knf.kuma.backup.objects import com.google.gson.annotations.SerializedName import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale open class BackupObject { @SerializedName("date") var date: String? = null @SerializedName("data") var data: List? = null constructor() constructor(data: List) { this.date = SimpleDateFormat("dd/MM/yyyy kk:mm", Locale.getDefault()).format(Calendar.getInstance().time) this.data = data } } ================================================ FILE: app/src/main/java/knf/kuma/backup/objects/FavList.kt ================================================ package knf.kuma.backup.objects import com.google.gson.Gson import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import knf.kuma.database.CacheDB import knf.kuma.pojos.FavoriteObject import xdroid.toaster.Toaster import java.io.InputStream import java.io.InputStreamReader class FavList { @SerializedName("response") internal var response: String? = null @SerializedName("favs") internal var favs: MutableList? = null internal class FavSection { @SerializedName("name") var name: String? = null @SerializedName("list") var list: MutableList? = null } internal class FavEntry { @SerializedName("title") var title: String? = null @SerializedName("aid") var aid: String? = null @SerializedName("section") var section: String? = null @SerializedName("order") var order: Int = 0 } companion object { fun decode(inputStream: InputStream?): MutableList? { if (inputStream == null) return null var totalCount = 0 var errorCount = 0 val dao = CacheDB.INSTANCE.animeDAO() val favList = Gson().fromJson(InputStreamReader(inputStream), object : TypeToken() { }.type) val favs = ArrayList() for (section in favList.favs ?: listOf()) { totalCount += section.list?.size ?: 0 for (favEntry in section.list ?: listOf()) { val animeObject = dao.getByAid(favEntry.aid ?: "") if (animeObject != null) { val fav = FavoriteObject(animeObject) fav.category = favEntry.section favs.add(fav) } else errorCount++ } } Toaster.toast("Migrados correctamente " + (totalCount - errorCount) + "/" + totalCount) return favs } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/objects/SeenList.kt ================================================ package knf.kuma.backup.objects import android.util.Log import com.google.gson.Gson import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import knf.kuma.database.CacheDB import knf.kuma.pojos.SeenObject import xdroid.toaster.Toaster import java.io.InputStream import java.io.InputStreamReader /** * Created by jordy on 01/03/2018. */ class SeenList { @SerializedName("response") internal var response: String? = null @SerializedName("vistos") internal var vistos: String? = null internal var list: MutableList? = null private fun deserialize() { list = ArrayList() Log.e("Seen", "$vistos") val els = vistos?.replace("E", "")?.split(":::".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() for (el in els ?: emptyArray()) { if (el != "") { val spl = el.split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() list?.add(SeenObj(spl[0], spl[1])) } } list?.sort() } internal inner class SeenObj(var aid: String, var num: String) : Comparable { override fun compareTo(other: SeenObj): Int { val bname = aid.compareTo(other.aid) return if (bname != 0) { bname } else { num.compareTo(other.num) } } } companion object { fun decode(inputStream: InputStream?): MutableList? { if (inputStream == null) return null var errorCount = 0 val dao = CacheDB.INSTANCE.animeDAO() val seenList = Gson().fromJson(InputStreamReader(inputStream), object : TypeToken() { }.type) seenList.deserialize() val totalCount = seenList.list?.size ?: 0 val chapters = ArrayList() var animeObject: AnimeChapters? = null for (obj in seenList.list ?: listOf()) { try { if (animeObject == null || animeObject.aid != obj.aid) animeObject = dao.getChaptersByAid(obj.aid) val chapterList = animeObject.chaptersList() var found = false for (chapter in chapterList) { try { if (chapter.number.endsWith(" " + obj.num)) { chapters.add(SeenObject.fromChapter(chapter)) found = true break } } catch (e: Exception) { e.printStackTrace() } } if (!found) errorCount++ } catch (e: Exception) { errorCount++ } } Toaster.toast("Migrados correctamente " + (totalCount - errorCount) + "/" + totalCount) return chapters } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/screens/MigrateDirectoryFragment.kt ================================================ package knf.kuma.backup.screens import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import knf.kuma.R import knf.kuma.database.CacheDB import knf.kuma.databinding.LayMigrateDirectoryBinding import knf.kuma.directory.DirectoryService import knf.kuma.directory.DirectoryService.Companion.STATE_FINISHED import knf.kuma.directory.DirectoryService.Companion.STATE_FULL import knf.kuma.directory.DirectoryService.Companion.STATE_INTERRUPTED import knf.kuma.directory.DirectoryService.Companion.STATE_PARTIAL class MigrateDirectoryFragment : Fragment() { private var onDirStatus: DirectoryService.OnDirStatus? = null private lateinit var binding: LayMigrateDirectoryBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.lay_migrate_directory, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = LayMigrateDirectoryBinding.bind(view) CacheDB.INSTANCE.animeDAO().countLive.observe(viewLifecycleOwner) { count -> binding.tvDirectoryCount.text = count.toString() } DirectoryService.getLiveStatus().observe(viewLifecycleOwner) { integer -> if (integer != null) when (integer) { STATE_PARTIAL -> Log.e("Dir", "Partial search") STATE_FULL -> Log.e("Dir", "Full search") STATE_INTERRUPTED -> { Log.e("Dir", "Interrupted") binding.loading.visibility = View.GONE binding.tvError.text = "Error: Creacion interrumpida" binding.tvError.visibility = View.VISIBLE } STATE_FINISHED -> { Log.e("Dir", "Finished") onDirStatus?.onFinished() } } } } fun setOnDirStatus(onDirStatus: DirectoryService.OnDirStatus) { this.onDirStatus = onDirStatus } companion object { operator fun get(dirStatus: DirectoryService.OnDirStatus): MigrateDirectoryFragment { val fragment = MigrateDirectoryFragment() fragment.setOnDirStatus(dirStatus) return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/screens/MigrateSuccessFragment.kt ================================================ package knf.kuma.backup.screens import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.App import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.backup.objects.FavList import knf.kuma.backup.objects.SeenList import knf.kuma.commons.safeDismiss import knf.kuma.commons.showSnackbar import knf.kuma.commons.toast import knf.kuma.database.CacheDB import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster class MigrateSuccessFragment : Fragment() { private val REQUEST_FAVS = 5628 private val REQUEST_SEEN = 9986 private lateinit var root: View override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.lay_migrate_success, container, false) root = view.find(R.id.root) view.find(R.id.migrate_favs).setOnClickListener { onMigrateFavs() } view.find(R.id.migrate_seen).setOnClickListener { onMigrateSeen() } return view } private fun onMigrateFavs() { try { startActivityForResult(Intent().setAction("knf.kuma.MIGRATE").putExtra("type", 0), REQUEST_FAVS) } catch (e: ActivityNotFoundException) { "No se encontró Animeflv App o la version es incorrecta!".toast() } } private fun onMigrateSeen() { try { startActivityForResult(Intent().setAction("knf.kuma.MIGRATE").putExtra("type", 1), REQUEST_SEEN) } catch (e: ActivityNotFoundException) { "No se encontró Animeflv App o la version es incorrecta!".toast() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val snackbar = root.showSnackbar("Migrando...", Snackbar.LENGTH_INDEFINITE) doAsync { try { data?.data?.let { when (requestCode) { REQUEST_FAVS -> { val list = FavList.decode(App.context.contentResolver.openInputStream(it)) ?: return@let null CacheDB.INSTANCE.favsDAO().addAll(list) syncData { favs() } } REQUEST_SEEN -> { val chapters = SeenList.decode(App.context.contentResolver.openInputStream(it)) ?: return@let null CacheDB.INSTANCE.seenDAO().addAll(chapters) syncData { seen() } } else -> null } } ?: throw IllegalStateException("Data or IS is null!") } catch (e: Exception) { e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) Toaster.toast("Error al migrar datos") } snackbar.safeDismiss() } } } ================================================ FILE: app/src/main/java/knf/kuma/backup/screens/MigrateVersionFragment.kt ================================================ package knf.kuma.backup.screens import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.core.content.pm.PackageInfoCompat import androidx.fragment.app.Fragment import knf.kuma.App import knf.kuma.R import org.jetbrains.anko.find class MigrateVersionFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.lay_migrate_version, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { view.find(R.id.tv_version_bad).text = installedCode.toString() } companion object { val installedCode: Long get() { return try { val info = App.context.packageManager.getPackageInfo("knf.animeflv", 0) PackageInfoCompat.getLongVersionCode(info) } catch (e: Exception) { -1 } } } } ================================================ FILE: app/src/main/java/knf/kuma/cast/CastCustom.kt ================================================ package knf.kuma.cast import android.app.Dialog import android.content.Context import android.content.Intent import android.view.View import android.widget.ImageView import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import es.munix.multidisplaycast.CastManager import es.munix.multidisplaycast.helpers.NotificationsHelper import es.munix.multidisplaycast.interfaces.DialogCallback import knf.kuma.commons.load import knf.kuma.custom.ThemedControlsActivity import org.jetbrains.anko.sdk27.coroutines.onClick class CastCustom : CastManager() { private val notificationHelper = CastNotificationHelper() override fun onLoadImage(context: Context, image: String, imageView: ImageView) { imageView.apply { load(image) onClick { context.startActivity(Intent(context, controlsClass).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } } } override fun getControlsClass(): Class<*> { return ThemedControlsActivity::class.java } override fun getNotificationsHelper(): NotificationsHelper { return notificationHelper } override fun getPairingDialog(context: Context, title: String, message: String, positiveText: String?, negativeText: String?, dialogCallback: DialogCallback): Dialog { return createDialog(context, null, title, message, positiveText, negativeText, dialogCallback) ?: super.getPairingDialog(context, title, message, positiveText, negativeText, dialogCallback) } override fun getDisconnectDialog(context: Context, customView: View, positiveText: String?, dialogCallback: DialogCallback): Dialog { return createDialog(context, customView, null, null, positiveText, null, dialogCallback) ?: super.getDisconnectDialog(context, customView, positiveText, dialogCallback) } override fun getPairingCodeDialog(context: Context, view: View, title: String, positiveText: String?, negativeText: String?, callback: DialogCallback): Dialog { return createDialog(context, view, title, null, positiveText, negativeText, callback) ?: super.getPairingCodeDialog(context, view, title, positiveText, negativeText, callback) } private fun createDialog(context: Context, view: View?, title: String?, message: String?, positiveText: String?, negativeText: String?, callback: DialogCallback): Dialog? { return try { MaterialDialog(context).apply { lifecycleOwner() if (!title.isNullOrEmpty()) title(text = title) if (!title.isNullOrEmpty()) message(text = message) if (view != null) customView(view = view) if (!positiveText.isNullOrEmpty()) positiveButton(text = positiveText) { callback.onPositive() } if (!negativeText.isNullOrEmpty()) negativeButton(text = negativeText) { callback.onNegative() } } } catch (e: Exception) { null } } } ================================================ FILE: app/src/main/java/knf/kuma/cast/CastMedia.kt ================================================ package knf.kuma.cast import android.net.Uri import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaMetadata import com.google.android.gms.common.images.WebImage import knf.kuma.animeinfo.ktx.filePath import knf.kuma.commons.PrefsUtil import knf.kuma.commons.SelfServer import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.RecentObject import knf.kuma.recents.RecentModel data class CastMedia(val url: String, val eid: String, val mediaInfo: MediaInfo) { val title: String get() = mediaInfo.metadata?.getString(MediaMetadata.KEY_TITLE)!! val subTitle: String get() = mediaInfo.metadata?.getString(MediaMetadata.KEY_SUBTITLE)!! val image: String get() = mediaInfo.metadata?.images!![0].url.toString() val type: String get() = mediaInfo.contentType!! companion object { fun create(chapter: AnimeObject.WebInfo.AnimeChapter?, url: String? = null): CastMedia? { if (chapter == null) return null val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE).apply { putString(MediaMetadata.KEY_TITLE, chapter.name) putString(MediaMetadata.KEY_SUBTITLE, chapter.number) addImage(WebImage(Uri.parse(if (chapter.img.isNullOrBlank()) "https://www3.animeflv.net/uploads/animes/thumbs/${chapter.aid}.jpg" else chapter.img))) } val fUrl = when { url.isNullOrBlank() -> SelfServer.start(chapter.filePath, true) PrefsUtil.isProxyCastEnabled -> ProxyCache.start(url) else -> url } val mediaInfo = MediaInfo.Builder(fUrl!!).apply { setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) setContentType("video/mp4") setMetadata(metadata) } return CastMedia(fUrl, chapter.eid, mediaInfo.build()) } fun create(recent: RecentObject?, url: String? = null): CastMedia? { if (recent == null) return null val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE).apply { putString(MediaMetadata.KEY_TITLE, recent.name) putString(MediaMetadata.KEY_SUBTITLE, recent.chapter) addImage(WebImage(Uri.parse("https://www3.animeflv.net/uploads/animes/thumbs/${recent.aid}.jpg"))) } val fUrl = when { url.isNullOrBlank() -> SelfServer.start(recent.filePath, true) PrefsUtil.isProxyCastEnabled -> ProxyCache.start(url) else -> url } val mediaInfo = MediaInfo.Builder(fUrl!!).apply { setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) setContentType("video/mp4") setMetadata(metadata) } return CastMedia(fUrl, recent.eid, mediaInfo.build()) } fun create(recent: RecentModel?, url: String? = null): CastMedia? { if (recent == null) return null val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE).apply { putString(MediaMetadata.KEY_TITLE, recent.name) putString(MediaMetadata.KEY_SUBTITLE, recent.chapter) addImage(WebImage(Uri.parse("https://www3.animeflv.net/uploads/animes/thumbs/${recent.aid}.jpg"))) } val fUrl = when { url.isNullOrBlank() -> SelfServer.start(recent.extras.filePath, true) PrefsUtil.isProxyCastEnabled -> ProxyCache.start(url) else -> url } val mediaInfo = MediaInfo.Builder(fUrl!!).apply { setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) setContentType("video/mp4") setMetadata(metadata) } return CastMedia(fUrl, recent.extras.eid, mediaInfo.build()) } fun create(fileDownObj: ExplorerObject.FileDownObj?): CastMedia? { if (fileDownObj == null) return null val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE).apply { putString(MediaMetadata.KEY_TITLE, fileDownObj.title) putString(MediaMetadata.KEY_SUBTITLE, "Episodio ${fileDownObj.chapter}") addImage(WebImage(Uri.parse(fileDownObj.chapPreviewLink))) } val url = SelfServer.start( fileDownObj.fileName.substring(fileDownObj.fileName.indexOf("$")), true ) val mediaInfo = MediaInfo.Builder(url!!).apply { setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) setContentType("video/mp4") setMetadata(metadata) } return CastMedia(url, fileDownObj.eid, mediaInfo.build()) } } } ================================================ FILE: app/src/main/java/knf/kuma/cast/CastNotificationHelper.kt ================================================ package knf.kuma.cast import es.munix.multidisplaycast.helpers.NotificationsHelper class CastNotificationHelper : NotificationsHelper() ================================================ FILE: app/src/main/java/knf/kuma/cast/ProxyCache.kt ================================================ package knf.kuma.cast import knf.kuma.App import knf.kuma.commons.SelfServer import knf.libs.videocache.HttpProxyCacheServer object ProxyCache { private val cacheServer: HttpProxyCacheServer by lazy { HttpProxyCacheServer(App.context) } fun start(url: String): String { return if (cacheServer.isCached(url)) SelfServer.start(cacheServer.getProxyUrl(url), true) ?: "" else cacheServer.getProxyUrl(url, false) } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/ChangeAdapter.kt ================================================ package knf.kuma.changelog import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.changelog.objects.Release import org.jetbrains.anko.find internal class ChangeAdapter(release: Release) : RecyclerView.Adapter() { private val changes = release.changes override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChangeItem { return ChangeItem(LayoutInflater.from(parent.context).inflate(R.layout.item_release, parent, false)) } override fun onBindViewHolder(holder: ChangeItem, position: Int) { val change = changes[position] setType(holder.type, change.type) holder.description.text = change.text } @SuppressLint("SetTextI18n") private fun setType(textView: TextView?, type: String) { if (textView == null) return when (type) { "new" -> { textView.text = "Nuevo" textView.setBackgroundResource(R.drawable.chip_new) } "change" -> { textView.text = "Cambio" textView.setBackgroundResource(R.drawable.chip_change) } "fix" -> { textView.text = "Arreglo" textView.setBackgroundResource(R.drawable.chip_error) } else -> { textView.text = "Cambio" textView.setBackgroundResource(R.drawable.chip_change) } } } override fun getItemCount(): Int { return changes.size } internal class ChangeItem(itemView: View) : RecyclerView.ViewHolder(itemView) { var type: TextView = itemView.find(R.id.type) var description: TextView = itemView.find(R.id.description) } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/ChangeAdapterMaterial.kt ================================================ package knf.kuma.changelog import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.chip.Chip import knf.kuma.R import knf.kuma.changelog.objects.Release import org.jetbrains.anko.find internal class ChangeAdapterMaterial(release: Release) : RecyclerView.Adapter() { private val changes = release.changes override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChangeItem { return ChangeItem(LayoutInflater.from(parent.context).inflate(R.layout.item_release_material, parent, false)) } override fun onBindViewHolder(holder: ChangeItem, position: Int) { val change = changes[position] setType(holder.type, change.type) holder.description.text = change.text } @SuppressLint("SetTextI18n") private fun setType(chip: Chip, type: String) { when (type) { "new" -> { chip.text = "Nuevo" chip.setChipBackgroundColorResource(R.color.release_new) } "fix" -> { chip.text = "Arreglo" chip.setChipBackgroundColorResource(R.color.release_error) } else -> { chip.text = "Cambio" chip.setChipBackgroundColorResource(R.color.release_change) } } } override fun getItemCount(): Int { return changes.size } internal class ChangeItem(itemView: View) : RecyclerView.ViewHolder(itemView) { var type: Chip = itemView.find(R.id.type) var description: TextView = itemView.find(R.id.description) } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/ChangelogActivity.kt ================================================ package knf.kuma.changelog import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.content.pm.PackageInfoCompat import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.MaterialDialog import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.changelog.objects.Changelog import knf.kuma.commons.EAHelper import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.databinding.RecyclerChangelogBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jsoup.Jsoup import org.jsoup.parser.Parser import xdroid.toaster.Toaster import java.io.BufferedReader import java.io.InputStreamReader class ChangelogActivity : GenericActivity() { private val binding by lazy { RecyclerChangelogBinding.inflate(layoutInflater) } private val changelog: Changelog @Throws(Exception::class) get() = if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("changelog_load", true)) { Changelog(Jsoup.parse(xml, "", Parser.xmlParser())) } else { Changelog(Jsoup.connect("https://raw.githubusercontent.com/jordyamc/UKIKU/master/app/src/main/assets/changelog.xml").parser(Parser.xmlParser()).get()) } private val xml: String? get() { var xmlString: String? = null val am = assets try { val reader = BufferedReader(InputStreamReader(am.open("changelog.xml"))) val sb = StringBuilder() var mLine: String? = reader.readLine() while (mLine != null) { sb.append(mLine) mLine = reader.readLine() } reader.close() xmlString = sb.toString() } catch (e1: Exception) { e1.printStackTrace() } return xmlString } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) binding.toolbar.title = "Changelog" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.toolbar.setNavigationOnClickListener { finish() } doAsync { try { val changelog = changelog binding.progress.post { binding.progress.visibility = View.GONE } binding.recycler.post { binding.recycler.adapter = ReleaseAdapter(changelog) } } catch (e: Exception) { FirebaseCrashlytics.getInstance().recordException(e) Toaster.toast("Error al cargar changelog") finish() } } } companion object { fun open(context: Context) { context.startActivity(Intent(context, ChangelogActivity::class.java)) } fun check(activity: AppCompatActivity) { doAsync { try { val cCode = PreferenceManager.getDefaultSharedPreferences(activity).getInt("version_code", 0) val pCode = PackageInfoCompat.getLongVersionCode(activity.packageManager.getPackageInfo(activity.packageName, 0)).toInt() if (pCode > cCode && cCode != 0) { activity.lifecycleScope.launch(Dispatchers.Main) { delay(2000) MaterialDialog(activity).safeShow { message(text = "Nueva versión, ¿Leer Changelog?") positiveButton(text = "Leer") { open(activity) PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } negativeButton(text = "Omitir") { PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } setOnCancelListener { PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } } } } else PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } catch (e: Exception) { e.printStackTrace() } } } } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/ChangelogActivityMaterial.kt ================================================ package knf.kuma.changelog import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.content.pm.PackageInfoCompat import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import com.afollestad.materialdialogs.MaterialDialog import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.changelog.objects.Changelog import knf.kuma.commons.EAHelper import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.databinding.RecyclerChangelogMaterialBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jsoup.Jsoup import org.jsoup.parser.Parser import xdroid.toaster.Toaster import java.io.BufferedReader import java.io.InputStreamReader class ChangelogActivityMaterial : GenericActivity() { private val binding by lazy { RecyclerChangelogMaterialBinding.inflate(layoutInflater) } private val changelog: Changelog @Throws(Exception::class) get() = if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("changelog_load", true)) { Changelog(Jsoup.parse(xml, "", Parser.xmlParser())) } else { Changelog(Jsoup.connect("https://raw.githubusercontent.com/jordyamc/UKIKU/master/app/src/main/assets/changelog.xml").parser(Parser.xmlParser()).get()) } private val xml: String? get() { var xmlString: String? = null val am = assets try { val reader = BufferedReader(InputStreamReader(am.open("changelog.xml"))) val sb = StringBuilder() var mLine: String? = reader.readLine() while (mLine != null) { sb.append(mLine) mLine = reader.readLine() } reader.close() xmlString = sb.toString() } catch (e1: Exception) { e1.printStackTrace() } return xmlString } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) binding.toolbar.title = "Changelog" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.toolbar.setNavigationOnClickListener { finish() } binding.recycler.addItemDecoration(DividerItemDecoration(this,DividerItemDecoration.VERTICAL)) doAsync { try { val changelog = changelog binding.progress.post { binding.progress.visibility = View.GONE } binding.recycler.post { binding.recycler.adapter = ReleaseAdapterMaterial(changelog) } } catch (e: Exception) { FirebaseCrashlytics.getInstance().recordException(e) Toaster.toast("Error al cargar changelog") finish() } } } companion object { fun open(context: Context) { context.startActivity(Intent(context, ChangelogActivityMaterial::class.java)) } fun check(activity: AppCompatActivity) { doAsync { try { val cCode = PreferenceManager.getDefaultSharedPreferences(activity).getInt("version_code", 0) val pCode = PackageInfoCompat.getLongVersionCode(activity.packageManager.getPackageInfo(activity.packageName, 0)).toInt() if (pCode > cCode && cCode != 0) { activity.lifecycleScope.launch(Dispatchers.Main) { delay(2000) MaterialDialog(activity).safeShow { message(text = "Nueva versión, ¿Leer Changelog?") positiveButton(text = "Leer") { open(activity) PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } negativeButton(text = "Omitir") { PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } setOnCancelListener { PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } } } } else PreferenceManager.getDefaultSharedPreferences(activity).edit().putInt("version_code", pCode).apply() } catch (e: Exception) { e.printStackTrace() } } } } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/ReleaseAdapter.kt ================================================ package knf.kuma.changelog import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.changelog.objects.Changelog import org.jetbrains.anko.find internal class ReleaseAdapter(changelog: Changelog) : RecyclerView.Adapter() { private val list = changelog.releases override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReleaseItem { return ReleaseItem(LayoutInflater.from(parent.context).inflate(R.layout.item_changelog, parent, false)) } override fun onBindViewHolder(holder: ReleaseItem, position: Int) { val release = list[position] holder.version.text = release.version holder.code.text = release.code holder.recyclerView.adapter = ChangeAdapter(release) } override fun getItemCount(): Int { return list.size } internal class ReleaseItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val version: TextView = itemView.find(R.id.version) val code: TextView = itemView.find(R.id.code) val recyclerView: RecyclerView = itemView.find(R.id.recycler) } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/ReleaseAdapterMaterial.kt ================================================ package knf.kuma.changelog import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.changelog.objects.Changelog import org.jetbrains.anko.find internal class ReleaseAdapterMaterial(changelog: Changelog) : RecyclerView.Adapter() { private val list = changelog.releases override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReleaseItem { return ReleaseItem(LayoutInflater.from(parent.context).inflate(R.layout.item_changelog_material, parent, false)) } override fun onBindViewHolder(holder: ReleaseItem, position: Int) { val release = list[position] holder.version.text = release.version holder.code.text = release.code holder.recyclerView.adapter = ChangeAdapterMaterial(release) } override fun getItemCount(): Int { return list.size } internal class ReleaseItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val version: TextView = itemView.find(R.id.version) val code: TextView = itemView.find(R.id.code) val recyclerView: RecyclerView = itemView.find(R.id.recycler) } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/objects/Change.kt ================================================ package knf.kuma.changelog.objects import org.jsoup.nodes.Element class Change(element: Element) { var type: String = element.attr("type") var text: String = element.text() } ================================================ FILE: app/src/main/java/knf/kuma/changelog/objects/Changelog.kt ================================================ package knf.kuma.changelog.objects import org.jsoup.nodes.Document class Changelog(document: Document) { var releases: MutableList init { val list = ArrayList() for (element in document.select("release")) { list.add(Release(element)) } this.releases = list } } ================================================ FILE: app/src/main/java/knf/kuma/changelog/objects/Release.kt ================================================ package knf.kuma.changelog.objects import org.jsoup.nodes.Element class Release(element: Element) { var version: String = element.attr("version") var code: String = element.attr("code") var changes: MutableList init { val list = ArrayList() for (e in element.select("change")) { list.add(Change(e)) } this.changes = list } } ================================================ FILE: app/src/main/java/knf/kuma/commons/AllSSLOkHttpClient.kt ================================================ package knf.kuma.commons import okhttp3.ConnectionSpec import okhttp3.OkHttpClient import org.conscrypt.Conscrypt import java.security.Security import javax.net.ssl.SSLContext object AllSSLOkHttpClient { fun get() = OkHttpClient.Builder() .connectionSpecs( listOf( ConnectionSpec.CLEARTEXT, ConnectionSpec.Builder(ConnectionSpec.COMPATIBLE_TLS) .allEnabledTlsVersions() .allEnabledCipherSuites() .build() ) ).build() fun enableTLS() { try { Security.insertProviderAt(Conscrypt.newProvider(), 1) SSLContext.getInstance("TLSv1.3").apply { init(null, null, null) createSSLEngine() } } catch (e: Exception) { e.printStackTrace() } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/BypassUtil.kt ================================================ package knf.kuma.commons import android.content.Context import android.webkit.CookieManager import android.webkit.WebSettings import android.webkit.WebView import androidx.preference.PreferenceManager import com.bumptech.glide.load.model.LazyHeaders import knf.kuma.App import knf.kuma.ads.AdsUtils import knf.kuma.uagen.UAGenerator import knf.kuma.uagen.randomUA import knf.tools.bypass.DisplayType import knf.tools.bypass.Request import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jsoup.HttpStatusException import org.jsoup.Jsoup /** * Created by jordy on 17/03/2018. */ class BypassUtil { interface BypassListener { fun onNeedRecreate() } companion object { val userAgent: String get() = if (PrefsUtil.useDefaultUserAgent && !PrefsUtil.alwaysGenerateUA) noCrashLet { WebSettings.getDefaultUserAgent(App.context) } ?: PrefsUtil.userAgent else PrefsUtil.userAgent var isLoading = false var isChecking = false private const val keyCfClearance = "cf_clearance" private const val keyCfDuid = "__cfduid" private const val keyCookiesBypass = "bypass_cookies" private const val defaultValue = "" const val testLink = "https://www3.animeflv.net/" fun createRequest(): Request { return Request( testLink, lastUA = PrefsUtil.userAgent, showReload = AdsUtils.remoteConfigs.getBoolean("bypass_show_reload"), useFocus = isTV, maxTryCount = AdsUtils.remoteConfigs.getLong("bypass_max_tries").toInt(), useLatestUA = true, reloadOnCaptcha = false, waitCaptcha = true, clearCookiesAtStart = true, displayType = DisplayType.DIALOG, dialogStyle = 0 ) } suspend fun clearCookiesIfNeeded() { if (!withContext(Dispatchers.IO) { isCloudflareActive() }) clearCookies(null) } fun saveCookies(context: Context, cookies: String): Boolean = noCrashLet(false) { bypassCookies = cookies if (cookies.contains(keyCfClearance)) { val parts = cookies.split(";").dropLastWhile { it.isEmpty() }.toTypedArray() for (cookie in parts) { if (cookie.contains(keyCfDuid)) setCFDuid(context, cookie.trim().substring(cookie.trim().indexOf("=") + 1)) if (cookie.contains(keyCfClearance)) { val clearance = cookie.trim().substring(cookie.trim().indexOf("=") + 1) if (clearance.isBlank()) return@noCrashLet false setClearance(context, clearance) } } return@noCrashLet true } false } fun clearCookies(webView: WebView?) { noCrash { val cookieManager = CookieManager.getInstance() cookieManager.removeAllCookies(null) webView?.clearCache(true) bypassCookies = null setCFDuid(App.context) setClearance(App.context) PrefsUtil.userAgent = UAGenerator.getRandomUserAgent() } } fun isNeeded(url: String = testLink): Boolean { return try { val response = jsoupCookies(url).execute() response.statusCode().let { it == 503 || it == 403 } } catch (e: HttpStatusException) { e.statusCode.let { it == 503 || it == 403 } } catch (e: Exception) { e.printStackTrace() false } } fun isCloudflareActive(url: String = testLink): Boolean { return try { val response = Jsoup.connect(url).followRedirects(true).execute() response.statusCode().let { it == 503 || it == 403 } } catch (e: HttpStatusException) { e.statusCode.let { it == 503 || it == 403 } } catch (e: Exception) { e.printStackTrace() false } } fun isCloudflareActiveRandom(url: String = testLink): Boolean { return try { val response = Jsoup.connect(url).followRedirects(true).userAgent(randomUA()).execute() response.statusCode().let { it == 503 || it == 403 } } catch (e: HttpStatusException) { e.statusCode.let { it == 503 || it == 403 } } catch (e: Exception) { e.printStackTrace() false } } fun isNeededFlag(): Int { return try { val response = jsoupCookies(testLink).execute() when (response.statusCode()) { 503 -> 1 403 -> 2 else -> 0 } } catch (e: HttpStatusException) { when (e.statusCode) { 503 -> 1 403 -> 2 else -> 0 } } catch (e: Exception) { e.printStackTrace() 0 } } fun getMapCookie(context: Context): Map { val map = LinkedHashMap() map["device"] = "computer" map["InstiSession"] = "eyJpZCI6IjRlNGYwNWYxLTg4NDMtNGQwOS05ODlmLWM1OWQ5N2NmNjVlYyIsInJlZmVycmVyIjoiIiwiY2FtcGFpZ24iOnsic291cmNlIjpudWxsLCJtZWRpdW0iOm51bGwsImNhbXBhaWduIjpudWxsLCJ0ZXJtIjpudWxsLCJjb250ZW50IjpudWxsfX0=" bypassCookies?.split(";")?.forEach { if (it.contains("=")) { val split = it.split("=") map[split[0]] = split[1] } } //getClearance(context).let { if (it != defaultValue) map[keyCfClearance] = it } //getCFDuid(context).let { if (it != defaultValue) map[keyCfDuid] = it } return map } fun getLazyHeaders(): LazyHeaders { return LazyHeaders.Builder().apply { addHeader("Cookie", getStringCookie(App.context)) addHeader("User-Agent", userAgent) }.build() } fun getMapHeaders(): Map { return mapOf( "Cookie" to getStringCookie(App.context), "User-Agent" to userAgent ) } fun getStringCookie(context: Context): String { val builder = StringBuilder() for ((key, value) in getMapCookie(context)) builder.append("$key=$value;") return builder.toString().dropLastWhile { it == ' ' || it == ';' } } var bypassCookies: String? set(value) = PreferenceManager.getDefaultSharedPreferences(App.context).edit().putString(keyCookiesBypass, value).apply() get() = PreferenceManager.getDefaultSharedPreferences(App.context).getString(keyCookiesBypass, null) fun getClearance(context: Context): String { return PreferenceManager.getDefaultSharedPreferences(context).getString(keyCfClearance, defaultValue) ?: defaultValue } private fun setClearance(context: Context, value: String = defaultValue) { PreferenceManager.getDefaultSharedPreferences(context).edit().putString(keyCfClearance, value).apply() } fun getCFDuid(context: Context): String { return PreferenceManager.getDefaultSharedPreferences(context).getString(keyCfDuid, defaultValue) ?: defaultValue } private fun setCFDuid(context: Context, value: String = defaultValue) { PreferenceManager.getDefaultSharedPreferences(context).edit().putString(keyCfDuid, value).apply() } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/CastUtil.kt ================================================ package knf.kuma.commons import android.app.Activity import android.content.Context import android.content.Intent import android.util.Log import android.view.Menu import android.view.View import androidx.lifecycle.MutableLiveData import com.google.android.material.snackbar.Snackbar import es.munix.multidisplaycast.CastManager import es.munix.multidisplaycast.interfaces.CastListener import es.munix.multidisplaycast.interfaces.PlayStatusListener import knf.kuma.App import knf.kuma.achievements.AchievementManager import knf.kuma.cast.CastCustom import knf.kuma.cast.CastMedia import knf.kuma.custom.ThemedControlsActivity import org.jetbrains.annotations.Contract import xdroid.toaster.Toaster class CastUtil private constructor(private val context: Context) : CastListener, PlayStatusListener { val casting = MutableLiveData() private var loading: Snackbar? = null init { CastManager.setInstance(CastCustom()) CastManager.getInstance().setDiscoveryManager() CastManager.getInstance().setPlayStatusListener(javaClass.simpleName, this) CastManager.getInstance().setCastListener(javaClass.simpleName, this) casting.value = NO_PLAYING } fun registerActivity(activity: Activity, menu: Menu, menuId: Int) = CastManager.getInstance().registerForActivity(activity, menu, menuId) fun connected(): Boolean { return CastManager.getInstance().isConnected //return isConnected; } fun play(view: View, castMedia: CastMedia?) { try { if (castMedia == null) throw IllegalStateException("CastMedia must not be null") if (connected()) { if (!castMedia.url.endsWith(":" + SelfServer.HTTP_PORT)) SelfServer.stop() Log.e("Cast", castMedia.url) CastManager.getInstance().playMedia(castMedia.url, "video/mp4", castMedia.title, castMedia.subTitle, castMedia.image) startLoading(view) setEid(castMedia.eid) AchievementManager.unlock(listOf(6)) } else { Toaster.toast("No hay dispositivo seleccionado") } } catch (e: Exception) { e.printStackTrace() stopLoading() Toaster.toast("Error al reproducir") } } fun stop() { CastManager.getInstance().stop() } fun onDestroy() { loading?.safeDismiss() loading = null CastManager.getInstance().onDestroy() } override fun isConnected() { } override fun isDisconnected() { stopLoading() setEid(NO_PLAYING) SelfServer.stop() } private fun getLoading(view: View): Snackbar { return view.showSnackbar("Cargando...", duration = Snackbar.LENGTH_INDEFINITE) } private fun startLoading(view: View) { doOnUIGlobal { loading = getLoading(view) } } private fun stopLoading() { doOnUIGlobal { loading?.safeDismiss() loading = null } } private fun setEid(eid: String) { doOnUIGlobal { casting.value = eid } } fun openControls() { context.startActivity(Intent(context, ThemedControlsActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } override fun onPlayStatusChanged(playStatus: Int) { when (playStatus) { PlayStatusListener.STATUS_START_PLAYING -> { stopLoading() openControls() } PlayStatusListener.STATUS_FINISHED, PlayStatusListener.STATUS_STOPPED -> { stopLoading() setEid(NO_PLAYING) } PlayStatusListener.STATUS_NOT_SUPPORT_LISTENER -> { stopLoading() setEid(NO_PLAYING) Toaster.toast("Video no soportado por dispositivo") } } } override fun onPositionChanged(currentPosition: Long) { } override fun onTotalDurationObtained(totalDuration: Long) { } override fun onSuccessSeek() { } companion object { var NO_PLAYING = "no_play" private val ourInstance: CastUtil by lazy { CastUtil(App.context) } @Contract(pure = true) fun get(): CastUtil { return ourInstance } fun registerActivity(activity: Activity, menu: Menu, menuId: Int) = get().registerActivity(activity, menu, menuId) } } ================================================ FILE: app/src/main/java/knf/kuma/commons/ChannelTools.kt ================================================ package knf.kuma.commons import java.io.IOException import java.nio.ByteBuffer import java.nio.channels.ReadableByteChannel import java.nio.channels.WritableByteChannel object ChannelTools { @Throws(IOException::class) fun fastChannelCopy(src: ReadableByteChannel, dest: WritableByteChannel) { val buffer = ByteBuffer.allocateDirect(16 * 1024) while (src.read(buffer) != -1) { // prepare the buffer to be drained buffer.flip() // write to the channel, may block dest.write(buffer) // If partial transfer, shift remainder down // If buffer is empty, same as doing clear() buffer.compact() } // EOF will leave buffer in fill state buffer.flip() // make sure the buffer is fully drained. while (buffer.hasRemaining()) { dest.write(buffer) } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/CipherExt.kt ================================================ package knf.kuma.commons import android.util.Base64 import knf.kuma.BuildConfig import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec object CipherContainer { val encryption = Encryption.getDefault(BuildConfig.CIPHER_PWD_16, BuildConfig.CIPHER_PWD_12, ByteArray(16)) } fun String.encrypt(): String? = CipherContainer.encryption.encryptOrNull(this) fun String.encryptOrThrow(): String = CipherContainer.encryption.encrypt(this) fun String.decrypt(): String? = CipherContainer.encryption.decryptOrNull(this) fun String.decryptOrThrow(): String = CipherContainer.encryption.decrypt(this) fun String.encrypt12(password: String): String { val secretKeySpec = SecretKeySpec(password.toByteArray(), "AES") val iv = ByteArray(12) val charArray = password.toCharArray() for (i in charArray.indices) { iv[i] = charArray[i].toByte() } val gcmParameterSpec = GCMParameterSpec(128, iv) val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec) val encryptedValue = cipher.doFinal(this.toByteArray()) return Base64.encodeToString(encryptedValue, Base64.DEFAULT) } fun String.encrypt16(password: String): String { val secretKeySpec = SecretKeySpec(password.toByteArray(), "AES") val iv = ByteArray(16) val charArray = password.toCharArray() for (i in charArray.indices) { iv[i] = charArray[i].toByte() } val ivParameterSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) val encryptedValue = cipher.doFinal(this.toByteArray()) return Base64.encodeToString(encryptedValue, Base64.DEFAULT) } fun String.encrypt32(password: String): String { val secretKeySpec = SecretKeySpec(password.toByteArray(), "AES") val iv = ByteArray(12) val charArray = password.toCharArray() for (i in charArray.indices) { iv[i] = charArray[i].toByte() } val ivParameterSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) val encryptedValue = cipher.doFinal(this.toByteArray()) return Base64.encodeToString(encryptedValue, Base64.DEFAULT) } fun String.decrypt12(password: String): String { val secretKeySpec = SecretKeySpec(password.toByteArray(), "AES") val iv = ByteArray(12) val charArray = password.toCharArray() for (i in charArray.indices) { iv[i] = charArray[i].toByte() } val gcmParameterSpec = GCMParameterSpec(128, iv) val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec) val decryptedByteValue = cipher.doFinal(Base64.decode(this, Base64.DEFAULT)) return String(decryptedByteValue) } fun String.decrypt16(password: String): String { val secretKeySpec = SecretKeySpec(password.toByteArray(), "AES") val iv = ByteArray(16) val charArray = password.toCharArray() for (i in charArray.indices) { iv[i] = charArray[i].toByte() } val ivParameterSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) val decryptedByteValue = cipher.doFinal(Base64.decode(this, Base64.DEFAULT)) return String(decryptedByteValue) } fun String.decrypt32(password: String): String { val secretKeySpec = SecretKeySpec(password.toByteArray(), "AES") val iv = ByteArray(12) val charArray = password.toCharArray() for (i in charArray.indices) { iv[i] = charArray[i].toByte() } val ivParameterSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) val decryptedByteValue = cipher.doFinal(Base64.decode(this, Base64.DEFAULT)) return String(decryptedByteValue) } ================================================ FILE: app/src/main/java/knf/kuma/commons/DesignUtils.kt ================================================ package knf.kuma.commons import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import androidx.fragment.app.FragmentActivity import knf.kuma.App import knf.kuma.Main import knf.kuma.MainMaterial import knf.kuma.animeinfo.ActivityAnime import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.emision.EmissionActivity import knf.kuma.emision.EmissionActivityMaterial import knf.kuma.explorer.ExplorerActivity import knf.kuma.explorer.ExplorerActivityMaterial object DesignUtils { private const val nameMainFlat = "knf.kuma.MainMaterial" private const val nameMainClassic = "knf.kuma.Main" private const val nameInfoFlat = "knf.kuma.animeinfo.ActivityAnimeMaterial" private const val nameInfoClassic = "knf.kuma.animeinfo.ActivityAnime" private var lastPref = PrefsUtil.designStyle val isFlat get() = PrefsUtil.designStyle == "0" val mainClass: Class<*> get() = if (isFlat) MainMaterial::class.java else Main::class.java val infoClass: Class<*> get() = if (isFlat) ActivityAnimeMaterial::class.java else ActivityAnime::class.java val explorerClass: Class<*> get() = if (isFlat) ExplorerActivityMaterial::class.java else ExplorerActivity::class.java val emissionClass: Class<*> get() = if (isFlat) EmissionActivityMaterial::class.java else EmissionActivity::class.java fun change(activity: FragmentActivity, to: String? = PrefsUtil.designStyle, start: Boolean = true) { to ?: return if (to == "0") { enableComponent(nameMainFlat) enableComponent(nameInfoFlat) if (start){ activity.finish() activity.startActivity(Intent(activity, MainMaterial::class.java).putExtra("start_position", 3)) } disableComponent(nameMainClassic) disableComponent(nameInfoClassic) } else { enableComponent(nameMainClassic) enableComponent(nameInfoClassic) if (start){ activity.finish() activity.startActivity(Intent(activity, Main::class.java).putExtra("start_position", 3)) } disableComponent(nameMainFlat) disableComponent(nameInfoFlat) } } private fun disableComponent(name: String) { App.context.packageManager.apply { setComponentEnabledSetting(ComponentName(App.context.packageName, name), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) } } private fun enableComponent(name: String) { App.context.packageManager.apply { setComponentEnabledSetting(ComponentName(App.context.packageName, name), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) } } fun listenDesignChange(activity: FragmentActivity){ PrefsUtil.getLiveDesignType().observe(activity) { noCrash { if (it != lastPref && it.toInt() >= 0) { lastPref = it change(activity, it) } } } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/EAHelper.kt ================================================ package knf.kuma.commons import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.TextView import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StyleRes import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdsUtils import knf.kuma.ads.FullscreenAdLoader import knf.kuma.ads.getFAdLoaderInterstitial import knf.kuma.ads.getFAdLoaderRewarded import knf.kuma.backup.firestore.syncData import knf.kuma.custom.GenericActivity import knf.kuma.database.EADB import knf.kuma.databinding.ActivityEaBinding import knf.kuma.pojos.EAObject import moe.feng.common.stepperview.IStepperAdapter import moe.feng.common.stepperview.VerticalStepperItemView import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick import xdroid.toaster.Toaster import java.util.Random import androidx.core.content.edit object EAHelper { private val CODE1: String by lazy { PreferenceManager.getDefaultSharedPreferences(App.context).getString("ea_code1", null) ?: generate(App.context, "ea_code1", arrayOf("R", "F", "D", "C")) } private val CODE2: String by lazy { PreferenceManager.getDefaultSharedPreferences(App.context).getString("ea_code2", null) ?: generate(App.context, "ea_code2", arrayOf("1", "2", "3", "4", "5", "6", "7")) } private var CURRENT_1 = "" private var CURRENT_2 = "" val isPart0Unlocked: Boolean get() = EADB.INSTANCE.eaDAO().isUnlocked(0) val isPart1Unlocked: Boolean get() = EADB.INSTANCE.eaDAO().isUnlocked(1) val isPart2Unlocked: Boolean get() = EADB.INSTANCE.eaDAO().isUnlocked(2) val isPart3Unlocked: Boolean get() = EADB.INSTANCE.eaDAO().isUnlocked(3) val isAllUnlocked: Boolean get() = isPart0Unlocked && isPart1Unlocked && isPart2Unlocked && isPart3Unlocked val phase: Int get() = when { isPart3Unlocked -> 4 isPart2Unlocked -> 3 isPart1Unlocked -> 2 isPart0Unlocked -> 1 else -> 0 } val eaMessage: String get() = when { isPart3Unlocked -> "Disfruta de la recompensa" isPart2Unlocked -> "El tesoro esta en Akihabara" isPart1Unlocked -> "LMMJVSD \u2192 US \u2192 $CODE2" isPart0Unlocked -> CODE1 else -> "\u26B2 easteregg" } fun getMessage(phase: Int): String { return when (phase) { 4 -> "Disfruta de la recompensa" 3 -> "El tesoro esta en Akihabara" 2 -> "LMMJVSD \u2192 US \u2192 $CODE2" 1 -> CODE1 else -> "\u26B2 easteregg" } } fun checkStart(query: String) { if (phase == 0 && query == BuildConfig.EASTER_SEARCH) { Toaster.toastLong(CODE1) setUnlocked(0) } } private fun generate(context: Context, key: String, array: Array): String { val builder = StringBuilder() for (i in 0..9) { builder.append(array[Random().nextInt(array.size)]) } PreferenceManager.getDefaultSharedPreferences(context).edit { putString( key, builder.toString() ) } return builder.toString() } fun enter1(part: String) { if (isPart0Unlocked && phase == 1) { CURRENT_1 += part if (CURRENT_1 == CODE1) { setUnlocked(1) Toaster.toastLong("LMMJVSD \u2192 US \u2192 $CODE2") clear1() } else if (!CODE1.startsWith(CURRENT_1)) { clear1() CURRENT_1 += part } } } fun clear1() { CURRENT_1 = "" } fun enter2(part: String) { if (isPart1Unlocked && phase == 2) { CURRENT_2 += part if (CURRENT_2 == CODE2) { setUnlocked(2) Toaster.toastLong("El tesoro esta en Akihabara") clear2() } else if (!CODE2.startsWith(CURRENT_2)) { clear2() CURRENT_2 += part } } } fun clear2() { CURRENT_2 = "" } internal fun enter3() { if (isPart2Unlocked && phase == 3) { setUnlocked(3) } } fun setUnlocked(phase: Int) { EADB.INSTANCE.eaDAO().unlock(EAObject(phase)) AchievementManager.onPhaseUnlocked(phase) syncData { ea() } } @StyleRes fun getTheme(): Int { return noCrashLet(R.style.AppTheme_NoActionBar) { if (!isPart0Unlocked || !isPart1Unlocked || !isPart2Unlocked or !isPart3Unlocked) R.style.AppTheme_DayNight when (PrefsUtil.themeColor) { "0" -> R.style.AppTheme_DayNight "1" -> R.style.AppTheme_Pink "2" -> R.style.AppTheme_Purple "3" -> R.style.AppTheme_DeepPurple "4" -> R.style.AppTheme_Indigo "5" -> R.style.AppTheme_Blue "6" -> R.style.AppTheme_LightBlue "7" -> R.style.AppTheme_Cyan "8" -> R.style.AppTheme_Teal "9" -> R.style.AppTheme_Green "10" -> R.style.AppTheme_LightGreen "11" -> R.style.AppTheme_Lime "12" -> R.style.AppTheme_Yellow "13" -> R.style.AppTheme_Amber "14" -> R.style.AppTheme_Orange "15" -> R.style.AppTheme_DeepOrange "16" -> R.style.AppTheme_Brown "17" -> R.style.AppTheme_Gray "18" -> R.style.AppTheme_BlueGray else -> R.style.AppTheme_DayNight } } } @StyleRes fun getThemeNA(): Int { return noCrashLet(R.style.AppTheme_NoActionBar) { if (!isPart0Unlocked || !isPart1Unlocked || !isPart2Unlocked or !isPart3Unlocked) R.style.AppTheme_NoActionBar when (PrefsUtil.themeColor) { "0" -> R.style.AppTheme_NoActionBar "1" -> R.style.AppTheme_NoActionBar_Pink "2" -> R.style.AppTheme_NoActionBar_Purple "3" -> R.style.AppTheme_NoActionBar_DeepPurple "4" -> R.style.AppTheme_NoActionBar_Indigo "5" -> R.style.AppTheme_NoActionBar_Blue "6" -> R.style.AppTheme_NoActionBar_LightBlue "7" -> R.style.AppTheme_NoActionBar_Cyan "8" -> R.style.AppTheme_NoActionBar_Teal "9" -> R.style.AppTheme_NoActionBar_Green "10" -> R.style.AppTheme_NoActionBar_LightGreen "11" -> R.style.AppTheme_NoActionBar_Lime "12" -> R.style.AppTheme_NoActionBar_Yellow "13" -> R.style.AppTheme_NoActionBar_Amber "14" -> R.style.AppTheme_NoActionBar_Orange "15" -> R.style.AppTheme_NoActionBar_DeepOrange "16" -> R.style.AppTheme_NoActionBar_Brown "17" -> R.style.AppTheme_NoActionBar_Gray "18" -> R.style.AppTheme_NoActionBar_BlueGray else -> R.style.AppTheme_NoActionBar } } } @StyleRes fun getThemeDialog(): Int { return noCrashLet(R.style.AppTheme_Dialog_Base) { if (!isPart0Unlocked || !isPart1Unlocked || !isPart2Unlocked or !isPart3Unlocked) R.style.AppTheme_Dialog_Base when (PrefsUtil.themeColor) { "0" -> R.style.AppTheme_Dialog_Base "1" -> R.style.AppTheme_Dialog_Pink "2" -> R.style.AppTheme_Dialog_Purple "3" -> R.style.AppTheme_Dialog_DeepPurple "4" -> R.style.AppTheme_Dialog_Indigo "5" -> R.style.AppTheme_Dialog_Blue "6" -> R.style.AppTheme_Dialog_LightBlue "7" -> R.style.AppTheme_Dialog_Cyan "8" -> R.style.AppTheme_Dialog_Teal "9" -> R.style.AppTheme_Dialog_Green "10" -> R.style.AppTheme_Dialog_LightGreen "11" -> R.style.AppTheme_Dialog_Lime "12" -> R.style.AppTheme_Dialog_Yellow "13" -> R.style.AppTheme_Dialog_Amber "14" -> R.style.AppTheme_Dialog_Orange "15" -> R.style.AppTheme_Dialog_DeepOrange "16" -> R.style.AppTheme_Dialog_Brown "17" -> R.style.AppTheme_Dialog_Gray "18" -> R.style.AppTheme_Dialog_BlueGray else -> R.style.AppTheme_Dialog_Base } } } @DrawableRes fun getThemeImg(): Int { return noCrashLet(getThemeImg("0")) { if (!isPart0Unlocked || !isPart1Unlocked || !isPart2Unlocked or !isPart3Unlocked) getThemeImg("0") getThemeImg(PrefsUtil.themeColor) } } @DrawableRes fun getThemeImg(value: String): Int { when (value) { "0" -> return R.drawable.side_nav_bar "1" -> return R.drawable.side_nav_bar_pink "2" -> return R.drawable.side_nav_bar_purple "3" -> return R.drawable.side_nav_bar_deep_purple "4" -> return R.drawable.side_nav_bar_indigo "5" -> return R.drawable.side_nav_bar_blue "6" -> return R.drawable.side_nav_bar_light_blue "7" -> return R.drawable.side_nav_bar_cyan "8" -> return R.drawable.side_nav_bar_teal "9" -> return R.drawable.side_nav_bar_green "10" -> return R.drawable.side_nav_bar_light_green "11" -> return R.drawable.side_nav_bar_lime "12" -> return R.drawable.side_nav_bar_yellow "13" -> return R.drawable.side_nav_bar_amber "14" -> return R.drawable.side_nav_bar_orange "15" -> return R.drawable.side_nav_bar_deep_orange "16" -> return R.drawable.side_nav_bar_brown "17" -> return R.drawable.side_nav_bar_gray "18" -> return R.drawable.side_nav_bar_blue_gray else -> return R.drawable.side_nav_bar } } @ColorRes fun getThemeColor(): Int { return noCrashLet(getThemeColor("0")) { if (!isPart0Unlocked || !isPart1Unlocked || !isPart2Unlocked or !isPart3Unlocked) getThemeColor("0") getThemeColor(PrefsUtil.themeColor) } } @ColorRes fun getThemeColor(value: String): Int { when (value) { "0" -> return R.color.colorAccent "1" -> return R.color.colorAccentPink "2" -> return R.color.colorAccentPurple "3" -> return R.color.colorAccentDeepPurple "4" -> return R.color.colorAccentIndigo "5" -> return R.color.colorAccentBlue "6" -> return R.color.colorAccentLightBlue "7" -> return R.color.colorAccentCyan "8" -> return R.color.colorAccentTeal "9" -> return R.color.colorAccentGreen "10" -> return R.color.colorAccentLightGreen "11" -> return R.color.colorAccentLime "12" -> return R.color.colorAccentYellow "13" -> return R.color.colorAccentAmber "14" -> return R.color.colorAccentOrange "15" -> return R.color.colorAccentDeepOrange "16" -> return R.color.colorAccentBrown "17" -> return R.color.colorAccentGray "18" -> return R.color.colorAccentBlueGrey else -> return R.color.colorAccent } } @ColorRes fun getThemeColorLight(): Int { return noCrashLet(getThemeColorLight("0")) { if (!isPart0Unlocked || !isPart1Unlocked || !isPart2Unlocked or !isPart3Unlocked) getThemeColorLight("0") getThemeColorLight(PrefsUtil.themeColor) } } @ColorRes fun getThemeColorLight(value: String): Int { when (value) { "0" -> return R.color.colorAccentLight "1" -> return R.color.colorAccentPinkLight "2" -> return R.color.colorAccentPurpleLight "3" -> return R.color.colorAccentDeepPurpleLight "4" -> return R.color.colorAccentIndigoLight "5" -> return R.color.colorAccentBlueLight "6" -> return R.color.colorAccentLightBlueLight "7" -> return R.color.colorAccentCyanLight "8" -> return R.color.colorAccentTealLight "9" -> return R.color.colorAccentGreenLight "10" -> return R.color.colorAccentLightGreenLight "11" -> return R.color.colorAccentLimeLight "12" -> return R.color.colorAccentYellowLight "13" -> return R.color.colorAccentAmberLight "14" -> return R.color.colorAccentOrangeLight "15" -> return R.color.colorAccentDeepOrangeLight "16" -> return R.color.colorAccentBrownLight "17" -> return R.color.colorAccentGrayLight "18" -> return R.color.colorAccentBlueGreyLight else -> return R.color.colorAccentLight } } } class EAUnlockActivity : GenericActivity(), IStepperAdapter { private val binding by lazy { ActivityEaBinding.inflate(layoutInflater) } private val rewardedAd: FullscreenAdLoader by lazy { getFAdLoaderRewarded(this) } private var interstitial: FullscreenAdLoader = getFAdLoaderInterstitial(this) override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.title = "Easter egg" binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } rewardedAd.load() interstitial.load() } override fun onResume() { super.onResume() doOnUI { binding.progress.visibility = View.GONE binding.verticalStepperView.stepperAdapter = this@EAUnlockActivity binding.verticalStepperView.currentStep = checkPurchases() } } private fun showAd() { diceOf<() -> Unit> { put({ rewardedAd.show() }, AdsUtils.remoteConfigs.getDouble("rewarded_percent")) put({ interstitial.show() }, AdsUtils.remoteConfigs.getDouble("interstitial_percent")) }() } private fun checkPurchases(): Int { return EAHelper.phase } override fun getTitle(index: Int): CharSequence { return "Paso ${index + 1}" } @SuppressLint("SetTextI18n") override fun onCreateCustomView(index: Int, context: Context?, view: VerticalStepperItemView?): View { val inflateView = LayoutInflater.from(context).inflate(R.layout.item_ea_step, view, false) val hint = inflateView.find(R.id.hint) hint.text = EAHelper.getMessage(index) val unlockCoinsButton = inflateView.find(R.id.unlockCoins) if (index == 0 || index == 4) { inflateView.find(R.id.layBuy).visibility = View.GONE } else { unlockCoinsButton.text = getPurchaseCost(index).toString() unlockCoinsButton.onClick { doOnUI { if (Economy.buy(getPurchaseCost(index))) { unlock(index) Toaster.toast("Compra realizada") } else Toaster.toast("No tienes suficientes loli-coins!!") } } } return inflateView } private fun getSkuCode(index: Int): String { return when (index) { 1 -> "ee_2" 2 -> "ee_3" 3 -> "ee_4" else -> "ee_all" } } private fun getPurchaseCost(index: Int): Int { return when (index) { 1 -> 700 2 -> 1300 3 -> 500 else -> 9999 } } private fun unlock(index: Int) { unlock(getSkuCode(index)) } private fun unlock(sku: String?) { when (sku) { "ee_2" -> { EAHelper.setUnlocked(1) binding.verticalStepperView.nextStep() } "ee_3" -> { EAHelper.setUnlocked(2) binding.verticalStepperView.nextStep() } "ee_4" -> { EAHelper.setUnlocked(3) binding.verticalStepperView.nextStep() } "ee_all" -> { EAHelper.setUnlocked(1) EAHelper.setUnlocked(2) EAHelper.setUnlocked(3) binding.verticalStepperView.currentStep = 4 invalidateOptionsMenu() } } } override fun getSummary(index: Int): CharSequence? { return null } override fun size(): Int { return 5 } override fun onShow(index: Int) { } override fun onHide(index: Int) { } override fun onCreateOptionsMenu(menu: Menu): Boolean { if (EAHelper.phase != 4) menuInflater.inflate(R.menu.menu_ea, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.coins -> { Economy.showWallet(this) { showAd() } } } return super.onOptionsItemSelected(item) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) binding.blockView.visibility = View.GONE } companion object { fun start(context: Context) { context.startActivity(Intent(context, EAUnlockActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/EAMapActivity.kt ================================================ package knf.kuma.commons import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.os.Bundle import androidx.core.content.ContextCompat import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.OnMapReadyCallback import com.google.android.gms.maps.SupportMapFragment import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.MarkerOptions import knf.kuma.R import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityEamapBinding import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size class EAMapActivity : GenericActivity(), OnMapReadyCallback { private val binding by lazy { ActivityEamapBinding.inflate(layoutInflater) } private lateinit var mMap: GoogleMap override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) // Obtain the SupportMapFragment and get notified when the map is ready to be used. val mapFragment = supportFragmentManager .findFragmentById(R.id.map) as? SupportMapFragment mapFragment?.getMapAsync(this) } /** * Manipulates the map once available. * This callback is triggered when the map is ready to be used. * This is where we can add markers or lines, add listeners or move the camera. In this case, * we just add a marker near Sydney, Australia. * If Google Play services is not installed on the device, the user will be prompted to install * it inside the SupportMapFragment. This method will only be triggered once the user has * installed Google Play services and returned to the app. */ override fun onMapReady(googleMap: GoogleMap) { mMap = googleMap mMap.uiSettings.apply { isMapToolbarEnabled = false isZoomControlsEnabled = false } val point = LatLng(35.702067, 139.774528) val marker = mMap.addMarker(MarkerOptions().apply { position(point) title("Easter Egg completado!") icon(BitmapDescriptorFactory.fromBitmap(getBitmapFromVD(this@EAMapActivity, R.drawable.ic_treasure))) }) googleMap.setOnCameraMoveListener { marker?.isVisible = mMap.cameraPosition.zoom >= 13 } googleMap.setOnMarkerClickListener { m -> if (m == marker) { EAHelper.enter3() binding.konfetti.build() .addColors(Color.BLUE, Color.RED, Color.YELLOW, Color.GREEN, Color.MAGENTA) .setDirection(0.0, 359.0) .setSpeed(4f, 7f) .setFadeOutEnabled(true) .setTimeToLive(2000) .addShapes(Shape.RECT, Shape.CIRCLE) .addSizes(Size(12, 6f), Size(16, 6f)) .setPosition(-50f, binding.konfetti.width + 50f, -50f, -50f) .streamFor(300, 10000L) } false } } private fun getBitmapFromVD(context: Context, drawableId: Int): Bitmap { val drawable = ContextCompat.getDrawable(context, drawableId) val bitmap = Bitmap.createBitmap(drawable?.intrinsicWidth ?: 0, drawable?.intrinsicHeight ?: 0, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) drawable?.setBounds(0, 0, canvas.width, canvas.height) drawable?.draw(canvas) return bitmap } companion object { fun start(context: Activity) { context.startActivityForResult(Intent(context, EAMapActivity::class.java), 5698) } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/Economy.kt ================================================ package knf.kuma.commons import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import androidx.lifecycle.MutableLiveData import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.databinding.DialogWalletBinding import org.jetbrains.anko.sdk27.coroutines.onClick import xdroid.toaster.Toaster.toast val Int.coins: String get() = "$this loli-coin${if (this == 1) "" else "s"}" object Economy { private val coinsLiveData = MutableLiveData(PrefsUtil.userCoins) val rewardedVideoLiveData = MutableLiveData(PrefsUtil.userRewardedVideoCount) fun reward(isAdClicked: Boolean = false, baseReward: Int = 1) { doOnUIException(onLog = { FirebaseCrashlytics.getInstance().recordException(it);toast("Error al obtener loli-coins\n${it.message}") }) { val reward = diceOf { put(baseReward, if (isAdClicked) 80.0 else 90.0) put(baseReward + 1, if (isAdClicked) 15.0 else 8.0) put(baseReward + 2, if (isAdClicked) 5.0 else 2.0) } PrefsUtil.userCoins = (PrefsUtil.userCoins + reward).also { coinsLiveData.value = it } PrefsUtil.userRewardedVideoCount = (PrefsUtil.userRewardedVideoCount + 1).also { rewardedVideoLiveData.value = it } FirestoreManager.updateTop() AchievementManager.incrementCount(reward, listOf(46, 47, 48, 49, 50, 51)) toast("Has ganado ${reward.coins}!!!!!") } } fun buy(price: Int): Boolean { val total = PrefsUtil.userCoins return if (total >= price) { PrefsUtil.userCoins = (total - price).also { doOnUIGlobal { coinsLiveData.value = it } } true } else false } fun showWallet(activity: FragmentActivity, themed: Boolean = false, onShow: () -> Unit) { activity.doOnUI { val view = activity.layoutInflater.inflate(R.layout.dialog_wallet, null) val binding = DialogWalletBinding.bind(view) if (themed) { binding.coinsCount.setTextColor(ContextCompat.getColor(activity, EAHelper.getThemeColorLight())) binding.backgroundTop.setBackgroundColor(ContextCompat.getColor(activity, EAHelper.getThemeColorLight())) binding.backgroundBottom.setBackgroundColor(ContextCompat.getColor(activity, EAHelper.getThemeColor())) } binding.coinsCount.text = PrefsUtil.userCoins.toString() coinsLiveData.observe(activity) { activity.doOnUI { binding.coinsCount.text = it.toString() } } binding.showAdButton.onClick { onShow() } AlertDialog.Builder(activity).apply { setView(view) }.show() } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/Encryption.java ================================================ package knf.kuma.commons; import android.util.Base64; import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; /** * A class to make more easy and simple the encrypt routines, this is the core of Encryption library */ public class Encryption { /** * The Builder used to create the Encryption instance and that contains the information about * encryption specifications, this instance need to be private and careful managed */ private final Builder mBuilder; /** * The private and unique constructor, you should use the Encryption.Builder to build your own * instance or get the default proving just the sensible information about encryption */ private Encryption(Builder builder) { mBuilder = builder; } /** * @return an default encryption instance or {@code null} if occur some Exception, you can * create yur own Encryption instance using the Encryption.Builder */ public static Encryption getDefault(String key, String salt, byte[] iv) { try { return Builder.getDefaultBuilder(key, salt, iv).build(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } /** * Encrypt a String * * @param data the String to be encrypted * @return the encrypted String or {@code null} if you send the data as {@code null} * @throws UnsupportedEncodingException if the Builder charset name is not supported or if * the Builder charset name is not supported * @throws NoSuchAlgorithmException if the Builder digest algorithm is not available * or if this has no installed provider that can * provide the requested by the Builder secret key * type or it is {@code null}, empty or in an invalid * format * @throws NoSuchPaddingException if no installed provider can provide the padding * scheme in the Builder digest algorithm * @throws InvalidAlgorithmParameterException if the specified parameters are inappropriate for * the cipher * @throws InvalidKeyException if the specified key can not be used to initialize * the cipher instance * @throws InvalidKeySpecException if the specified key specification cannot be used * to generate a secret key * @throws BadPaddingException if the padding of the data does not match the * padding scheme * @throws IllegalBlockSizeException if the size of the resulting bytes is not a * multiple of the cipher block size * @throws NullPointerException if the Builder digest algorithm is {@code null} or * if the specified Builder secret key type is * {@code null} * @throws IllegalStateException if the cipher instance is not initialized for * encryption or decryption */ public String encrypt(String data) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException { if (data == null) return null; SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey())); byte[] dataBytes = data.getBytes(mBuilder.getCharsetName()); Cipher cipher = Cipher.getInstance(mBuilder.getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, secretKey, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom()); return Base64.encodeToString(cipher.doFinal(dataBytes), mBuilder.getBase64Mode()); } /** * This is a sugar method that calls encrypt method and catch the exceptions returning * {@code null} when it occurs and logging the error * * @param data the String to be encrypted * @return the encrypted String or {@code null} if you send the data as {@code null} */ public String encryptOrNull(String data) { try { return encrypt(data); } catch (Exception e) { e.printStackTrace(); return null; } } /** * This is a sugar method that calls encrypt method in background, it is a good idea to use this * one instead the default method because encryption can take several time and with this method * the process occurs in a AsyncTask, other advantage is the Callback with separated methods, * one for success and other for the exception * * @param data the String to be encrypted * @param callback the Callback to handle the results */ public void encryptAsync(final String data, final Callback callback) { if (callback == null) return; new Thread(new Runnable() { @Override public void run() { try { String encrypt = encrypt(data); if (encrypt == null) { callback.onError(new Exception("Encrypt return null, it normally occurs when you send a null data")); } callback.onSuccess(encrypt); } catch (Exception e) { callback.onError(e); } } }).start(); } /** * Decrypt a String * * @param data the String to be decrypted * @return the decrypted String or {@code null} if you send the data as {@code null} * @throws UnsupportedEncodingException if the Builder charset name is not supported or if * the Builder charset name is not supported * @throws NoSuchAlgorithmException if the Builder digest algorithm is not available * or if this has no installed provider that can * provide the requested by the Builder secret key * type or it is {@code null}, empty or in an invalid * format * @throws NoSuchPaddingException if no installed provider can provide the padding * scheme in the Builder digest algorithm * @throws InvalidAlgorithmParameterException if the specified parameters are inappropriate for * the cipher * @throws InvalidKeyException if the specified key can not be used to initialize * the cipher instance * @throws InvalidKeySpecException if the specified key specification cannot be used * to generate a secret key * @throws BadPaddingException if the padding of the data does not match the * padding scheme * @throws IllegalBlockSizeException if the size of the resulting bytes is not a * multiple of the cipher block size * @throws NullPointerException if the Builder digest algorithm is {@code null} or * if the specified Builder secret key type is * {@code null} * @throws IllegalStateException if the cipher instance is not initialized for * encryption or decryption */ public String decrypt(String data) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { if (data == null) return null; byte[] dataBytes = Base64.decode(data, mBuilder.getBase64Mode()); SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey())); Cipher cipher = Cipher.getInstance(mBuilder.getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, secretKey, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom()); byte[] dataBytesDecrypted = (cipher.doFinal(dataBytes)); return new String(dataBytesDecrypted); } /** * This is a sugar method that calls decrypt method and catch the exceptions returning * {@code null} when it occurs and logging the error * * @param data the String to be decrypted * @return the decrypted String or {@code null} if you send the data as {@code null} */ public String decryptOrNull(String data) { try { return decrypt(data); } catch (Exception e) { e.printStackTrace(); return null; } } /** * This is a sugar method that calls decrypt method in background, it is a good idea to use this * one instead the default method because decryption can take several time and with this method * the process occurs in a AsyncTask, other advantage is the Callback with separated methods, * one for success and other for the exception * * @param data the String to be decrypted * @param callback the Callback to handle the results */ public void decryptAsync(final String data, final Callback callback) { if (callback == null) return; new Thread(new Runnable() { @Override public void run() { try { String decrypt = decrypt(data); if (decrypt == null) { callback.onError(new Exception("Decrypt return null, it normally occurs when you send a null data")); } callback.onSuccess(decrypt); } catch (Exception e) { callback.onError(e); } } }).start(); } /** * creates a 128bit salted aes key * * @param key encoded input key * @return aes 128 bit salted key * @throws NoSuchAlgorithmException if no installed provider that can provide the requested * by the Builder secret key type * @throws UnsupportedEncodingException if the Builder charset name is not supported * @throws InvalidKeySpecException if the specified key specification cannot be used to * generate a secret key * @throws NullPointerException if the specified Builder secret key type is {@code null} */ private SecretKey getSecretKey(char[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance(mBuilder.getSecretKeyType()); KeySpec spec = new PBEKeySpec(key, mBuilder.getSalt().getBytes(mBuilder.getCharsetName()), mBuilder.getIterationCount(), mBuilder.getKeyLength()); SecretKey tmp = factory.generateSecret(spec); return new SecretKeySpec(tmp.getEncoded(), mBuilder.getKeyAlgorithm()); } /** * takes in a simple string and performs an sha1 hash * that is 128 bits long...we then base64 encode it * and return the char array * * @param key simple inputted string * @return sha1 base64 encoded representation * @throws UnsupportedEncodingException if the Builder charset name is not supported * @throws NoSuchAlgorithmException if the Builder digest algorithm is not available * @throws NullPointerException if the Builder digest algorithm is {@code null} */ private char[] hashTheKey(String key) throws UnsupportedEncodingException, NoSuchAlgorithmException { MessageDigest messageDigest = MessageDigest.getInstance(mBuilder.getDigestAlgorithm()); messageDigest.update(key.getBytes(mBuilder.getCharsetName())); return Base64.encodeToString(messageDigest.digest(), Base64.NO_PADDING).toCharArray(); } /** * When you encrypt or decrypt in callback mode you get noticed of result using this interface */ public interface Callback { /** * Called when encrypt or decrypt job ends and the process was a success * * @param result the encrypted or decrypted String */ void onSuccess(String result); /** * Called when encrypt or decrypt job ends and has occurred an error in the process * * @param exception the Exception related to the error */ void onError(Exception exception); } /** * This class is used to create an Encryption instance, you should provide ALL data or start * with the Default Builder provided by the getDefaultBuilder method */ public static class Builder { private byte[] mIv; private int mKeyLength; private int mBase64Mode; private int mIterationCount; private String mSalt; private String mKey; private String mAlgorithm; private String mKeyAlgorithm; private String mCharsetName; private String mSecretKeyType; private String mDigestAlgorithm; private String mSecureRandomAlgorithm; private SecureRandom mSecureRandom; private IvParameterSpec mIvParameterSpec; /** * @return an default builder with the follow defaults: * the default char set is UTF-8 * the default base mode is Base64 * the Secret Key Type is the PBKDF2WithHmacSHA1 * the default salt is "some_salt" but can be anything * the default length of key is 128 * the default iteration count is 65536 * the default algorithm is AES in CBC mode and PKCS 5 Padding * the default secure random algorithm is SHA1PRNG * the default message digest algorithm SHA1 */ public static Builder getDefaultBuilder(String key, String salt, byte[] iv) { return new Builder() .setIv(iv) .setKey(key) .setSalt(salt) .setKeyLength(128) .setKeyAlgorithm("AES") .setCharsetName("UTF8") .setIterationCount(1) .setDigestAlgorithm("SHA1") .setBase64Mode(Base64.DEFAULT) .setAlgorithm("AES/CBC/PKCS5Padding") .setSecureRandomAlgorithm("SHA1PRNG") .setSecretKeyType("PBKDF2WithHmacSHA1"); } /** * Build the Encryption with the provided information * * @return a new Encryption instance with provided information * @throws NoSuchAlgorithmException if the specified SecureRandomAlgorithm is not available * @throws NullPointerException if the SecureRandomAlgorithm is {@code null} or if the * IV byte array is null */ public Encryption build() throws NoSuchAlgorithmException { setSecureRandom(SecureRandom.getInstance(getSecureRandomAlgorithm())); setIvParameterSpec(new IvParameterSpec(getIv())); return new Encryption(this); } /** * @return the charset name */ private String getCharsetName() { return mCharsetName; } /** * @param charsetName the new charset name * @return this instance to follow the Builder patter */ public Builder setCharsetName(String charsetName) { mCharsetName = charsetName; return this; } /** * @return the algorithm */ private String getAlgorithm() { return mAlgorithm; } /** * @param algorithm the algorithm to be used * @return this instance to follow the Builder patter */ public Builder setAlgorithm(String algorithm) { mAlgorithm = algorithm; return this; } /** * @return the key algorithm */ private String getKeyAlgorithm() { return mKeyAlgorithm; } /** * @param keyAlgorithm the keyAlgorithm to be used in keys * @return this instance to follow the Builder patter */ public Builder setKeyAlgorithm(String keyAlgorithm) { mKeyAlgorithm = keyAlgorithm; return this; } /** * @return the Base 64 mode */ private int getBase64Mode() { return mBase64Mode; } /** * @param base64Mode set the base 64 mode * @return this instance to follow the Builder patter */ public Builder setBase64Mode(int base64Mode) { mBase64Mode = base64Mode; return this; } /** * @return the type of aes key that will be created, on KITKAT+ the API has changed, if you * are getting problems please @see http://android-developers.blogspot.com.br/2013/12/changes-to-secretkeyfactory-api-in.html */ private String getSecretKeyType() { return mSecretKeyType; } /** * @param secretKeyType the type of AES key that will be created, on KITKAT+ the API has * changed, if you are getting problems please @see http://android-developers.blogspot.com.br/2013/12/changes-to-secretkeyfactory-api-in.html * @return this instance to follow the Builder patter */ public Builder setSecretKeyType(String secretKeyType) { mSecretKeyType = secretKeyType; return this; } /** * @return the value used for salting */ private String getSalt() { return mSalt; } /** * @param salt the value used for salting * @return this instance to follow the Builder patter */ public Builder setSalt(String salt) { mSalt = salt; return this; } /** * @return the key */ private String getKey() { return mKey; } /** * @param key the key. * @return this instance to follow the Builder patter */ public Builder setKey(String key) { mKey = key; return this; } /** * @return the length of key */ private int getKeyLength() { return mKeyLength; } /** * @param keyLength the length of key * @return this instance to follow the Builder patter */ public Builder setKeyLength(int keyLength) { mKeyLength = keyLength; return this; } /** * @return the number of times the password is hashed */ private int getIterationCount() { return mIterationCount; } /** * @param iterationCount the number of times the password is hashed * @return this instance to follow the Builder patter */ public Builder setIterationCount(int iterationCount) { mIterationCount = iterationCount; return this; } /** * @return the algorithm used to generate the secure random */ private String getSecureRandomAlgorithm() { return mSecureRandomAlgorithm; } /** * @param secureRandomAlgorithm the algorithm to generate the secure random * @return this instance to follow the Builder patter */ public Builder setSecureRandomAlgorithm(String secureRandomAlgorithm) { mSecureRandomAlgorithm = secureRandomAlgorithm; return this; } /** * @return the IvParameterSpec bytes array */ private byte[] getIv() { return mIv; } /** * @param iv the byte array to create a new IvParameterSpec * @return this instance to follow the Builder patter */ public Builder setIv(byte[] iv) { mIv = iv; return this; } /** * @return the SecureRandom */ private SecureRandom getSecureRandom() { return mSecureRandom; } /** * @param secureRandom the Secure Random * @return this instance to follow the Builder patter */ public Builder setSecureRandom(SecureRandom secureRandom) { mSecureRandom = secureRandom; return this; } /** * @return the IvParameterSpec */ private IvParameterSpec getIvParameterSpec() { return mIvParameterSpec; } /** * @param ivParameterSpec the IvParameterSpec * @return this instance to follow the Builder patter */ public Builder setIvParameterSpec(IvParameterSpec ivParameterSpec) { mIvParameterSpec = ivParameterSpec; return this; } /** * @return the message digest algorithm */ private String getDigestAlgorithm() { return mDigestAlgorithm; } /** * @param digestAlgorithm the algorithm to be used to get message digest instance * @return this instance to follow the Builder patter */ public Builder setDigestAlgorithm(String digestAlgorithm) { mDigestAlgorithm = digestAlgorithm; return this; } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/ExtensionUtils.kt ================================================ package knf.kuma.commons import android.annotation.SuppressLint import android.app.Activity import android.content.ComponentName import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager import android.content.res.Resources import android.graphics.drawable.AnimationDrawable import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.os.Environment import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils import android.widget.ImageView import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.FontRes import androidx.annotation.IdRes import androidx.annotation.LayoutRes import androidx.annotation.MenuRes import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.Toolbar import androidx.asynclayoutinflater.view.AsyncLayoutInflater import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import com.squareup.picasso.Callback import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.custom.snackbar.SnackProgressBar import knf.kuma.custom.snackbar.SnackProgressBarManager import knf.kuma.database.CacheDB import knh.kuma.commons.cloudflarebypass.util.ConvertUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.ConnectionSpec import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import org.json.JSONArray import org.json.JSONObject import org.jsoup.Connection import org.jsoup.HttpStatusException import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.nield.kotlinstatistics.WeightedDice import pl.droidsonroids.jspoon.Jspoon import xdroid.toaster.Toaster import java.io.File import java.net.URLDecoder import java.net.URLEncoder import java.nio.charset.StandardCharsets import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine fun Toolbar.changeToolbarFont(@FontRes res: Int) { for (i in 0 until childCount) { val view = getChildAt(i) if (view is TextView && view.text == title) { view.typeface = ResourcesCompat.getFont(context, res) break } } } val String.urlFixed: String get() = if (!contains("animeflv.net")) "https://www3.animeflv.net$this" else this val String.fixedHost: String get() = if (!startsWith("http")) "https:$this" else this val LiveData.distinct: LiveData get() = this.distinctUntilChanged() fun getPackage(): String { return if (BuildConfig.BUILD_TYPE == "debug" || BuildConfig.BUILD_TYPE == "release" || BuildConfig.BUILD_TYPE == "playstore" || BuildConfig.BUILD_TYPE == "amazon") "knf.kuma" else "knf.kuma.${BuildConfig.BUILD_TYPE}" } val getUpdateDir: String get() = when (BuildConfig.BUILD_TYPE) { "debug", "release" -> "release" else -> BuildConfig.BUILD_TYPE } fun currentTime(): Long = System.currentTimeMillis() val ffFile: File get() = File(baseDir, "data.crypt") val admFile: File get() = File(baseDir, BuildConfig.ADM_FILE) val baseDir: File get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) File(Environment.getExternalStorageDirectory(), "UKIKU/backups") else App.context.filesDir fun verifiyFF() { try { if (PrefsUtil.isFamilyFriendly && !ffFile.exists()) { ffFile.parentFile?.mkdirs() ffFile.createNewFile() ffFile.writeText(PrefsUtil.ffPass) } else if (!PrefsUtil.isFamilyFriendly && ffFile.exists()) { PrefsUtil.isFamilyFriendly = true PrefsUtil.ffPass = ffFile.readText() CacheDB.INSTANCE.animeDAO().nukeEcchi() } } catch (e: Exception) { // } } fun MaterialDialog.safeShow(func: MaterialDialog.() -> Unit): MaterialDialog { doOnUIGlobal { try { lifecycleOwner() this.func() this@safeShow.show() } catch (e: Exception) { // } } return this } fun MaterialDialog.safeShow() { doOnUIGlobal { try { lifecycleOwner() this@safeShow.show() } catch (e: Exception) { // } } } fun MaterialDialog.safeDismiss() { try { dismiss() } catch (exception: Exception) { // } } fun Snackbar.safeDismiss() { try { dismiss() } catch (e: Exception) { e.printStackTrace() } } operator fun JSONArray.iterator(): Iterator = (0 until length()).asSequence().map { get(it) as JSONObject }.iterator() fun RecyclerView.verifyManager(size: Int = 115) { val manager = layoutManager if (manager is GridLayoutManager) { manager.spanCount = gridColumns(size) layoutManager = manager } setHasFixedSize(true) } val canGroupNotifications: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N fun gridColumns(size: Int = 115): Int { val metrics = App.context.resources.displayMetrics val dpWidht = metrics.widthPixels / metrics.density return (dpWidht / size).toInt().coerceAtLeast(1) } fun View.showSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT, animation: Int = Snackbar.ANIMATION_MODE_FADE): Snackbar { return Snackbar.make(this, text, duration).apply { animationMode = animation }.also { doOnUIGlobal { it.show() } } } fun View.showSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT, button: String, onAction: (View) -> Unit): Snackbar { return Snackbar.make(this, text, duration).apply { animationMode = Snackbar.ANIMATION_MODE_SLIDE setAction(button, onAction) }.also { doOnUIGlobal { it.show() } } } fun SnackProgressBarManager.showProgressSnackbar(text: String, duration: Int = SnackProgressBarManager.LENGTH_SHORT) { val snackbar = SnackProgressBar(SnackProgressBar.TYPE_HORIZONTAL, text) .setIsIndeterminate(true) .setProgressMax(100) .setShowProgressPercentage(false) doOnUIGlobal { show(snackbar, duration) } } fun View.createSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT): Snackbar { return Snackbar.make(this, text, duration) } val Int.asPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() fun Activity.bind(@IdRes res: Int): Lazy { @Suppress("UNCHECKED_CAST") return lazy { findViewById(res) } } fun View.bind(@IdRes res: Int): Lazy { @Suppress("UNCHECKED_CAST") return lazy { findViewById(res) } } fun View.optionalBind(@IdRes res: Int): Lazy { @Suppress("UNCHECKED_CAST") return lazy { findViewById(res) } } fun bind(activity: AppCompatActivity, @IdRes res: Int): Lazy { @Suppress("UNCHECKED_CAST") return lazy { activity.findViewById(res) } } fun optionalBind(activity: AppCompatActivity, @IdRes res: Int): Lazy { @Suppress("UNCHECKED_CAST") return lazy { activity.findViewById(res) } } fun Activity.optionalBind(@IdRes res: Int): Lazy { @Suppress("UNCHECKED_CAST") return lazy { findViewById(res) } } fun Request.execute(followRedirects: Boolean = true): Response { return OkHttpClient().newBuilder().apply { followRedirects(followRedirects) connectionSpecs( listOf( ConnectionSpec.CLEARTEXT, ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .allEnabledTlsVersions() .allEnabledCipherSuites() .build() ) ) }.build().newCall(this).execute() } fun Request.executeNoSSl(followRedirects: Boolean = true): Response { return NoSSLOkHttpClient.get().newBuilder().apply { followRedirects(followRedirects) connectionSpecs( listOf( ConnectionSpec.CLEARTEXT, ConnectionSpec.Builder(ConnectionSpec.COMPATIBLE_TLS) .allEnabledTlsVersions() .allEnabledCipherSuites() .build() ) ) }.build().newCall(this).execute() } val safeContext: Context get() = App.context val isTV: Boolean get() = App.context.resources.getBoolean(R.bool.isTv) @ColorInt fun Int.toColor(): Int { return ContextCompat.getColor(App.context, this) } fun ImageView.load(link: String?, callback: Callback? = null) { try { Glide.with(this).load(GlideUrl(link, BypassUtil.getLazyHeaders())) .diskCacheStrategy(DiskCacheStrategy.ALL) .listener(object : RequestListener { override fun onLoadFailed( e: GlideException?, model: Any?, target: Target, isFirstResource: Boolean ): Boolean { callback?.onError(e) return false } override fun onResourceReady( resource: Drawable, model: Any, target: Target?, dataSource: DataSource, isFirstResource: Boolean ): Boolean { callback?.onSuccess() return false } }).into(this) } catch (e: Exception) { e.printStackTrace() } } fun ImageView.loadGlide(link: String) { Glide.with(safeContext).load(link).into(this) } fun ImageView.load(uri: Uri?, callback: Callback? = null) { Glide.with(this).load(uri) .listener(object : RequestListener { override fun onLoadFailed( e: GlideException?, model: Any?, target: Target, isFirstResource: Boolean ): Boolean { callback?.onError(e) return false } override fun onResourceReady( resource: Drawable, model: Any, target: Target?, dataSource: DataSource, isFirstResource: Boolean ): Boolean { callback?.onSuccess() return false } }).into(this) } fun retry(numOfRetries: Int, block: () -> T): T { var throwable: Throwable? = null (1..numOfRetries).forEach { try { return block() } catch (e: Throwable) { throwable = e } } throw throwable!! } fun noCrash(enableLog: Boolean = true, func: () -> Unit): String? { return try { func() null } catch (e: Exception) { if (enableLog) e.printStackTrace() e.message } } suspend fun noCrashSuspend(enableLog: Boolean = true, func: suspend () -> Unit): String? { return try { func() null } catch (e: Exception) { if (enableLog) e.printStackTrace() e.message } } fun noCrashException(enableLog: Boolean = true, func: () -> Unit): Exception? { return try { func() null } catch (e: Exception) { if (enableLog) e.printStackTrace() e } } fun noCrashExec(exec: () -> Unit = {}, func: () -> Unit) { try { func() } catch (e: Exception) { e.printStackTrace() exec() } } fun noCrashLet(onCrash: () -> Unit = {}, func: () -> T): T? { return try { func() } catch (e: Exception) { e.printStackTrace() onCrash() null } } fun noCrashLet(onCrash: T, func: () -> T): T { return try { func() } catch (e: Exception) { e.printStackTrace() onCrash } } fun noCrashLetNullable(onCrash: T? = null, func: () -> T): T? { return try { func() } catch (e: Exception) { e.printStackTrace() onCrash } } fun String?.toast() { if (!this.isNullOrEmpty()) Toaster.toast(this) } fun String?.toastLong() { if (!this.isNullOrEmpty()) Toaster.toastLong(this) } fun LifecycleOwner.doOnUI(enableLog: Boolean = true, onLog: (text: String) -> Unit = {}, func: suspend () -> Unit) { lifecycleScope.launch(Dispatchers.Main) { noCrashSuspend(enableLog) { func() }?.also { onLog(it) } } } fun doOnUIGlobal(enableLog: Boolean = true, onLog: (text: String) -> Unit = {}, func: suspend () -> Unit) { GlobalScope.launch(Dispatchers.Main) { noCrashSuspend(enableLog) { func() }?.also { onLog(it) } } } fun doOnUIException(enableLog: Boolean = true, onLog: (e: Exception) -> Unit = {}, func: () -> Unit) { GlobalScope.launch(Dispatchers.Main) { noCrashException(enableLog) { func() }?.also { onLog(it) } } } fun List.transform(): List = this.map { it as T } fun MutableList.removeAll(vararg elements: Collection) { elements.forEach { removeAll(it) } } infix fun Collection?.notSameContent(collection: Collection?) = !isSameContent(collection) infix fun Collection?.isSameContent(collection: Collection?) = collection.let { this != null && it != null && this.size == it.size && this.containsAll(it) } fun NotificationCompat.Builder.create(func: NotificationCompat.Builder.() -> Unit): NotificationCompat.Builder { this.func() return this } fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false, context: Context = this.context): View = LayoutInflater.from(context).inflate(layout, this, attachToRoot) fun inflate(context: Context, @LayoutRes layout: Int, attachToRoot: Boolean = false): View = LayoutInflater.from(context).inflate(layout, null, attachToRoot) suspend fun asyncInflate(context: Context, @LayoutRes layout: Int, attachToRoot: Boolean = false): View = withContext(Dispatchers.Main) { suspendCoroutine { AsyncLayoutInflater(context).inflate(layout, null) { view, _, _ -> it.resume(view) } } } fun File.safeDelete(log: Boolean = false) { try { delete() } catch (e: Exception) { if (log) e.printStackTrace() } } fun RecyclerView.removeAllDecorations() { while (itemDecorationCount > 0) removeItemDecorationAt(0) } fun Any?.isNull(): Boolean { return this == null } fun Any?.notNull(): Boolean { return this != null } fun String.r(from: String, to: String) = replace(from, to) fun Context.findActivity(): Activity? { var context = this while (context is ContextWrapper) { if (context is Activity) { return context } context = context.baseContext } return null } fun Context.findLifecycleOwner(): LifecycleOwner? { var context = this while (context is ContextWrapper) { if (context is LifecycleOwner) { return context } context = context.baseContext } return null } @UiThread fun ImageView.setAnimatedResource(@DrawableRes res: Int) { setImageResource(res) val animated = drawable as AnimationDrawable animated.callback = this animated.setVisible(true, true) animated.start() } fun FloatingActionButton.forceHide() { val params = layoutParams as? CoordinatorLayout.LayoutParams params?.behavior = null requestLayout() visibility = View.GONE } fun httpRequest(url: String) = NoSSLOkHttpClient.get().newCall(okHttpCookies(url)).execute() fun httpJsoup(url: String) = Jsoup.connect(url) .ignoreContentType(true) .ignoreHttpErrors(true) .execute() fun jsoupCookies(url: String?, followRedirects: Boolean = true): Connection = Jsoup.connect(url) .cookies(BypassUtil.getMapCookie(App.context)) .userAgent(BypassUtil.userAgent) .timeout(PrefsUtil.timeoutTime.toInt() * 1000) .followRedirects(followRedirects) fun jsoupCookiesAdapter(url: String?, adapter: Class, followRedirects: Boolean = true): T = Jspoon.create().adapter(adapter).fromHtml( Jsoup.connect(url) .cookies(BypassUtil.getMapCookie(App.context)) .userAgent(BypassUtil.userAgent) .timeout(PrefsUtil.timeoutTime.toInt() * 1000) .followRedirects(followRedirects) .get().outerHtml() ) fun jsoupCookiesDir(url: String?, useCookies: Boolean): Connection = Jsoup.connect(url).apply { if (useCookies) if (PrefsUtil.useDefaultUserAgent && !PrefsUtil.alwaysGenerateUA) { cookies(ConvertUtil.List2Map(PrefsUtil.dirCookies)) } else { cookies(BypassUtil.getMapCookie(App.context)) } if (PrefsUtil.useDefaultUserAgent && !PrefsUtil.alwaysGenerateUA) userAgent(PrefsUtil.userAgentDir) else userAgent(PrefsUtil.userAgent) timeout(PrefsUtil.timeoutTime.toInt() * 1000) followRedirects(true) } fun okHttpCookies(url: String, method: String = "GET"): Request = Request.Builder().apply { url(url) method(method, if (method == "POST") "".toRequestBody("text/plain".toMediaType()) else null) header("User-Agent", BypassUtil.userAgent) header("Cookie", BypassUtil.getStringCookie(App.context)) }.build() fun okHttpDocument(url: String): Document = Jsoup.parse(okHttpCookies(url).execute(true).use { if (it.isSuccessful) it.body?.string() else throw IllegalStateException("Response error Url: ${it.request.url}, code: ${it.code}") }) fun isHostValid(hostName: String): Boolean { if (validateAds(hostName)) return true return when (hostName) { "fex.net", "api.crashlytics.com", "e.crashlytics.com", "reports.crashlytics.com", "sdk-android.ad.smaato.net", "cdn.myanimelist.net", "myanimelist.cdn-dena.com", "settings.crashlytics.com", "somoskudasai.com", "animeflv.net", "m.animeflv.net", "github.com", "raw.githubusercontent.com", "cdn.animeflv.net", "s1.animeflv.net", "streamango.com", "ok.ru", "www.rapidvideo.com", "us-central1-nu-client.cloudfunctions.net", "", "worldvideodownload.com", "okvid.download", "www.yourupload.com" -> true else -> isVideoHostName(hostName) }.also { if (!it) Log.e("Hostname", "Not verified: $hostName") } } private fun validateAds(hostName: String): Boolean { listOf( "android", "doubleclick", "invitemedia.com", "media.admob.com", "gstatic", "google", "goo.gl", "gvtl", "gvt2", "urchin", "gkecnapps", "youtube", "youtu.be", "yt.be", "ytimg", "g.co", "ggpht", "gkecnapps", "appbrain", "apptornado", "startappservice", "criteo", "appcoachs" ).forEach { if (hostName.contains(it)) return true } return false } private fun isVideoHostName(hostName: String): Boolean { return when { hostName.contains("google.com") -> true hostName.contains("fex.net") || hostName.contains("content-na.drive.amazonaws.com") || hostName.contains("mediafire") || hostName.contains("fruithosted.net") || hostName.contains("mp4upload.com") || hostName.contains("storage.googleapis.com") || hostName.contains("playercdn.net") || hostName.contains("vidcache.net") || hostName.contains("fembed.com") || hostName.contains("leasewebcdn.me") || hostName.contains("fvs.io") -> true else -> false } } fun diceOf(default: T? = null, mapCreator: MutableMap.() -> Unit): T { val map = mutableMapOf() mapCreator(map) if (default != null && map.isEmpty()) return default return WeightedDice(map).roll() } inline var View.isVisibleAnimate: Boolean get() = isVisible set(value) { isVisible = value startAnimation(AnimationUtils.loadAnimation(context, if (value) R.anim.fadein else R.anim.fadeout)) } fun View.onClickMenu( @MenuRes menu: Int, showIcons: Boolean = true, hideItems: () -> List = { emptyList() }, onItemClicked: (item: MenuItem) -> Unit = {} ) = this.setOnClickListener { popUpMenu(this@onClickMenu, menu, showIcons, hideItems, onItemClicked) } @SuppressLint("RestrictedApi") fun popUpMenu( view: View, @MenuRes menu: Int, showIcons: Boolean = true, hideItems: () -> List = { emptyList() }, onItemClicked: (item: MenuItem) -> Unit = {} ) = doOnUIGlobal { PopupMenu(view.context, view).apply { inflate(menu) setOnMenuItemClickListener { onItemClicked.invoke(it) true } hideItems().forEach { getMenu().findItem(it).isVisible = false } }.show() } fun AppCompatActivity.setSurfaceBars() { val surfaceColor = getSurfaceColor() window.apply { statusBarColor = surfaceColor navigationBarColor = surfaceColor } } fun AppCompatActivity.getSurfaceColor(): Int { return TypedValue().apply { theme.resolveAttribute(R.attr.colorSurface, this, true) }.data } fun Fragment.getSurfaceColor(): Int { return (requireActivity() as AppCompatActivity).getSurfaceColor() } fun String.resolveRedirection(tryCount: Int = 0): String = try { jsoupCookies(this).execute().url().toString() } catch (e: HttpStatusException) { if (tryCount >= 3) this else resolveRedirection(tryCount + 1) } fun List>.toArray(): Array { val list = mutableListOf() forEach { e -> list.add(e.first) list.add(e.second) } return list.toTypedArray() } fun urlEncode(url: String): String { return try { if (Build.VERSION.SDK_INT >= 33) { URLEncoder.encode(url, StandardCharsets.UTF_8) } else { URLEncoder.encode(url, "utf-8") } } catch (e: Exception) { url } } fun urlDecode(url: String): String { return try { if (Build.VERSION.SDK_INT >= 33) { URLDecoder.decode(url, StandardCharsets.UTF_8) } else { URLDecoder.decode(url, "utf-8") } } catch (e: Exception) { url } } val isFullMode: Boolean get() = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE != "playstore" private fun isIntentResolved(ctx: Context, intent: Intent): Boolean { return ctx.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null } fun FragmentActivity.findView(@IdRes id: Int): Lazy = object : Lazy { private var view: T? = null override val value: T get() = view ?: findViewById(id).also { view = it } override fun isInitialized(): Boolean { return view != null } } val isMIUI: Boolean by lazy { Build.MANUFACTURER.lowercase().contains("huawei") || Build.BRAND.contains("huawei") || isIntentResolved( safeContext, Intent("miui.intent.action.OP_AUTO_START").addCategory(Intent.CATEGORY_DEFAULT) ) || isIntentResolved( safeContext, Intent().setComponent( ComponentName( "com.miui.securitycenter", "com.miui.permcenter.autostart.AutoStartManagementActivity" ) ) ) || isIntentResolved( safeContext, Intent("miui.intent.action.POWER_HIDE_MODE_APP_LIST").addCategory(Intent.CATEGORY_DEFAULT) ) || isIntentResolved( safeContext, Intent().setComponent( ComponentName("com.miui.securitycenter", "com.miui.powercenter.PowerSettings") ) ) } ================================================ FILE: app/src/main/java/knf/kuma/commons/FileUtil.kt ================================================ package knf.kuma.commons import android.annotation.TargetApi import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Build import android.os.storage.StorageManager import android.provider.DocumentsContract import android.util.Pair import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.download.FileAccessHelper import org.jetbrains.anko.doAsync import java.io.File import java.io.OutputStream import java.lang.reflect.Array import java.util.Locale object FileUtil { private const val PRIMARY_VOLUME_NAME = "primary" internal var TAG = "TAG" val externalMounts: HashSet get() { val out = HashSet() val reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*" val s = StringBuilder() try { val process = ProcessBuilder().command("mount").redirectErrorStream(true).start() process.waitFor() val inputStream = process.inputStream val buffer = ByteArray(1024) while (inputStream.read(buffer) != -1) { s.append(String(buffer)) } inputStream.close() } catch (e: Exception) { e.printStackTrace() } val lines = s.toString().split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() for (line in lines) { if (!line.lowercase(Locale.US).contains("asec")) { if (line.matches(reg.toRegex())) { val parts = line.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() for (part in parts) { if (part.startsWith("/")) { if (!part.lowercase(Locale.US).contains("vold")) { out.add(part) } } } } } } return out } fun getFullPathFromTreeUri(treeUri: Uri?, con: Context): String? { try { treeUri ?: return null var volumePath: String? = getVolumePath(getVolumeIdFromTreeUri(treeUri), con) ?: return File.separator if (volumePath?.endsWith(File.separator) == true) { volumePath = volumePath.substring(0, volumePath.length - 1) } var documentPath = getDocumentPathFromTreeUri(treeUri) if (documentPath.endsWith(File.separator)) { documentPath = documentPath.substring(0, documentPath.length - 1) } return if (documentPath.isNotEmpty()) { if (documentPath.startsWith(File.separator)) { volumePath + documentPath } else { volumePath + File.separator + documentPath } } else { volumePath } } catch (e: Exception) { return null } } private fun getVolumePath(volumeId: String?, con: Context): String? { try { val mStorageManager = con.getSystemService(Context.STORAGE_SERVICE) as StorageManager val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume") val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList") val getUuid = storageVolumeClazz.getMethod("getUuid") val getPath = storageVolumeClazz.getMethod("getPath") val isPrimary = storageVolumeClazz.getMethod("isPrimary") val result = getVolumeList.invoke(mStorageManager) val length = Array.getLength(result) for (i in 0 until length) { val storageVolumeElement = Array.get(result, i) val uuid = getUuid.invoke(storageVolumeElement) as String? val primary = isPrimary.invoke(storageVolumeElement) as Boolean // primary volume? if (primary && PRIMARY_VOLUME_NAME == volumeId) { return getPath.invoke(storageVolumeElement) as String } // other volumes? if (uuid != null) { if (uuid == volumeId) { return getPath.invoke(storageVolumeElement) as String } } } // not found. return null } catch (ex: Exception) { return null } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private fun getVolumeIdFromTreeUri(treeUri: Uri): String? { val docId = DocumentsContract.getTreeDocumentId(treeUri) val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return if (split.isNotEmpty()) { split[0] } else { null } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private fun getDocumentPathFromTreeUri(treeUri: Uri): String { val docId = DocumentsContract.getTreeDocumentId(treeUri) val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return if (split.size >= 2) { split[1] } else { File.separator } } fun moveFile(resolver: ContentResolver, uri: Uri?, outputStream: OutputStream?, delete: Boolean = true): LiveData> { val liveData = MutableLiveData>() if (uri == null || outputStream == null) { doOnUIGlobal { liveData.value = Pair(-1, true) } return liveData } doAsync { try { val inputStream = resolver.openInputStream(uri) val total = inputStream?.available()?.toLong() ?: 0 val buffer = ByteArray(128 * 1024) var read: Int = inputStream?.read(buffer) ?: 0 var current: Long = 0 while (read != -1) { outputStream.write(buffer, 0, read) current += read.toLong() val prog = (current * 100 / total).toInt() doOnUIGlobal { liveData.value = Pair(prog, false) } read = inputStream?.read(buffer) ?: 0 } inputStream?.close() outputStream.flush() outputStream.close() try { if (delete) DocumentsContract.deleteDocument(resolver, uri) } catch (e: Exception) { e.printStackTrace() } doOnUIGlobal { liveData.value = Pair(100, true) } } catch (e: Exception) { e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) doOnUIGlobal { liveData.value = Pair(-1, true) } } } return liveData } fun moveFiles(resolver: ContentResolver, pairs: MutableList>): LiveData, Boolean>> { val liveData = MutableLiveData, Boolean>>() doAsync { val ps = "Importando archivos: %d/%d" val gTotal = pairs.size var success = 0 for ((g_count, pair) in pairs.withIndex()) { try { val inputStream = resolver.openInputStream(pair.first) val outputStream = FileAccessHelper.getOutputStream(pair.second) val total = inputStream?.available()?.toLong() ?: 0 val buffer = ByteArray(128 * 1024) var read: Int = inputStream?.read(buffer) ?: 0 var current: Long = 0 while (read != -1) { outputStream?.write(buffer, 0, read) current += read.toLong() val prog = (current * 100 / total).toInt() doOnUIGlobal { liveData.value = Pair(Pair(String.format(Locale.US, ps, g_count, gTotal), prog), false) } read = inputStream?.read(buffer) ?: 0 } inputStream?.close() outputStream?.flush() outputStream?.close() doOnUIGlobal { liveData.value = Pair(Pair(String.format(Locale.US, ps, g_count, gTotal), 100), false) } try { DocumentsContract.deleteDocument(resolver, pair.first) } catch (e: Exception) { e.printStackTrace() } success++ } catch (e: Exception) { e.printStackTrace() FileAccessHelper.delete(pair.second) } } val finalSuccess = success doOnUIGlobal { liveData.value = Pair(Pair(String.format(Locale.US, ps, gTotal, gTotal), finalSuccess), true) } } return liveData } fun moveFile(file_name: String, callback: MoveCallback) { doAsync { try { val inputStream = FileAccessHelper.getTmpInputStream(file_name) val outputStream = FileAccessHelper.getOutputStream(file_name) val total = inputStream?.available()?.toLong() ?: 0 val buffer = ByteArray(128 * 1024) var read: Int = inputStream?.read(buffer) ?: 0 var current: Long = 0 while (read != -1) { outputStream?.write(buffer, 0, read) current += read.toLong() val prog = (current * 100 / total).toInt() callback.onProgress(Pair(prog, false)) read = inputStream?.read(buffer) ?: 0 } inputStream?.close() outputStream?.flush() outputStream?.close() try { val file = FileAccessHelper.getTmpFile(file_name) file.delete() if (file.parentFile?.list()?.isEmpty() == true) file.parentFile?.delete() } catch (e: Exception) { e.printStackTrace() } callback.onProgress(Pair(100, true)) } catch (e: Exception) { e.printStackTrace() FileAccessHelper.getTmpFile(file_name).delete() callback.onProgress(Pair(-1, true)) } } } interface MoveCallback { fun onProgress(pair: Pair) } } ================================================ FILE: app/src/main/java/knf/kuma/commons/FileWrapper.kt ================================================ package knf.kuma.commons import android.os.Build import android.os.Environment import androidx.documentfile.provider.DocumentFile import knf.kuma.App import knf.kuma.download.FileAccessHelper import java.io.File import java.io.InputStream abstract class FileWrapper(val path: String) { abstract var exist: Boolean abstract fun existForced(): Boolean abstract fun file(): File? abstract fun name(): String abstract fun length(): Long? abstract fun lastModified(): Long? abstract fun inputStream(): InputStream? abstract fun parentSize(): Int abstract fun generate(): T abstract fun reset() companion object { fun create(file_name: String?): FileWrapper<*> { file_name ?: throw IllegalStateException("Path can't be null!") return when { PrefsUtil.downloadType == "0" -> NormalFileWrapper(file_name) Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> DocumentFileWrapper(file_name) else -> NormalPreQFileWrapper(file_name) } } fun fromFileName(file_name: String): File = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) } } class NormalFileWrapper(path: String) : FileWrapper(path) { private var mFile = generate() override var exist = mFile?.exists() == true override fun existForced(): Boolean { reset() return exist } override fun file(): File? = mFile override fun name(): String = mFile?.name ?: path override fun length(): Long? = mFile?.length() override fun lastModified(): Long? = mFile?.lastModified() override fun inputStream(): InputStream? = mFile?.inputStream() override fun parentSize(): Int = mFile?.parentFile?.list()?.size ?: 0 override fun generate(): File? = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(path)).listFiles { file -> file.name.contains(path) }?.let { if (it.isNotEmpty()) it[0] else null } override fun reset() { mFile = generate() exist = mFile?.exists() == true } } class NormalPreQFileWrapper(path: String) : FileWrapper(path) { private var mFile = generate() override var exist = mFile?.exists() == true override fun existForced(): Boolean { reset() return exist } override fun file(): File? = mFile override fun name(): String = mFile?.name ?: path override fun length(): Long? = mFile?.length() override fun lastModified(): Long? = mFile?.lastModified() override fun inputStream(): InputStream? = mFile?.inputStream() override fun parentSize(): Int = mFile?.parentFile?.list()?.size ?: 0 override fun generate(): File? = File(FileUtil.getFullPathFromTreeUri(FileAccessHelper.treeUri, App.context), "UKIKU/downloads/" + PatternUtil.getNameFromFile(path)).listFiles { file -> file.name.contains(path) }?.let { if (it.isNotEmpty()) it[0] else null } override fun reset() { mFile = generate() exist = mFile?.exists() == true } } class DocumentFileWrapper(path: String) : FileWrapper(path) { private var document = generate() override var exist = document?.exists() == true override fun existForced(): Boolean { reset() return exist } override fun file(): File? = FileUtil.getFullPathFromTreeUri(document?.uri, App.context)?.let { File(it) } override fun name(): String = document?.name ?: path override fun length(): Long? = document?.length() override fun lastModified(): Long? = document?.lastModified() override fun inputStream(): InputStream? = document?.let { App.context.contentResolver.openInputStream(it.uri) } override fun parentSize(): Int = document?.parentFile?.listFiles()?.size ?: 0 override fun generate(): DocumentFile? = FileAccessHelper.treeUri?.let { uri -> FileAccessHelper.find(DocumentFile.fromTreeUri(App.context, uri), "UKIKU/downloads/" + PatternUtil.getNameFromFile(path), false)?.listFiles()?.let { list -> var file: DocumentFile? = null list.forEach { if (it.name?.contains(path) == true) { file = it return@forEach } } file } } override fun reset() { document = generate() exist = document?.exists() == true } } ================================================ FILE: app/src/main/java/knf/kuma/commons/Network.kt ================================================ package knf.kuma.commons import android.annotation.SuppressLint import android.content.Context import android.content.Context.WIFI_SERVICE import android.net.ConnectivityManager import android.net.wifi.WifiManager import android.text.format.Formatter import knf.kuma.App import java.io.BufferedReader import java.io.FileInputStream import java.io.InputStreamReader @SuppressLint("StaticFieldLeak") object Network { val isConnected: Boolean get() { return try { val cm = App.context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager val activeNetwork = cm?.activeNetworkInfo activeNetwork != null && activeNetwork.isConnected } catch (e: Exception) { false } } val ipAddress: String get() { val wm = App.context.applicationContext.getSystemService(WIFI_SERVICE) as? WifiManager return Formatter.formatIpAddress(wm?.connectionInfo?.ipAddress ?: 0) } val isAdsBlocked: Boolean by lazy { return@lazy try { BufferedReader(InputStreamReader(FileInputStream("/etc/hosts"))).readLines().forEach { if (it.contains("admob") || it.contains("appbrains")) return@lazy true } false } catch (e: Exception) { false } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/NoSSLOkHttpClient.kt ================================================ package knf.kuma.commons import knf.kuma.custom.TlsOnlySocketFactory import okhttp3.Cache import okhttp3.OkHttpClient import java.io.File import java.security.cert.CertificateException import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager object NoSSLOkHttpClient { private val appCache by lazy { Cache(File(safeContext.cacheDir, "okhttpcache"), 10 * 1024 * 1024) } fun get(): OkHttpClient { try { val trustAllCerts = arrayOf(object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array, authType: String) { } @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array, authType: String) { } override fun getAcceptedIssuers(): Array { return arrayOf() } }) val sslContext = SSLContext.getInstance("SSL").apply { init(null, trustAllCerts, java.security.SecureRandom()) } val sslSocketFactory = if (isMIUI) { TlsOnlySocketFactory(sslContext.socketFactory) } else { sslContext.socketFactory } val builder = OkHttpClient.Builder().apply { connectTimeout(PrefsUtil.timeoutTime, TimeUnit.SECONDS) readTimeout(PrefsUtil.timeoutTime, TimeUnit.SECONDS) sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) cache(appCache) hostnameVerifier { _, _ -> /*isHostValid(hostName)*/ true } } /*val dns = DnsOverHttps.Builder().client(builder).apply { url("https://dns.google/dns-query".toHttpUrl()) bootstrapDnsHosts(InetAddress.getByName("8.8.4.4"), InetAddress.getByName("8.8.8.8")) }.build() return builder.newBuilder().dns(dns).build()*/ return builder.build() } catch (e: Exception) { throw RuntimeException(e) } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/PatternUtil.kt ================================================ package knf.kuma.commons import android.os.Build import android.text.Html import android.util.Log import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import knf.kuma.App import knf.kuma.pojos.AnimeObject import java.util.regex.Pattern object PatternUtil { @Suppress("DEPRECATION") fun fromHtml(html: String): String { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) Html.fromHtml(html.r("\\\\u", "\\u").r("\\/", "/"), Html.FROM_HTML_MODE_LEGACY).toString() else Html.fromHtml(html.r("\\\\u", "\\u").r("\\/", "/")).toString() } fun getLinkNumber(link: String): String { val pattern = Pattern.compile("/(\\d+)[/.]") val matcher = pattern.matcher(link) matcher.find() return matcher.group(1) } fun getRapidLink(link: String): String { val pattern = Pattern.compile("value=([\\w#.]+)") val matcher = pattern.matcher(link) matcher.find() return "https://www.rapidvideo.com/e/" + matcher.group(1) } fun getRapidVideoLink(link: String): String { val pattern = Pattern.compile("\"(http.*\\.mp4)\"") val matcher = pattern.matcher(link) matcher.find() return matcher.group(1) } fun getYULink(link: String): String { val pattern = Pattern.compile("\"(.*yourupload.*)\"") val matcher = pattern.matcher(link) matcher.find() return matcher.group(1) } fun getYUvideoLink(link: String): String { val pattern = Pattern.compile("file: ?'(.*vidcache.*mp4)'") val matcher = pattern.matcher(link) matcher.find() return matcher.group(1) } fun getLinkId(link: String): String { val matcher = Pattern.compile("^.*/(.*)-\\d+$").matcher(link) matcher.find() return matcher.group(1) } fun getLinkNum(link: String): String { val matcher = Pattern.compile("^.*-(\\d+)$").matcher(link) matcher.find() return matcher.group(1) } fun getFileName(link: String): String { return try { val matcher = Pattern.compile("^.*/(.*-\\d+\\.?\\d*)$").matcher(link) matcher.find() matcher.group(1) + ".mp4" } catch (e: Exception) { Log.e("Pattern", "No name found in: $link", e) "N-F.mp4" } } fun getRootFileName(link: String): String { return try { val matcher = Pattern.compile("^.*/([a-z\\-\\d]+).*$").matcher(link) matcher.find() matcher.group(1) } catch (e: Exception) { Log.e("Pattern", "No name found in: $link", e) "N-F" } } fun getNameFromFile(file: String?): String { if (file.isNull()) return "" val matcher = Pattern.compile("^.*\\$(.*)-\\d+\\.?\\d*\\.mp4$").matcher(file) matcher.find() return noCrashLet("null/") { matcher.group(1) + "/" } } fun getNumFromFile(file: String): String { val matcher = Pattern.compile("^.*\\$[\\w-]+-(\\d+\\.?\\d*)\\.mp4$").matcher(file) matcher.find() return matcher.group(1) } fun getEidFromFile(file: String): String { val matcher = Pattern.compile("^(-?\\d+)\\$.*$").matcher(file) matcher.find() return matcher.group(1) } fun extractLink(html: String): String { val matcher = Pattern.compile("https?://[a-zA-Z0-9.=?/!&#_\\-]+|/[a-zA-Z0-9.=?/!&#_\\-]+").matcher(html) matcher.find() return matcher.group(0) } fun extractMangoLink(html: String): String { val matcher = Pattern.compile("\"(https.*streamango\\.com[/a-z]+)\"").matcher(html) matcher.find() return matcher.group(1) } fun extractMediaLink(html: String): String { val matcher = Pattern.compile("www\\.mediafire[a-zA-Z0-a.=?/&%]+").matcher(html) matcher.find() return "https://" + matcher.group().replace("%2F", "/") } fun extractOkruLink(html: String): String { val matcher = Pattern.compile("\"(https://ok\\.ru.*)\"").matcher(html) matcher.find() return matcher.group(1) } fun getAnimeUrl(chapter: String, aid: String): String { return "https://www3.animeflv.net/anime/" + aid + chapter.substring( chapter.lastIndexOf("/"), chapter.lastIndexOf("-") ) } fun getCover(aid: String?): String { return "https://www3.animeflv.net/uploads/animes/covers/$aid.jpg" } fun getThumb(aid: String?): String { return "https://ukiku.app/thumbs/$aid.jpg" } fun getCoverGlide(aid: String?): GlideUrl { return GlideUrl( "https://m.animeflv.net/uploads/animes/covers/$aid.jpg", LazyHeaders.Builder().apply { addHeader("Cookie", BypassUtil.getStringCookie(App.context)) addHeader("User-Agent", BypassUtil.userAgent) }.build() ) } fun getBanner(aid: String): String { return "https://www3.animeflv.net/uploads/animes/banners/$aid.jpg" } fun getEpListMap(code: String): HashMap { val map = LinkedHashMap() val matcher = Pattern.compile("\\[(\\d+\\.?\\d?),(\\d+)]").matcher(code) while (matcher.find()) { map[matcher.group(1)] = matcher.group(2) } return map } fun isCustomSearch(s: String): Boolean { return s.matches("^:[a-z]+:.*$".toRegex()) } fun getCustomSearch(s: String): String { val matcher = Pattern.compile("^:[a-z]+:(.*$)").matcher(s) matcher.find() return matcher.group(1) } fun getCustomAttr(s: String): String { val matcher = Pattern.compile("^:([a-z]+):.*$").matcher(s) matcher.find() return matcher.group(1) } fun getEids(chapters: MutableList): MutableList = chapters.map { it.eid }.toMutableList() } ================================================ FILE: app/src/main/java/knf/kuma/commons/PicassoSingle.kt ================================================ package knf.kuma.commons import android.annotation.SuppressLint import com.squareup.picasso.OkHttp3Downloader import com.squareup.picasso.Picasso import knf.kuma.App import okhttp3.ConnectionSpec import okhttp3.OkHttpClient object PicassoSingle { @SuppressLint("StaticFieldLeak") private lateinit var picasso: Picasso fun get(): Picasso { if (!::picasso.isInitialized) picasso = create() return picasso } private fun create(): Picasso = Picasso.Builder(App.context) .downloader( OkHttp3Downloader( OkHttpClient().newBuilder() .connectionSpecs( listOf( ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT ) ) .addInterceptor { val nRequest = it.request().newBuilder().apply { addHeader("Cookie", BypassUtil.getStringCookie(App.context)) addHeader("User-Agent", BypassUtil.userAgent) }.build() it.proceed(nRequest) }.build() ) ).build() fun clear() { picasso = create() } } ================================================ FILE: app/src/main/java/knf/kuma/commons/PrefsUtil.kt ================================================ package knf.kuma.commons import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Build import androidx.lifecycle.LiveData import androidx.preference.PreferenceManager import com.securepreferences.SecurePreferences import knf.kuma.App import knf.kuma.R import knf.kuma.ads.AdsUtils import knf.kuma.player.CustomExoPlayer import knf.kuma.player.VideoActivity import knf.kuma.uagen.randomUA import knh.kuma.commons.cloudflarebypass.util.ConvertUtil import java.net.HttpCookie import java.util.UUID @SuppressLint("StaticFieldLeak") object PrefsUtil { private var context: Context = App.context val layType: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("lay_type", context.getString(R.string.layType)) ?: "0" val themeOption: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("theme_option", context.getString(R.string.theme_default)) ?: context.getString(R.string.theme_default) val themeColor: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("theme_color", "0") ?: "0" var favsOrder: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getInt("favs_order", 0) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putInt("favs_order", value).apply() var dirOrder: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getInt("dir_order", 0) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putInt("dir_order", value).apply() var achievementsVersion: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getInt("achievements_version", 0) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putInt("achievements_version", value).apply() val isChapsAsc: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("asc_chapters", false) var isDirectoryFinished: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("directory_finished", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("directory_finished", value).apply() var isFetchDBReset: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("fetch_db_reset", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("fetch_db_reset", value).apply() val isAdsEnabled: Boolean get() = (!isSubscriptionEnabled && AdsUtils.remoteConfigs.getBoolean("ads_forced")) || PreferenceManager.getDefaultSharedPreferences(context).getBoolean("ads_enabled_new", true) val downloaderType: Int get() = Integer.parseInt( PreferenceManager.getDefaultSharedPreferences(context) .getString("downloader_type", null) ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isMIUI) "0" else "1" ) var autoBackupTime: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("auto_backup", "0") ?: "0" set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("auto_backup", value).apply() val showFavIndicator: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean("show_fav_count", true) var spProtectionEnabled: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean("security_blocking_firestore", true) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit() .putBoolean("security_blocking_firestore", value).apply() var tvRecentsChannelCreated: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean("tv_channel_recents_created", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit() .putBoolean("tv_channel_recents_created", value).apply() var tvRecentsPreFilled: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean("tv_channel_recents_prefilled", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit() .putBoolean("tv_channel_recents_prefilled", value).apply() var tvRecentsChannelId: Long get() = PreferenceManager.getDefaultSharedPreferences(context) .getLong("tv_channel_recents_id", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit() .putLong("tv_channel_recents_id", value).apply() var tvRecentsChannelLastEid: String? get() = PreferenceManager.getDefaultSharedPreferences(context) .getString("tv_channel_recents_last_eid", null) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit() .putString("tv_channel_recents_last_eid", value).apply() var tvRecentsChannelIds: Set? get() = PreferenceManager.getDefaultSharedPreferences(context) .getStringSet("tv_channel_recents_ids", null) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit() .putStringSet("tv_channel_recents_ids", value).apply() var spErrorType: String? get() = PreferenceManager.getDefaultSharedPreferences(context) .getString("sp_error_type", null) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit() .putString("sp_error_type", value).apply() private val useExperimentalPlayer: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean("experimental_player", false) val collapseDirectoryNotification: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean("collapse_dir_nots", true) val showRecentImage: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("recent_image", true) val useSmoothAnimations: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("smooth_animations", true) var emissionBlacklist: MutableSet get() = PreferenceManager.getDefaultSharedPreferences(context).getStringSet("emision_blacklist", LinkedHashSet()) ?: LinkedHashSet() set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putStringSet("emision_blacklist", value).apply() var emissionShowHidden: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("show_hidden", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("show_hidden", value).apply() var isAchievementsOmitted: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("achievements_omited", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("achievements_omited", value).apply() var isSecurityUpdated: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("securityUpdated", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("securityUpdated", value).apply() var lastStart: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("last_start", System.currentTimeMillis()) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("last_start", value).apply() var firstStart: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("first_start_new", 0) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("first_start_new", value).apply() val saveWithName: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getString("save_type", "0") == "0" var emissionShowFavs: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("show_favs", true) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("show_favs", value).apply() var timeoutTime: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getString("timeout_time", if (context.resources.getBoolean(R.bool.isTv)) "0" else "10")?.toLong() ?: 0 set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("timeout_time", value.toString()).apply() var rememberServer: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("remember_server", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("remember_server", value).apply() var lastServer: String? get() = PreferenceManager.getDefaultSharedPreferences(context).getString("last_server", null) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("last_server", value).apply() var lastBackup: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("last_backup", "Desconocido") ?: "Desconocido" set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("last_backup", value).apply() val notifyFavs: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("notify_favs", false) val isProxyCastEnabled: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("force_local_cast", false) val isGroupingEnabled: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("group_notifications", true) && canGroupNotifications var storageType: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("storage_type", "Sin almacenamiento") ?: "Sin almacenamiento" set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("storage_type", value).apply() var downloadType: String get() = defaultDownloadType set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("download_type", value).apply() val maxParallelDownloads: Int get() = Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString("max_parallel_downloads", "3") ?: "3") var userAgent: String get() = if (alwaysGenerateUA && mayUseRandomUA) randomUA() else PreferenceManager.getDefaultSharedPreferences(context).getString("user_agent", randomUA()) ?: randomUA() set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("user_agent", value).apply() val mayUseRandomUA: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("may_use_random_useragent_1", false) var alwaysGenerateUA: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("alwaysGenerateUA", true) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("alwaysGenerateUA", value).apply() var userAgentDir: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("user_agent_dir", randomUA()) ?: randomUA() set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("user_agent_dir", value).apply() var randomLimit: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getInt("random_limit", 25) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putInt("random_limit", value).apply() val useHome: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getString("recents_design", "0") == "1" var useDefaultUserAgent: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("use_device_useragent", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("use_device_useragent", value).apply() val usePlaceholders: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("list_placeholder", false) var instanceUuid: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("instance_uuid", null) ?: UUID.randomUUID().toString().also { instanceUuid = it } set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("instance_uuid", value).apply() var instanceName: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("instance_name", "Anónimo") ?: "Anónimo" set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("instance_name", value).apply() var recentLastHiddenNew: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getInt("recent_last_hidden_new", 0) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putInt("recent_last_hidden_new", value).apply() private val rewardedVideoCount: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getInt("rewarded_videos_seen", 0) var userRewardedVideoCount: Int get() = SecurePreferences(context).getInt("user_rewarded_videos_seen", rewardedVideoCount) set(value) = SecurePreferences(context).edit().putInt("user_rewarded_videos_seen", value).apply() private val coins: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getString("coinsNum", null)?.decrypt()?.toInt() ?: 0 var userCoins: Int get() = noCrashLet(0) { SecurePreferences(context).getInt("userCoins", try { coins } catch (e: Exception) { 0 }) } set(value) = SecurePreferences(context).edit().putInt("userCoins", value).apply() var lsAchievements: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_achievements", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_achievements", value).apply() var lsEa: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_ea", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_ea", value).apply() var lsFavs: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_favs", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_favs", value).apply() var lsGenres: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_genres", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_genres", value).apply() var lsHistory: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_history", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_history", value).apply() var lsQueue: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_queue", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_queue", value).apply() var lsSeeing: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_seeing", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_seeing", value).apply() var lsSeen: Long get() = PreferenceManager.getDefaultSharedPreferences(context).getLong("ls_seen", -1) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putLong("ls_seen", value).apply() var isFamilyFriendly: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("family_friendly_enabled", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("family_friendly_enabled", value).apply() var ffPass: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("ff_pass_cbc", "") ?: "" set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("ff_pass_cbc", value).apply() var topCount: Int get() = PreferenceManager.getDefaultSharedPreferences(context).getInt("top_count", 25) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putInt("top_count", value).apply() var subscriptionToken: String? get() = SecurePreferences(context).getString("subscription_token", null) set(value) = SecurePreferences(context).edit().putString("subscription_token", value).apply() var isPSWarned: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("isPSWarned1", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("isPSWarned1", value).apply() var isNativeAdsEnabled: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("isNativeAdsEnabled", true) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("isNativeAdsEnabled", value).apply() var isFullAdsEnabled: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("isFullAdsEnabled", true) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("isFullAdsEnabled", value).apply() var fullAdsProbability: Float get() = PreferenceManager.getDefaultSharedPreferences(context).getFloat("fullAdsProbability", AdsUtils.remoteConfigs.getDouble("full_show_probability").toFloat()) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putFloat("fullAdsProbability", value).apply() var fullAdsExtraProbability: Float get() = PreferenceManager.getDefaultSharedPreferences(context).getFloat("fullAdsExtraProbability", AdsUtils.remoteConfigs.getDouble("full_show_extra_probability").toFloat()) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putFloat("fullAdsExtraProbability", value).apply() val designStyle: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("designStyleType", "0") ?: "0" val recentActionType: String get() = PreferenceManager.getDefaultSharedPreferences(context).getString("recentActionType", "0") ?: "0" var dirCookies: List get() = ConvertUtil.String2List(PreferenceManager.getDefaultSharedPreferences(context).getString("dirCookies", "") ?: "") set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putString("dirCookies", ConvertUtil.listToString(value)).apply() var isForbiddenTipShown: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("isForbiddenTipShown", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("isForbiddenTipShown", value).apply() var isBypassWarningShown: Boolean get() = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("isBypassWarningShown", false) set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("isBypassWarningShown", value).apply() fun showProgress(): Boolean { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("show_progress", true) } fun showFavSections(): Boolean { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("fav_sections", true) } fun showImport(): Boolean { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("show_import", false) } fun bufferSize(): Int { return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString("buffer_size", "32") ?: "32") } fun getLiveEmissionBlackList(): LiveData> { return PreferenceManager.getDefaultSharedPreferences(context).stringSetLiveData("emision_blacklist", LinkedHashSet()) } fun getPlayerIntent(): Intent { return if (useExperimentalPlayer) Intent(context, VideoActivity::class.java) else Intent(context, CustomExoPlayer::class.java) } fun getLiveShowFavIndicator(): LiveData { return PreferenceManager.getDefaultSharedPreferences(context).booleanLiveData("show_fav_count", true) } fun getLiveDesignType(): LiveData { return PreferenceManager.getDefaultSharedPreferences(context).stringLiveData("designStyleType", "0").distinct } fun getLiveEmissionVisibility(): LiveData = PreferenceManager.getDefaultSharedPreferences(context).booleanLiveData("show_hidden", false) val isSubscriptionEnabled: Boolean get() = subscriptionToken != null private val defaultDownloadType: String get() { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) "1" else PreferenceManager.getDefaultSharedPreferences(context).getString("download_type", "0") ?: "0" } } ================================================ FILE: app/src/main/java/knf/kuma/commons/SSLSkipper.kt ================================================ package knf.kuma.commons import java.security.GeneralSecurityException import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.HostnameVerifier import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager object SSLSkipper { fun skip() { val trustAllCertificates = arrayOf(object : X509TrustManager { override fun getAcceptedIssuers(): Array? { return null // Not relevant. } override fun checkClientTrusted(certs: Array, authType: String) { noCrash { certs.forEach { it.checkValidity() } } } override fun checkServerTrusted(certs: Array, authType: String) { noCrash { certs.forEach { it.checkValidity() } } } }) val trustAllHostnames = HostnameVerifier { hostName, _ -> /*isHostValid(hostName)*/ true } try { System.setProperty("jsse.enableSNIExtension", "false") val sc = SSLContext.getInstance("SSL") sc.init(null, trustAllCertificates, SecureRandom()) HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory) HttpsURLConnection.setDefaultHostnameVerifier(trustAllHostnames) } catch (e: GeneralSecurityException) { throw ExceptionInInitializerError(e) } try { val context = SSLContext.getInstance("TLS") context.init(null, arrayOf(object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array, authType: String) { noCrash { chain.forEach { it.checkValidity() } } } @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array, authType: String) { noCrash { chain.forEach { it.checkValidity() } } } override fun getAcceptedIssuers(): Array { return arrayOfNulls(0) } }), SecureRandom()) HttpsURLConnection.setDefaultSSLSocketFactory(context.socketFactory) } catch (e: Exception) { } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/SelfServer.kt ================================================ package knf.kuma.commons import android.app.Notification import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.Uri import android.os.Build import android.os.IBinder import android.webkit.URLUtil import androidx.core.app.NotificationCompat import fi.iki.elonen.NanoHTTPD import knf.kuma.App import knf.kuma.R import knf.kuma.download.DownloadManager import knf.kuma.download.foreground import knf.kuma.download.service import okhttp3.OkHttpClient import okhttp3.Request import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster import java.io.File import java.io.FileInputStream import java.io.IOException import java.io.InputStream import java.io.PipedInputStream import java.io.PipedOutputStream import java.net.HttpURLConnection import java.net.URL import androidx.core.net.toUri class SelfServer : Service() { private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { CastUtil.get().stop() stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { foreground(64587, foregroundNotification(), false) return START_STICKY } override fun onCreate() { super.onCreate() foreground(64587, foregroundNotification(), false) val filter = IntentFilter("knf.cast.stop.foreground") if (Build.VERSION.SDK_INT >= 33) { registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED) } else { registerReceiver(receiver, filter) } } override fun onDestroy() { super.onDestroy() try { unregisterReceiver(receiver) } catch (e: Exception) { e.printStackTrace() } } override fun onBind(intent: Intent?): IBinder? { return null } private fun foregroundNotification(): Notification { return NotificationCompat.Builder(App.context, DownloadManager.CHANNEL_FOREGROUND).apply { setSmallIcon(R.drawable.ic_server_running) setOngoing(true) priority = NotificationCompat.PRIORITY_MIN setGroup("manager") if (PrefsUtil.collapseDirectoryNotification) setSubText("Servidor activo") else setContentTitle("Servidor activo") addAction(R.drawable.ic_stop, "Detener", PendingIntent.getBroadcast( App.context, 4689, Intent("knf.cast.stop.foreground"), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) }.build() } companion object { var HTTP_PORT = 6991 private var INSTANCE: Server? = null fun start(data: String, isFile: Boolean = true): String? { return try { stop(true) App.context.service(Intent(App.context, SelfServer::class.java)) INSTANCE = Server(data, isFile) "http://" + Network.ipAddress + ":" + HTTP_PORT } catch (e: Exception) { e.printStackTrace() Toaster.toast("Error al iniciar server") null } } fun stop(isRestart: Boolean = false) { if (INSTANCE?.isAlive == true) { INSTANCE?.stop() if (!isRestart) App.context.sendBroadcast(Intent("knf.cast.stop.foreground").apply { setPackage("knf.kuma") }) } } } private class Server @Throws(Exception::class) constructor(private val data: String, private val isFile: Boolean) : NanoHTTPD(HTTP_PORT) { init { start(SOCKET_READ_TIMEOUT, false) } override fun serve(session: IHTTPSession): Response { return if (isFile) if (URLUtil.isFileUrl(data)) { var file = File(data.toUri().path) if (!file.exists()) file = file.parentFile?.listFiles { f -> f.name.contains(data.toUri().path!!.substringAfterLast("$")) }!![0] serveFile(session.headers, file) } else serveFile(session.headers, data) else serveWeb(session.headers, data) } private fun getSize(url: String): Long { return try { val connection = URL(url).openConnection() as HttpURLConnection connection.connect() connection.contentLength.toLong() } catch (e: Exception) { 0 } } private fun serveWeb(header: Map, url: String): Response { var res: Response? = null val okHttpClient = OkHttpClient() val request = Request.Builder().url(url) val response = okHttpClient.newCall(request.build()).execute() val body = response.body val total = body?.contentLength() ?: 0 val inputStream = body?.byteStream() val pipedIn = PipedInputStream() val pipedOut = PipedOutputStream(pipedIn) if (inputStream != null) { doAsync { noCrash { val b = ByteArray(16 * 1024) var len = inputStream.read(b, 0, 16 * 1024) while (len != -1) { pipedOut.write(b, 0, len) len = inputStream.read(b, 0, 16 * 1024) } pipedOut.flush() response.close() } } Thread.sleep(400) res = createResponse(Response.Status.OK, "video/mp4", pipedIn, total) } return res ?: getResponse("Error 404: File not found") } private fun serveFile(header: Map, file_name: String): Response { var res: Response? val mime = "video/mp4" val fileWrapper = FileWrapper.create(file_name) try { if (!fileWrapper.exist) throw IllegalAccessException() // Calculate etag val etag = Integer.toHexString((fileWrapper.file()?.absolutePath + fileWrapper.lastModified() + "" + fileWrapper.length()).hashCode()) // Support (simple) skipping: var startFrom: Long = 0 var endAt: Long = -1 var range = header["range"] if (range != null) { if (range.startsWith("bytes=")) { range = range.substring("bytes=".length) val minus = range.indexOf('-') try { if (minus > 0) { startFrom = java.lang.Long.parseLong(range.substring(0, minus)) endAt = java.lang.Long.parseLong(range.substring(minus + 1)) } } catch (ignored: NumberFormatException) { } } } // Change return code and add Content-Range header when skipping is requested val fileLen = fileWrapper.length() ?: 0 if (range != null && startFrom >= 0) { if (startFrom >= fileLen) { res = createResponse(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "") res.addHeader("Content-Range", "bytes 0-0/$fileLen") res.addHeader("ETag", etag) } else { if (endAt < 0) { endAt = fileLen - 1 } var newLen = endAt - startFrom + 1 if (newLen < 0) { newLen = 0 } val dataLen = newLen val fis = fileWrapper.inputStream() fis?.skip(startFrom) res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis, dataLen) res.addHeader("Content-Length", "" + dataLen) res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen) res.addHeader("ETag", etag) } } else { if (etag == header["if-none-match"]) res = createResponse(Response.Status.NOT_MODIFIED, mime, "") else { res = createResponse(Response.Status.OK, mime, fileWrapper.inputStream(), fileLen) res.addHeader("Content-Length", "" + fileLen) res.addHeader("ETag", etag) } } } catch (ioe: IOException) { res = getResponse("Forbidden: Reading file failed") } return res ?: getResponse("Error 404: File not found") } private fun serveFile(header: Map, file: File): Response { var res: Response? val mime = "video/mp4" try { // Calculate etag val etag = Integer.toHexString((file.absolutePath + file.lastModified() + "" + file.length()).hashCode()) // Support (simple) skipping: var startFrom: Long = 0 var endAt: Long = -1 var range = header["range"] if (range != null) { if (range.startsWith("bytes=")) { range = range.substring("bytes=".length) val minus = range.indexOf('-') try { if (minus > 0) { startFrom = java.lang.Long.parseLong(range.substring(0, minus)) endAt = java.lang.Long.parseLong(range.substring(minus + 1)) } } catch (ignored: NumberFormatException) { } } } // Change return code and add Content-Range header when skipping is requested val fileLen = file.length() if (range != null && startFrom >= 0) { if (startFrom >= fileLen) { res = createResponse(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "") res.addHeader("Content-Range", "bytes 0-0/$fileLen") res.addHeader("ETag", etag) } else { if (endAt < 0) { endAt = fileLen - 1 } var newLen = endAt - startFrom + 1 if (newLen < 0) { newLen = 0 } val dataLen = newLen val fis = FileInputStream(file) fis.skip(startFrom) res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis, dataLen) res.addHeader("Content-Length", "" + dataLen) res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen) res.addHeader("ETag", etag) } } else { if (etag == header["if-none-match"]) res = createResponse(Response.Status.NOT_MODIFIED, mime, "") else { res = createResponse(Response.Status.OK, mime, FileInputStream(file), fileLen) res.addHeader("Content-Length", "" + fileLen) res.addHeader("ETag", etag) } } } catch (ioe: IOException) { res = getResponse("Forbidden: Reading file failed") } return res ?: getResponse("Error 404: File not found") } // Announce that the file server accepts partial content requests private fun createResponse(status: Response.Status, mimeType: String, message: InputStream?, lenght: Long): Response { val res = newFixedLengthResponse(status, mimeType, message, lenght) res.addHeader("Accept-Ranges", "bytes") return res } // Announce that the file server accepts partial content requests private fun createResponse(status: Response.Status, mimeType: String, message: String): Response { val res = newFixedLengthResponse(status, mimeType, message) res.addHeader("Accept-Ranges", "bytes") return res } private fun getResponse(message: String): Response { return createResponse(Response.Status.OK, "text/plain", message) } } } ================================================ FILE: app/src/main/java/knf/kuma/commons/SharedPreferenceLiveData.kt ================================================ package knf.kuma.commons import android.content.SharedPreferences import androidx.lifecycle.LiveData abstract class SharedPreferenceLiveData(val sharedPrefs: SharedPreferences, val key: String, private val defValue: T) : LiveData() { private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key == this.key) { value = getValueFromPreferences(key, defValue) } } abstract fun getValueFromPreferences(key: String, defValue: T): T override fun onActive() { super.onActive() value = getValueFromPreferences(key, defValue) sharedPrefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener) } override fun onInactive() { sharedPrefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) super.onInactive() } } class SharedPreferenceIntLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Int) : SharedPreferenceLiveData(sharedPrefs, key, defValue) { override fun getValueFromPreferences(key: String, defValue: Int): Int = sharedPrefs.getInt(key, defValue) } class SharedPreferenceStringLiveData(sharedPrefs: SharedPreferences, key: String, defValue: String) : SharedPreferenceLiveData(sharedPrefs, key, defValue) { override fun getValueFromPreferences(key: String, defValue: String): String = sharedPrefs.getString(key, defValue) ?: defValue } class SharedPreferenceBooleanLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Boolean) : SharedPreferenceLiveData(sharedPrefs, key, defValue) { override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean = sharedPrefs.getBoolean(key, defValue) } class SharedPreferenceFloatLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Float) : SharedPreferenceLiveData(sharedPrefs, key, defValue) { override fun getValueFromPreferences(key: String, defValue: Float): Float = sharedPrefs.getFloat(key, defValue) } class SharedPreferenceLongLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Long) : SharedPreferenceLiveData(sharedPrefs, key, defValue) { override fun getValueFromPreferences(key: String, defValue: Long): Long = sharedPrefs.getLong(key, defValue) } class SharedPreferenceStringSetLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Set) : SharedPreferenceLiveData>(sharedPrefs, key, defValue) { override fun getValueFromPreferences(key: String, defValue: Set): Set = sharedPrefs.getStringSet(key, defValue) ?: defValue } fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData { return SharedPreferenceIntLiveData(this, key, defValue) } fun SharedPreferences.stringLiveData(key: String, defValue: String): SharedPreferenceLiveData { return SharedPreferenceStringLiveData(this, key, defValue) } fun SharedPreferences.booleanLiveData(key: String, defValue: Boolean): SharedPreferenceLiveData { return SharedPreferenceBooleanLiveData(this, key, defValue) } fun SharedPreferences.floatLiveData(key: String, defValue: Float): SharedPreferenceLiveData { return SharedPreferenceFloatLiveData(this, key, defValue) } fun SharedPreferences.longLiveData(key: String, defValue: Long): SharedPreferenceLiveData { return SharedPreferenceLongLiveData(this, key, defValue) } fun SharedPreferences.stringSetLiveData(key: String, defValue: Set): SharedPreferenceLiveData> { return SharedPreferenceStringSetLiveData(this, key, defValue) } ================================================ FILE: app/src/main/java/knf/kuma/commons/ThumbsDownloader.kt ================================================ package knf.kuma.commons import android.content.Context import android.graphics.Bitmap import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File import java.io.FileOutputStream object ThumbsDownloader { fun start(context: Context) { GlobalScope.launch(Dispatchers.IO) { val thumbs = context.getExternalFilesDir("thumbs") for (id in 1..3500) { val result = try { val thumb = File(thumbs, "$id.jpg") if (!thumb.exists()) { val bitmap = PicassoSingle.get() .load("https://www3.animeflv.net/uploads/animes/thumbs/$id.jpg").get() thumb.createNewFile() bitmap.compress(Bitmap.CompressFormat.JPEG, 100, FileOutputStream(thumb)) delay(100) } true } catch (e: Exception) { false } Log.e("Thumb", "Download id $id, success: $result") } } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/AchievementUnlocked.java ================================================ package knf.kuma.custom; import static android.text.TextUtils.isEmpty; import static android.view.Gravity.CENTER_HORIZONTAL; import static android.view.View.GONE; import static android.widget.LinearLayout.VERTICAL; import static java.lang.Boolean.FALSE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.os.Build.VERSION; import android.os.PowerManager; import android.provider.Settings; import android.text.Editable; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.AnticipateInterpolator; import android.view.animation.LinearInterpolator; import android.view.animation.OvershootInterpolator; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.RelativeLayout.LayoutParams; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; import java.util.Collection; import knf.kuma.App; import knf.kuma.BuildConfig; /** * Basically an animated toast notification with queue support. *

* It uses a set of invisible views (called 'fake') to measure * the data before showing it to user. This similar to using measureText * method but more accurate. *

* Doesn't work with power-saving mode on unless you implement your * own valueAnimator class. *

* This is 'all-in-one' library. You have to copy the class file * to your package folder, otherwise you won't have access to inner * classes such as AchievementData and listener. *

* Don't forget to grant 'draw over apps' permission (SYSTEM_ALERT_WINDOW) *

* GPL * By Darkion Avey @ http://darkion.net/ */ @SuppressWarnings({"unused", "SetTextI18n"}) public class AchievementUnlocked { //animation interpolators private final static TimeInterpolator TIME_INTERPOLATOR = new DeceleratingInterpolator(50); private static final String TAG = "AU"; private final OvershootInterpolator overshootInterpolator = new OvershootInterpolator(); private final AnticipateInterpolator anticipateInterpolator = new AnticipateInterpolator(); private final TimeInterpolator accelerateInterpolator = new AccelerateInterpolator(50); private final int focusable = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; private final int nonFocusable = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; private final boolean DEBUG = BuildConfig.DEBUG; private int currentContainerWidth; //dimens private int smallSize, largeSize, elevation, paddingLarge, paddingSmall, translationY, margin; private int initialSize = -1; //indices of data iterator private int index = 0; private final Context context; private boolean dismissible = false; private boolean added = false; //achievements data private AchievementData[] achievements; private int readingDelay = 1300; private int matchParent; private boolean dismissed = false; private AchievementListenerAdapter listener; private boolean isPowerSavingModeOn = false; private boolean isLarge = true, alignTop = true, isRounded = true; private boolean notchMode = VERSION.SDK_INT >= 26; private Integer statusBarHeight; private ViewGroup container; private AchievementIconView icon; private TextView titleTextView; private ScrollTextView subtitleTextView; private ViewGroup achievementLayout; private WindowManager.LayoutParams mainViewLP; private float mPxPerSeconds = 40; //private AchievementQueue queue = new AchievementQueue(); private boolean initiatedGlobalFields = false; private boolean hasBeenDismissed = false; public AchievementUnlocked(Context context) { this.context = context; initGlobalFields(); } /** * For debugging purposes */ static long getScaledDuration(int duration) { return (long) (1f * duration); } private static int countMatches(final String str, final String sub) { if (isEmpty(str) || isEmpty(sub)) { return 0; } int count = 0; int idx = 0; while ((idx = str.indexOf(sub, idx)) != -1) { count++; idx += sub.length(); } return count; } /** * Set how many pixels should be scrolled per second when * the subtitle is scrollable (longer than screen width) *

* Default value is 40; * * @param PxPerSeconds higher values will result in faster * scrolling */ public void setScrollingPxPerSeconds(float PxPerSeconds) { this.mPxPerSeconds = PxPerSeconds; } /** * Indicate that the system is running on a notched device. * This is set to true on Oreo+ devices since TYPE_SYSTEM_ERROR is * deprecated anyway and the popup will have to move below the status bar * * @param statusBarHeight custom status bar height (y shift) since this library * does not have direct access to decor view. You can supply * a null value and use the hardcoded status bar height */ public void setNotchMode(@Nullable Integer statusBarHeight) { this.notchMode = true; if (statusBarHeight != null) this.statusBarHeight = statusBarHeight; } /** * Indicate whether the popup should appear on top of the screen * or not * * @param alignTop true for top alignment * @return same AchievementUnlocked object */ public AchievementUnlocked setTopAligned(boolean alignTop) { this.alignTop = alignTop; return this; } /** * Set how many milliseconds the popup should wait before the next * animation is played. This value is ignored when the popup width * exceeds display width (aka scrolling popup). * The default value is 1500 which is 1.5 seconds * * @param readingDelay reading duration in milliseconds * @return same AchievementUnlocked object */ public AchievementUnlocked setReadingDelay(int readingDelay) { this.readingDelay = readingDelay; return this; } /** * Set true if you want the popup to be rounded. Default * value is true * * @param rounded true for complete rounded appearance, false for rounded box * @return same AchievementUnlocked object */ public AchievementUnlocked setRounded(boolean rounded) { isRounded = rounded; return this; } /** * Callbacks for different events occurring throughout the popup's * life span * * @param listener the listener to be used */ public void setAchievementListener(@Nullable AchievementListenerAdapter listener) { this.listener = listener; } /** * Set to true if you want the popup to be large. Default value * is true. Large popup height is 65dp whereas the small one is * 50dp * * @param large true for large popups, false for small ones * @return same AchievementUnlocked object */ public AchievementUnlocked setLarge(boolean large) { this.isLarge = large; return this; } /** * @return the popup view without the scrim. You should not modify * any of its properties since that might cause the animations * to go haywire */ public View getAchievementView() { return container; } /** * @return the title text view */ public TextView getTitleTextView() { return titleTextView; } /** * @return the subtitle text view */ public TextView getSubtitleTextView() { return subtitleTextView; } /** * @return the icon view */ public View getIconView() { return icon; } /** * @return get the view containing the scrim (background fade) and * the popup. You should not modify any of its properties since that might * cause the animations to go haywire */ public ViewGroup getAchievementParent() { return achievementLayout; } private int convertDpToPixel(float dp) { DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); float px = dp * (metrics.densityDpi / 160f); return Math.round(px); } @SuppressLint({"ObsoleteSdkInt", "SetTextI18n"}) private void initGlobalFields() { try { if (!initiatedGlobalFields) { margin = convertDpToPixel(16); elevation = convertDpToPixel(10); paddingLarge = convertDpToPixel(10); paddingSmall = convertDpToPixel(5); smallSize = convertDpToPixel(50); largeSize = convertDpToPixel(65); translationY = convertDpToPixel(20); achievementLayout = new RelativeLayout(context); achievementLayout.setClipToPadding(FALSE); LayoutParams motherLayoutLP = new LayoutParams(-2, -2); achievementLayout.setLayoutParams(motherLayoutLP); achievementLayout.setTag("motherLayout"); LinearLayout textContainerFake = new LinearLayout(context); textContainerFake.setOrientation(VERTICAL); textContainerFake.setPadding(convertDpToPixel(10), 0, convertDpToPixel(20), 0); textContainerFake.setVisibility(View.INVISIBLE); LayoutParams textContainerFakeLP = new LayoutParams(-2, -2); textContainerFakeLP.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE); textContainerFake.setLayoutParams(textContainerFakeLP); textContainerFake.setTag("textContainerFake"); TextView titleFake = new TextView(context); titleFake.setText("Title"); LayoutParams titleFakeLP = new LayoutParams(-2, -2); titleFake.setLayoutParams(titleFakeLP); titleFake.setTag("titleFake"); titleFake.setMaxLines(1); ScrollTextView subtitleFake = new ScrollTextView(context); subtitleFake.setText("Subtitle"); subtitleFake.setVisibility(GONE); subtitleFake.setMaxLines(1); LayoutParams subtitleFakeLP = new LayoutParams(-2, -2); subtitleFake.setLayoutParams(subtitleFakeLP); subtitleFake.setTag("subtitleFake"); textContainerFake.addView(titleFake); textContainerFake.addView(subtitleFake); achievementLayout.addView(textContainerFake); container = new RelativeLayout(context); container.setClipToPadding(false); container.setClipChildren(false); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { achievementLayout.setClipToOutline(true); } LayoutParams achievementBodyLP = new LayoutParams(-2, largeSize); achievementBodyLP.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); achievementBodyLP.addRule(CENTER_HORIZONTAL, RelativeLayout.TRUE); achievementBodyLP.bottomMargin = achievementBodyLP.topMargin = convertDpToPixel(10); if ((VERSION.SDK_INT >= 26 || notchMode) && alignTop) { achievementBodyLP.topMargin += statusBarHeight == null ? Math.round(getStatusBarHeight() * 1.7f) : statusBarHeight; } container.setLayoutParams(achievementBodyLP); container.setTag("achievementBody"); LinearLayout achievementIconBg = new LinearLayout(context); LayoutParams achievementIconBgLP = new LayoutParams(largeSize, largeSize); achievementIconBg.setLayoutParams(achievementIconBgLP); achievementIconBg.setTag("achievementIconBg"); container.addView(achievementIconBg); icon = new AchievementIconView(context); icon.setPadding(convertDpToPixel(7), convertDpToPixel(7), convertDpToPixel(7), convertDpToPixel(7)); LayoutParams achievementIconLP = new LayoutParams(largeSize, largeSize); icon.setMaxWidth(largeSize); icon.setLayoutParams(achievementIconLP); icon.setTag("achievementIcon"); achievementIconBg.addView(icon); LinearLayout textContainer = new LinearLayout(context); textContainer.setClipToPadding(false); textContainer.setClipChildren(false); textContainer.setOrientation(VERTICAL); textContainer.setTag("textContainer"); LayoutParams textContainerLP = new LayoutParams(-2, -2); textContainer.setLayoutParams(textContainerLP); textContainerLP.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE); container.addView(textContainer); container.setTag("achievementBody"); titleTextView = new TextView(context); titleTextView.setText("Title"); titleTextView.setMaxLines(1); LayoutParams titleLP = new LayoutParams(-2, -2); titleTextView.setLayoutParams(titleLP); titleTextView.setTag("title"); subtitleTextView = new ScrollTextView(context); subtitleTextView.setText("Subtitle"); subtitleTextView.setVisibility(GONE); subtitleTextView.setLayoutParams(titleLP); subtitleTextView.setMaxLines(1); subtitleTextView.setTag("subtitle"); textContainer.addView(titleTextView); textContainer.addView(subtitleTextView); achievementLayout.addView(container); if (mainViewLP == null) { mainViewLP = new WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowOverlayCompat.TYPE_SYSTEM_ERROR, focusable, PixelFormat.TRANSLUCENT); } if (titleTextView != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isLarge) { titleTextView.setGravity(View.TEXT_ALIGNMENT_CENTER); } titleTextView.setSingleLine(true); titleTextView.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) { ((TextView) achievementLayout.findViewWithTag("titleFake")).setText(titleTextView.getText()); } }); } if (subtitleTextView != null) { subtitleTextView.setSingleLine(true); subtitleTextView.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) { ((TextView) achievementLayout.findViewWithTag("subtitleFake")).setText(subtitleTextView.getText()); } }); } initiatedGlobalFields = true; } } catch (Exception e) { e.printStackTrace(); } } /** * Set to true if you want the popup to be swipeable * * @param dismissible true for swipe to dismiss behaviour */ public void setDismissible(boolean dismissible) { this.dismissible = dismissible; if (dismissible) { achievementLayout.setOnTouchListener(new SwipeDismissTouchListener()); container.setOnTouchListener(new SwipeDismissTouchListener()); } else { achievementLayout.setOnTouchListener(null); container.setOnTouchListener(null); } } private int getTargetWidth(AchievementData data) { View textContainerFake = achievementLayout.findViewWithTag("textContainerFake"); ((TextView) textContainerFake.findViewWithTag("titleFake")).setText(data.getTitle()); ((TextView) textContainerFake.findViewWithTag("subtitleFake")).setText(data.getSubtitle()); textContainerFake.measure(MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); return textContainerFake.getMeasuredWidth(); } private void buildAchievement() { initGlobalFields(); int padding; if (isLarge) { initialSize = largeSize; padding = paddingLarge; } else { initialSize = smallSize; padding = paddingSmall; } ((View) icon.getParent()).invalidate(); icon.setPadding(padding, padding, padding, padding); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { container.setElevation(elevation); } titleTextView.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 (s == null || s.length() == 0) { titleTextView.setVisibility(GONE); } else { titleTextView.setVisibility(View.VISIBLE); } } }); final TextView fakeTitle = (achievementLayout.findViewWithTag("titleFake")); fakeTitle.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 (s == null || s.length() == 0) { fakeTitle.setVisibility(GONE); } else { fakeTitle.setVisibility(View.VISIBLE); } } }); final TextView fakeSubTitle = (achievementLayout.findViewWithTag("subtitleFake")); fakeSubTitle.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 (s == null || s.length() == 0) { fakeSubTitle.setVisibility(GONE); } else { fakeSubTitle.setVisibility(View.VISIBLE); } } }); subtitleTextView.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 (s == null || s.length() == 0) { subtitleTextView.setVisibility(GONE); } else subtitleTextView.setVisibility(View.VISIBLE); } }); titleTextView.setAlpha(0f); titleTextView.setTranslationY(translationY); subtitleTextView.setTranslationY(translationY); subtitleTextView.setAlpha(0f); container.setScaleY(0f); container.setScaleX(0f); container.setVisibility(GONE); DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); matchParent = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels) - margin; //stretched = 900; // textContainer.setVisibility(View.GONE); View textContainer = achievementLayout.findViewWithTag("textContainer"); if (textContainer != null) { textContainer.setPadding(convertDpToPixel(10) + (initialSize), 0, convertDpToPixel(20), 0); achievementLayout.findViewWithTag("textContainerFake").setPadding(textContainer.getPaddingLeft(), textContainer.getPaddingTop(), textContainer.getPaddingRight(), textContainer.getPaddingBottom()); } icon.setMaxWidth(initialSize); container.getLayoutParams().width = container.getLayoutParams().height = icon.getLayoutParams().height = icon.getLayoutParams().width = ((View) icon.getParent()).getLayoutParams().height = ((View) icon.getParent()).getLayoutParams().width = initialSize; container.requestLayout(); if (alignTop) { mainViewLP.gravity = Gravity.TOP; } else { mainViewLP.gravity = Gravity.BOTTOM; } // No scrim for Android P if (alignTop && VERSION.SDK_INT < 28 && (achievementLayout.getBackground() == null || !(achievementLayout.getBackground() instanceof GradientDrawable))) { GradientDrawable scrim = new GradientDrawable(); scrim.setShape(GradientDrawable.RECTANGLE); scrim.setColors(new int[]{0x40000000, 0}); scrim.setAlpha(0); achievementLayout.setBackground(scrim); achievementLayout.setClipToPadding(false); } else if (!alignTop) { achievementLayout.setBackground(null); } final WindowManager manager = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)); if (manager == null) throw new RuntimeException("No window manager found"); manager.addView(achievementLayout, mainViewLP); added = true; } private void setTextColor(int textColor) { subtitleTextView.setTextColor(Color.parseColor("#B2FFFFFF")); titleTextView.setTextColor(Color.rgb(Color.red(textColor), Color.green(textColor), Color.blue(textColor))); } /** * use listeners instead */ @Deprecated public AchievementUnlocked createViews() { buildAchievement(); return this; } public void show(Collection data) { show(data.toArray(new AchievementData[0])); } /** * Pop the popup with the supplied data * * @param data data to be shown */ public void show(AchievementData... data) { if (data == null || data.length == 0) { return; } //Check permission first if (VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(context)) { if (DEBUG) Toast.makeText(context, "'canDrawOverlays' permission is not granted", Toast.LENGTH_LONG).show(); Log.e(TAG, "'canDrawOverlays' permission is not granted"); return; } //Don't bother if powersaving is on if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); isPowerSavingModeOn = powerManager != null && powerManager.isPowerSaveMode(); if (isPowerSavingModeOn) { Log.w(TAG, "Power saving is on, AU was canceled"); return; } } if (added) { if (achievements != null) { achievements = concat(achievements, data); } else achievements = data; return; } dismissWithoutAnimation(); this.achievements = data; buildAchievement(); setContainerBg(achievements[0].getBackgroundColor()); if (listener != null) listener.onViewCreated(this, data); prepareMorphism(); } /** * Instantly remove the popup view from window manager */ public void dismissWithoutAnimation() { removeView(); if (listener != null) listener.onAchievementDismissed(this); } private void removeListeners(Animator animatorSet) { if (animatorSet == null) return; if (animatorSet instanceof AnimatorSet && !((AnimatorSet) animatorSet).getChildAnimations().isEmpty()) for (Animator animator : ((AnimatorSet) animatorSet).getChildAnimations()) { removeListeners(animator); } else { if (animatorSet instanceof ValueAnimator) { ((ValueAnimator) animatorSet).removeAllUpdateListeners(); } animatorSet.removeAllListeners(); animatorSet.end(); animatorSet.cancel(); } } private AchievementData[] concat(AchievementData[] a, AchievementData[] b) { int aLen = a.length; int bLen = b.length; AchievementData[] c = new AchievementData[aLen + bLen]; System.arraycopy(a, 0, c, 0, aLen); System.arraycopy(b, 0, c, aLen, bLen); return c; } @SuppressLint("ObsoleteSdkInt") private void setBackground(View v, Drawable d) { v.setBackground(d); } private void removeView() { if (!added) return; index = 0; setSwipeEffect(0); hasBeenDismissed = false; isPowerSavingModeOn = false; icon.setVisibility(View.VISIBLE); setBackground(((View) icon.getParent()), null); setBackground(container, null); setBackground(icon, null); isLarge = true; alignTop = true; isRounded = true; icon.setOnClickListener(null); container.setOnClickListener(null); achievementLayout.setOnClickListener(null); achievementLayout.setVisibility(View.VISIBLE); // container.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; container.setOnTouchListener(null); container.setVisibility(View.VISIBLE); container.setTranslationX(0f); container.setAlpha(1f); achievementLayout.setAlpha(1f); setDismissible(false); listener = null; ((View) icon.getParent()).setBackground(null); dismissed = false; final WindowManager manager = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)); try { if (manager != null) manager.removeView(achievementLayout); added = false; } catch (Exception e) { e.printStackTrace(); // *shrug emoji* //there's no way to check if view is already added to windowManager or not, probably the exception is nullPointerException where achievementLayout is null //best thing we could do is to check added boolean } } private int clamp(int val, int min, int max) { return Math.max(min, Math.min(max, val)); } private int getStartValue(int start) { return clamp(start, initialSize, matchParent); } private int getEndValue(int end) { return Math.min(end, matchParent); } private ValueAnimator getContainerStretchAnimation(int start, int end) { final ValueAnimator containerStretch = ValueAnimator.ofInt(getStartValue(start), getEndValue(end)); containerStretch.addUpdateListener(valueAnimator -> { if (!dismissed) { int val = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = container.getLayoutParams(); layoutParams.width = val; currentContainerWidth = val; container.setLayoutParams(layoutParams); } }); containerStretch.setInterpolator(TIME_INTERPOLATOR); containerStretch.setDuration(getScaledDuration(300)); return containerStretch; } private GradientDrawableWithColors getContainerBg() { if ((container.getBackground()) instanceof GradientDrawableWithColors) return (GradientDrawableWithColors) (container.getBackground()); GradientDrawableWithColors iconBackground = new GradientDrawableWithColors(); if (isRounded) iconBackground.setCornerRadius(initialSize / 2f); else iconBackground.setCornerRadius(convertDpToPixel(2)); return iconBackground; } private void setContainerBg(int color) { Drawable bgDrawable = container.getBackground(); if (bgDrawable instanceof GradientDrawable) ((GradientDrawableWithColors) bgDrawable).setColor(color); else { GradientDrawableWithColors iconBackground = getContainerBg(); iconBackground.setColor(color); setBackground(container, iconBackground); } } private int getIconBgColor(int defaultColor) { Drawable bgDrawable = ((View) icon.getParent()).getBackground(); if (bgDrawable instanceof GradientDrawable) return ((GradientDrawableWithColors) bgDrawable).getGradientColor(); return defaultColor; } private int getContainerBgColor(int defaultColor) { Drawable bgDrawable = container.getBackground(); if (bgDrawable instanceof GradientDrawable) return ((GradientDrawableWithColors) bgDrawable).getGradientColor(); return defaultColor; } private GradientDrawableWithColors getIconBg() { if ((((View) icon.getParent()).getBackground()) instanceof GradientDrawable) return (GradientDrawableWithColors) (((View) icon.getParent()).getBackground()); GradientDrawableWithColors iconBackground = new GradientDrawableWithColors(); if (isRounded) iconBackground.setShape(GradientDrawable.OVAL); else iconBackground.setCornerRadius(convertDpToPixel(2)); return iconBackground; } private void setIconBg(int color) { Drawable bgDrawable = (((View) icon.getParent()).getBackground()); if (bgDrawable instanceof GradientDrawable) bgDrawable.setColorFilter(Color.argb(bgDrawable.getAlpha(), Color.red(color), Color.green(color), Color.blue(color)), PorterDuff.Mode.SRC_IN); else { GradientDrawableWithColors iconBackground = getIconBg(); iconBackground.setColor(color); setBackground(((View) icon.getParent()), iconBackground); } } private AnimatorSet getExitAnimation() { final ObjectAnimator containerScale = ObjectAnimator.ofFloat(container, View.SCALE_X, 1f, 0f); containerScale.addUpdateListener(animation -> { if (!dismissed) container.setScaleY((float) animation.getAnimatedValue()); }); containerScale.setDuration(getScaledDuration(250)); containerScale.setStartDelay(100); containerScale.setInterpolator(anticipateInterpolator); boolean scrimIsAvailable = alignTop && achievementLayout.getBackground() != null; ObjectAnimator scrim = null; if (scrimIsAvailable) { scrim = ObjectAnimator.ofInt(achievementLayout.getBackground(), "alpha", 255, 0); } AnimatorSet out = new AnimatorSet(); if (scrim != null) out.playTogether(containerScale, scrim); else out.play(containerScale); AnimatorSet set = new AnimatorSet(); set.playSequentially(getContainerStretchAnimation(Math.min(container.getMeasuredWidth(), matchParent), initialSize), out); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { try { super.onAnimationEnd(animation); dismissWithoutAnimation(); } catch (Exception e) { // } } }); return set; } private int getContainerBackgroundColor() { if ((container).getBackground() != null) if ((container).getBackground() instanceof GradientDrawableWithColors) return ((GradientDrawableWithColors) (container).getBackground()).getGradientColor(); return 0xffffffff; } private int getSubtitleLines(String subtitleRaw) { if (subtitleRaw.contains("\n")) return countMatches(subtitleRaw, "\n") + 1; return 1; } private boolean isBlank(final String cs) { int strLen; if (cs == null || (strLen = cs.length()) == 0) { return true; } for (int i = 0; i < strLen; i++) { if (!Character.isWhitespace(cs.charAt(i))) { return false; } } return true; } private boolean allClear(AnimatorSet[] sets) { for (AnimatorSet set : sets) { if (set == null) return false; } return true; } private AnimatorSet morphData() { AnimatorSet sets = new AnimatorSet(); AchievementData data = achievements[index]; sets.play(animateData(achievements[index])); sets.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { try { super.onAnimationEnd(animation); if (AchievementUnlocked.this.achievements != null && !hasBeenDismissed && AchievementUnlocked.this.achievements.length > 0 && index + 1 < AchievementUnlocked.this.achievements.length) { index++; morphData().start(); } else getExitAnimation().start(); } catch (Exception e) { // } } }); return sets; } private AnimatorSet animateData(final AchievementData data) { final AnimatorSet backgroundAnimators = new AnimatorSet(); final AnimatorSet inAnimation = new AnimatorSet(); final AnimatorSet outAnimation = new AnimatorSet(); final AnimatorSet result = new AnimatorSet(); ObjectAnimator titleIn, subtitleIn = null, titleOut, subtitleOut = null; if ((container.getTag() != null && container.getTag() != data)) { int previousBgColor = 0xffffffff; int previousIconBgColor = 0x30ffffff; if (index == 0) { previousBgColor = data.getBackgroundColor(); previousIconBgColor = data.getIconBackgroundColor(); } else if (index > 0 && index < achievements.length) { previousBgColor = achievements[index - 1].getBackgroundColor(); previousIconBgColor = achievements[index - 1].getIconBackgroundColor(); } ValueAnimator iconBgColor = ValueAnimator.ofInt(getIconBgColor(previousIconBgColor), data.getIconBackgroundColor()); iconBgColor.setEvaluator(new ArgbEvaluator()); iconBgColor.addUpdateListener(animation -> { if (!dismissed) setIconBg((int) animation.getAnimatedValue()); }); ValueAnimator bgColor = ValueAnimator.ofInt(getContainerBgColor(previousBgColor), data.getBackgroundColor()); bgColor.setEvaluator(new ArgbEvaluator()); bgColor.addUpdateListener(animation -> { if (!dismissed) setContainerBg((int) animation.getAnimatedValue()); }); bgColor.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); if (index > 0) setIcon(data); } }); backgroundAnimators.play(iconBgColor).with(bgColor); backgroundAnimators.setInterpolator(TIME_INTERPOLATOR); backgroundAnimators.setDuration(getScaledDuration(300)); } titleIn = ObjectAnimator.ofFloat(titleTextView, View.TRANSLATION_Y, translationY, 0); titleIn.addUpdateListener(animation -> { if (dismissed) return; titleTextView.setAlpha(animation.getAnimatedFraction()); }); titleIn.setDuration(getScaledDuration(300)); titleIn.setInterpolator(TIME_INTERPOLATOR); titleOut = ObjectAnimator.ofFloat(titleTextView, View.TRANSLATION_Y, 0, translationY); titleOut.addUpdateListener(animation -> { if (dismissed) return; titleTextView.setAlpha(1f - animation.getAnimatedFraction()); }); titleOut.setInterpolator(accelerateInterpolator); final boolean dataHasSubtitle = dataHasSubtitle(data); //indicates that scrolling is needed final boolean overFlow = (matchParent) < getTargetWidth(data); final int startScrollingDelay = dataHasSubtitle ? 800 : 0; final int scrollDistance = overFlow ? Math.abs(getTargetWidth(data) - matchParent) : 0; //if the text is scrolling, pause for a while at the end before collapsing final int endReadingDelay = overFlow ? 400 : 0; final int duration; if (overFlow) { final float density = context.getResources().getDisplayMetrics().density; float dpPerSec = mPxPerSeconds * density; //if the scroll distance is short, then use standard readingDelay value since the animation //will run too quickly and user won't be abel to read the contents duration = scrollDistance <= matchParent / 4 ? readingDelay : Math.round(scrollDistance * 1000 / dpPerSec); } else { duration = readingDelay; } ValueAnimator stretch = getContainerStretchAnimation(container.getMeasuredWidth(), getTargetWidth(data)); if (dataHasSubtitle) { subtitleIn = ObjectAnimator.ofFloat(subtitleTextView, View.TRANSLATION_Y, translationY, 0); subtitleIn.addUpdateListener(animation -> { if (dismissed) return; subtitleTextView.setAlpha(animation.getAnimatedFraction()); }); subtitleIn.setInterpolator(TIME_INTERPOLATOR); subtitleIn.setStartDelay(getScaledDuration(150)); subtitleIn.setInterpolator(TIME_INTERPOLATOR); subtitleIn.setDuration(getScaledDuration(300)); } //use previousWidth better than real-time measuring to increase performance if (dataHasSubtitle) { AnimatorSet textViews = new AnimatorSet(); //this null check is useful when seperating subtitle different lines //into different poups if (titleIn != null) textViews.playTogether(titleIn, subtitleIn); else textViews.playTogether(subtitleIn); inAnimation.play(stretch).with(backgroundAnimators).before(textViews); } else { if (titleIn != null) inAnimation.play(stretch).with(backgroundAnimators).before(titleIn); else inAnimation.playTogether(backgroundAnimators, stretch); } // inAnimation.setInterpolator(interpolator); if (dataHasSubtitle) { subtitleOut = ObjectAnimator.ofFloat(subtitleTextView, View.TRANSLATION_Y, 0, translationY); subtitleOut.addUpdateListener(animation -> { if (dismissed) return; subtitleTextView.setAlpha(1f - animation.getAnimatedFraction()); }); subtitleOut.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); subtitleTextView.stopScrolling(); } }); subtitleOut.setInterpolator(accelerateInterpolator); } if (dataHasSubtitle) { if (titleOut != null) { titleOut.setStartDelay(getScaledDuration(150)); outAnimation.playTogether(subtitleOut, titleOut); } else outAnimation.play(subtitleOut); } else { if (titleOut != null) outAnimation.play(titleOut); } final String title = data.getTitle(), subtitle = data.getSubtitle(); result.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); if (listener != null) listener.onAchievementMorphed(AchievementUnlocked.this, data); if (data.getPopUpOnClickListener() != null || dismissible) { mainViewLP.flags = focusable; } else { mainViewLP.flags = nonFocusable; } container.setOnClickListener(data.getPopUpOnClickListener()); final WindowManager manager = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)); if (manager != null && added) manager.updateViewLayout(achievementLayout, mainViewLP); subtitleTextView.setText(subtitle); subtitleTextView.updateScroller(scrollDistance); titleTextView.setText(title); setTextColor(data.getTextColor()); } }); ScrollTextView fake = (achievementLayout.findViewWithTag("subtitleFake")); fake.setText(data.getSubtitle()); subtitleTextView.setDurations(getScaledDuration(duration + endReadingDelay), getScaledDuration(startScrollingDelay)); outAnimation.setStartDelay(getScaledDuration(duration + endReadingDelay + startScrollingDelay)); outAnimation.setDuration(getScaledDuration(300)); result.playSequentially(inAnimation, outAnimation); result.setInterpolator(TIME_INTERPOLATOR); container.setTag(data); return result; } private void prepareMorphism() { if (achievements == null || achievements.length == 0) return; index = 0; AnimatorSet scene = new AnimatorSet(); scene.playSequentially(getEntranceAnimation(achievements[0]), morphData()); scene.start(); } private boolean dataHasSubtitle(AchievementData data) { return data.getSubtitle() != null && data.getSubtitle().length() > 0 && !data.getSubtitle().isEmpty(); } private AnimatorSet getEntranceAnimation(final AchievementData data) { final int iconBG = data.getIconBackgroundColor(); // final Drawable iconDrawable = data.getIcon(); // final int bg = data.getBackgroundColor(); //ValueAnimator stretch = getContainerStretchAnimation(initialSize, getTargetWidth(data)); ObjectAnimator containerScale = ObjectAnimator.ofFloat(container, View.SCALE_X, 0f, 1f); containerScale.addUpdateListener(animation -> { if (dismissed) return; container.setScaleY((float) animation.getAnimatedValue()); }); containerScale.setDuration(getScaledDuration(250)); containerScale.setInterpolator(overshootInterpolator); boolean scrimIsAvailable = alignTop && achievementLayout.getBackground() != null; ObjectAnimator scrim = null; if (scrimIsAvailable) { scrim = ObjectAnimator.ofInt(achievementLayout.getBackground(), "alpha", 0, 255); } AnimatorSet set = new AnimatorSet(); if (scrim != null) set.playTogether(containerScale, scrim); else set.play(containerScale); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); if (Color.alpha(iconBG) > 0) { setIconBg(iconBG); } else { View textContainer = (View) titleTextView.getParent(); textContainer.setPadding((isLarge ? largeSize : smallSize), textContainer.getPaddingTop(), textContainer.getPaddingRight(), textContainer.getPaddingBottom()); achievementLayout.findViewWithTag("textContainerFake").setPadding(textContainer.getPaddingLeft(), textContainer.getPaddingTop(), textContainer.getPaddingRight(), textContainer.getPaddingBottom()); } container.setVisibility(View.VISIBLE); setIcon(data); } }); return set; } private void setIcon(AchievementData data) { if (data == null) { // icon.setDrawable(null); return; } if (data.getState() == AchievementIconView.AchievementIconViewStates.SAME_DRAWABLE) return; Drawable d = data.getIcon(); if (d != null) { if (data.getState() == AchievementIconView.AchievementIconViewStates.FADE_DRAWABLE) icon.fadeDrawable(d); else icon.setDrawable(d); } else icon.setDrawable(null); } private void setSwipeEffect(float amount) { container.setTranslationX(amount); } private int getStatusBarHeight() { int result = 0; int resourceId = Resources.getSystem().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = Resources.getSystem().getDimensionPixelSize(resourceId); } return result; } /* used by the abstract class adapter */ @SuppressWarnings("unused") interface AchievementListener { void onViewCreated(AchievementUnlocked achievement, AchievementData[] data); void onAchievementMorphed(AchievementUnlocked achievement, AchievementData data); void onAchievementDismissed(AchievementUnlocked achievement); } /** * Class that holds the data to be displayed by * AchievementUnlocked object using the * {@link AchievementUnlocked#show(AchievementData...)} method */ public static class AchievementData { private String title = "", subtitle; private Drawable icon; private int textColor = 0xff000000, backgroundColor = 0xffffffff, iconBackgroundColor = 0x0; private View.OnClickListener onClickListener; private AchievementIconView.AchievementIconViewStates state = null; public static AchievementData copyFrom(AchievementData data) { AchievementData result = new AchievementData(); result.setTitle(data.getTitle()); result.setSubtitle(data.getSubtitle()); result.setIcon(data.getIcon()); result.setState(data.getState()); result.setBackgroundColor(data.getBackgroundColor()); result.setIconBackgroundColor(data.getIconBackgroundColor()); result.setTextColor(data.getTextColor()); result.setPopUpOnClickListener(data.getPopUpOnClickListener()); return result; } public View.OnClickListener getPopUpOnClickListener() { return onClickListener; } /** * Assign a per-data onclick listener to the popup * * @return same AchievementData object */ public AchievementData setPopUpOnClickListener(View.OnClickListener onClickListener) { this.onClickListener = onClickListener; return this; } public int getTextColor() { return textColor; } public AchievementData setTextColor(int textColor) { this.textColor = textColor; return this; } public String getTitle() { return title; } public AchievementData setTitle(String title) { this.title = title; return this; } public String getSubtitle() { return subtitle; } public AchievementData setSubtitle(String subtitle) { this.subtitle = subtitle; return this; } public AchievementIconView.AchievementIconViewStates getState() { return state; } /** * Indicate whether the popup icon should stay the same or * fade when showing different Achievement data. Default is * null which is the same as SAME_DRAWABLE. * When FADE_DRAWABLE is set, the icon will animate change to the * next data icon. * * @param state either of these two: FADE_DRAWABLE, SAME_DRAWABLE */ public void setState(AchievementIconView.AchievementIconViewStates state) { this.state = state; } public Drawable getIcon() { return icon; } /** * Set popuup icon. Transparent one will be used if non is assigned * * @param icon icon drawable * @return same AchievementData object */ public AchievementData setIcon(Drawable icon) { this.icon = icon; return this; } int getBackgroundColor() { return backgroundColor; } /** * Set popup background color * * @param backgroundColor integer color of background * @return same AchievementData object */ public AchievementData setBackgroundColor(int backgroundColor) { this.backgroundColor = backgroundColor; return this; } int getIconBackgroundColor() { return iconBackgroundColor; } /** * Set the background of the popup's icon * * @param iconBackgroundColor integer color * @return same AchievementData object */ public AchievementData setIconBackgroundColor(int iconBackgroundColor) { this.iconBackgroundColor = iconBackgroundColor; return this; } } /** * Ticker text view used for subtitle view */ @SuppressLint("AppCompatCustomView") @SuppressWarnings("unused") final static class ScrollTextView extends TextView { private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); private final ValueAnimator mScrollingAnimator = ValueAnimator.ofInt(0, 1); private long mDuration, mStartOffset; public ScrollTextView(Context context) { super(context); init(); } public ScrollTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { setSingleLine(); } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); setSelected(visibility == VISIBLE); } @Override public void setAlpha(float alpha) { super.setAlpha(alpha); if (alpha <= 0.1f) { stopScrolling(); } } public void stopScrolling() { if (mScrollingAnimator.isRunning()) mScrollingAnimator.cancel(); } public void startScrolling() { requestFocus(); setSelected(true); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopScrolling(); } /** * Directly apply previously-calculated values instead of recalculating them * * @param scrollingDuration how many milliseconds shall the animator take to finish * scrolling the overflown text * @param startOffset how many milliseconds before the animator starts */ public void setDurations(long scrollingDuration, long startOffset) { this.mDuration = scrollingDuration; this.mStartOffset = startOffset; } /** * Use previously-calculated values for efficiency * * @param scrollAmount previously-calculated horizontal scroll amount */ void updateScroller(int scrollAmount) { if (mScrollingAnimator.isRunning()) { mScrollingAnimator.cancel(); } if (scrollAmount == 0) return; mScrollingAnimator.setIntValues(0, -scrollAmount); mScrollingAnimator.removeAllListeners(); mScrollingAnimator.removeAllUpdateListeners(); mScrollingAnimator.addUpdateListener(animation -> scrollTo(-(Integer) animation.getAnimatedValue(), 0)); mScrollingAnimator.setDuration(mDuration); //LINEAR_INTERPOLATOR must be used mScrollingAnimator.setInterpolator(LINEAR_INTERPOLATOR); mScrollingAnimator.setStartDelay(mStartOffset); mScrollingAnimator.start(); } } /** * GradientDrawable that saves the drawable colors; used for AU background */ final static class GradientDrawableWithColors extends GradientDrawable { private int mColor; int getGradientColor() { return mColor; } @Override public void setColor(int argb) { super.setColors(new int[]{argb, argb}); mColor = argb; } @Override public void setColors(int[] colors) { super.setColors(colors); mColor = colors[0]; } } /** * ImageView that animates drawable change. It also scales * according to the drawable size to make sure it doesn't clip */ @SuppressLint("AppCompatCustomView") static class AchievementIconView extends ImageView { public AchievementIconView(Context context) { super(context); } public void setDrawable(final Drawable drawable) { if (drawable == null) { setImageDrawable(null); return; } if (getScaleType() != ScaleType.CENTER_CROP) setScaleType(ScaleType.CENTER_CROP); final float scaleX = 3.5f / (getMaxWidth() / drawable.getIntrinsicWidth()); final float scaleY = 3.5f / (getMaxWidth() / drawable.getIntrinsicHeight()); if (getDrawable() == null) { setImageDrawable(drawable); setScaleX(1 / Math.max(scaleX, scaleY)); setScaleY(1 / Math.max(scaleX, scaleY)); } else { if (drawable.getAlpha() < 255) drawable.setAlpha(255); animate() .scaleX(0f) .setDuration(AchievementUnlocked.getScaledDuration(200)) .scaleY(0f) .alpha(0f) .withEndAction(() -> animate() .setDuration(AchievementUnlocked.getScaledDuration(200)) .scaleX(1 / Math.max(scaleX, scaleY)) .scaleY(1 / Math.max(scaleX, scaleY)) .alpha(1f) .withStartAction(() -> setImageDrawable(drawable)) .start()) .start(); } } public void fadeDrawable(final Drawable drawable) { if (drawable == null) { setImageDrawable(null); return; } if (getScaleType() != ScaleType.CENTER_CROP) setScaleType(ScaleType.CENTER_CROP); final float scaleX = 3.5f / (getMaxWidth() / drawable.getIntrinsicWidth()); final float scaleY = 3.5f / (getMaxWidth() / drawable.getIntrinsicHeight()); if (getDrawable() == null) { setImageDrawable(drawable); setScaleX(1 / Math.max(scaleX, scaleY)); setScaleY(1 / Math.max(scaleX, scaleY)); } else { if (drawable.getAlpha() < 255) drawable.setAlpha(255); animate() .setDuration(AchievementUnlocked.getScaledDuration(50)) .alpha(0f) .withEndAction(() -> animate() .setDuration(AchievementUnlocked.getScaledDuration(50)) .alpha(1f) .withStartAction(() -> setImageDrawable(drawable)) .start()) .start(); } } public enum AchievementIconViewStates { FADE_DRAWABLE, SAME_DRAWABLE } } /** * Same as LogDecelerateInterpolator.java from Launcher3 */ final static class DeceleratingInterpolator implements TimeInterpolator { private final float mLogScale; private final int mBase; DeceleratingInterpolator(int base) { mBase = base; mLogScale = 1f / computeLog(1, mBase); } private static float computeLog(float t, int base) { return (float) -Math.pow(base, -t) + 1; } @Override public float getInterpolation(float t) { return computeLog(t, mBase) * mLogScale; } } final static class WindowOverlayCompat { private static final int ANDROID_OREO = 26; private static final int TYPE_APPLICATION_OVERLAY = 2038; static final int TYPE_SYSTEM_ERROR = Build.VERSION.SDK_INT < ANDROID_OREO ? WindowManager.LayoutParams.TYPE_SYSTEM_ERROR : TYPE_APPLICATION_OVERLAY; } private class SwipeDismissTouchListener implements View.OnTouchListener { private final int mSlop; private final int mMinFlingVelocity; private final int mMaxFlingVelocity; private final long mAnimationTime; private float mDownX; private boolean mSwiping; private float mTranslationX; private final Runnable end; SwipeDismissTouchListener() { ViewConfiguration vc = ViewConfiguration.get(App.Companion.getContext()); mSlop = vc.getScaledTouchSlop(); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); mAnimationTime = App.Companion.getContext().getResources().getInteger(android.R.integer.config_shortAnimTime); end = new Runnable() { @Override public void run() { hasBeenDismissed = true; //Fade out the popup view instead of direct visibility // change, for aesthetics. // Since there is no scrim in bottom-aligned and Android P+, // we can use setVisibility directly if (alignTop && VERSION.SDK_INT < 28) achievementLayout.animate().alpha(0f).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); achievementLayout.setVisibility(GONE); } }).start(); else achievementLayout.setVisibility(GONE); } }; } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View view, MotionEvent motionEvent) { motionEvent.offsetLocation(mTranslationX, 0); float deltaX = (motionEvent.getRawX() - mDownX); switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mDownX = motionEvent.getRawX(); view.onTouchEvent(motionEvent); return false; } case MotionEvent.ACTION_UP: { if (container.getAlpha() == 0) { dismissWithoutAnimation(); return true; } boolean dismiss = false; boolean dismissRight = false; int spaceToEdge = ((achievementLayout.getWidth() - container.getWidth()) / 2); float swipePercentage = Math.abs(mTranslationX / spaceToEdge); if (swipePercentage >= 0.5f) { dismiss = true; dismissRight = deltaX > 0; } if (dismiss) { ObjectAnimator translation = ObjectAnimator.ofFloat(container, View.TRANSLATION_X, container.getTranslationX(), dismissRight ? container.getMeasuredWidth() : -container.getMeasuredWidth()); translation.addUpdateListener(animation -> setSwipeEffect((float) animation.getAnimatedValue())); translation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (end != null) end.run(); } }); translation.setInterpolator(TIME_INTERPOLATOR); translation.setDuration(mAnimationTime); translation.start(); dismissed = true; } else { ObjectAnimator translation = ObjectAnimator.ofFloat(container, View.TRANSLATION_X, container.getTranslationX(), 0); translation.addUpdateListener(animation -> setSwipeEffect((float) animation.getAnimatedValue())); translation.setDuration(mAnimationTime); translation.setInterpolator(TIME_INTERPOLATOR); translation.start(); dismissed = false; } mTranslationX = 0; mDownX = 0; mSwiping = false; break; } case MotionEvent.ACTION_MOVE: { if (Math.abs(deltaX) > mSlop) { mSwiping = true; container.getParent().requestDisallowInterceptTouchEvent(true); MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); container.onTouchEvent(cancelEvent); } if (mSwiping) { mTranslationX = deltaX; setSwipeEffect(mTranslationX); return true; } break; } } return false; } } /** * Adapter for listener */ abstract class AchievementListenerAdapter implements AchievementListener { @Override public void onAchievementDismissed(AchievementUnlocked achievement) { } @Override public void onViewCreated(AchievementUnlocked achievement, AchievementData[] data) { } @Override public void onAchievementMorphed(AchievementUnlocked achievement, AchievementData data) { } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/AppWebView.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import android.webkit.WebView class AppWebView : WebView { constructor(context: Context) : super(context.applicationContext) constructor(context: Context, attrs: AttributeSet) : super(context.applicationContext, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context.applicationContext, attrs, defStyleAttr) } ================================================ FILE: app/src/main/java/knf/kuma/custom/BackgroundExecutor.kt ================================================ package knf.kuma.custom import android.os.AsyncTask import java.util.concurrent.Executor class BackgroundExecutor : Executor { override fun execute(command: Runnable?) { AsyncTask.execute(command) } } ================================================ FILE: app/src/main/java/knf/kuma/custom/BannerContainerView.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import knf.kuma.R import org.jetbrains.anko.find class BannerContainerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : LinearLayout(context, attrs, defStyle) { private var showBottom = false init { attrs?.let { val array = context.obtainStyledAttributes(it, R.styleable.BannerContainerView) showBottom = array.getBoolean(R.styleable.BannerContainerView_showBottomSpace, false) array.recycle() } inflate(context, R.layout.lay_banner_container, this) } fun show(view: View) { if (showBottom) { ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> find(R.id.spaceBottom).layoutParams.height = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom WindowInsetsCompat.CONSUMED } find(R.id.spaceBottom).visibility = VISIBLE } find(R.id.spaceTop).visibility = VISIBLE with(find(R.id.container)) { removeAllViews() addView(view) } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/CenterLayoutManager.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView class CenterLayoutManager : LinearLayoutManager { constructor(context: Context) : super(context) constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State?, position: Int) { val centerSmoothScroller = CenterSmoothScroller(recyclerView.context) centerSmoothScroller.targetPosition = position startSmoothScroll(centerSmoothScroller) } private class CenterSmoothScroller(context: Context) : LinearSmoothScroller(context) { override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2) } } ================================================ FILE: app/src/main/java/knf/kuma/custom/ConnectionState.kt ================================================ package knf.kuma.custom import android.animation.Animator import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.graphics.Color import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.res.use import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.lifecycleScope import it.sephiroth.android.library.xtooltip.ClosePolicy import it.sephiroth.android.library.xtooltip.Tooltip import knf.kuma.App import knf.kuma.Diagnostic import knf.kuma.R import knf.kuma.commons.BypassUtil import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.asPx import knf.kuma.commons.distinct import knf.kuma.commons.findActivity import knf.kuma.commons.isFullMode import knf.kuma.commons.jsoupCookies import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashLet import knf.kuma.database.CacheDB import knf.kuma.databinding.LayStatusBarBinding import knf.kuma.directory.DirectoryService import knf.tools.bypass.startBypass import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick import org.jetbrains.anko.sdk27.coroutines.onLongClick import org.jetbrains.anko.textColor import org.json.JSONObject import org.jsoup.Connection import org.jsoup.HttpStatusException import org.jsoup.Jsoup import java.net.NoRouteToHostException import java.net.SocketTimeoutException import java.net.URL import java.net.UnknownHostException import javax.net.ssl.SSLException @SuppressLint("SetTextI18n") class ConnectionState : LinearLayout { constructor(context: Context) : super(context) { inflate(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { inflate(context, attrs) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { inflate(context, attrs) } private var bgColor = Color.TRANSPARENT private lateinit var binding: LayStatusBarBinding private fun inflate(context: Context, attrs: AttributeSet? = null) { context.obtainStyledAttributes(attrs, R.styleable.ConnectionState).use { bgColor = it.getColor(R.styleable.ConnectionState_cs_bg_color, ContextCompat.getColor(context, R.color.colorPrimary)) } val inflater = context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater inflater.inflate(R.layout.lay_status_bar, this) binding = LayStatusBarBinding.bind(this) } override fun onFinishInflate() { super.onFinishInflate() binding.container.setBackgroundColor(bgColor) } private var isInitialized = false fun setUp(owner: LifecycleOwner, onShowDialog: (message: String) -> Unit) { if (!isInitialized || visibility == VISIBLE) GlobalScope.launch(Dispatchers.Main + untilDestroyJob(owner)) { normalState() show() delay(1000) if (!Network.isConnected) { noNetworkState() while (!Network.isConnected) { delay(3000) } normalState() } doNetworkTests(owner, onShowDialog) isInitialized = true } } private suspend fun doNetworkTests(owner: LifecycleOwner, onShowDialog: (message: String) -> Unit) { when (val code = withContext(Dispatchers.IO + untilDestroyJob(owner)) { doCookiesTest() }) { -2 -> networkTimeoutState(owner, onShowDialog) 200 -> { if (!PrefsUtil.isDirectoryFinished && !withContext(Dispatchers.IO) { BypassUtil.isCloudflareActive() }) directoryListenState() else { okState() dismiss() } } 403 -> { errorBlockedState(403, onShowDialog) var defResult = true val observer = Observer> { if (defResult) { defResult = false return@Observer } GlobalScope.launch(Dispatchers.Main + untilDestroyJob(owner)) { if (BypassUtil.isLoading) { warningCreatingState() } else if (it.first && !it.second) { okState() delay(1000) dismiss() GenericActivity.removeBypassObserver("connectionState") } else if (!it.first && !it.second) { //dismiss() GenericActivity.removeBypassObserver("connectionState") normalState() doNetworkTests(owner, onShowDialog) } } } GenericActivity.addBypassObserver("connectionState", owner, observer) } 502 -> errorDownState(owner, onShowDialog) 503 -> { errorBlockedState(503, onShowDialog) var defResult = true val observer = Observer> { if (defResult) { defResult = false return@Observer } GlobalScope.launch(Dispatchers.Main + untilDestroyJob(owner)) { if (BypassUtil.isLoading) { warningCreatingState() } else if (it.first && !it.second) { okState() delay(1000) dismiss() GenericActivity.removeBypassObserver("connectionState") } else if (!it.first && !it.second) { //dismiss() GenericActivity.removeBypassObserver("connectionState") normalState() doNetworkTests(owner, onShowDialog) } } } GenericActivity.addBypassObserver("connectionState", owner, observer) } else -> { try { val json = JSONObject(withContext(Dispatchers.IO) { URL("https://ipinfo.io/json").readText() }) if (json.getString("country") == "PE") { errorCountryState(owner, onShowDialog) } else { networkErrorState(owner = owner, onShowDialog = onShowDialog) } } catch (e: Exception) { when (e) { is UnknownHostException, is NoRouteToHostException, is SSLException -> { okState() dismiss() } else -> { networkErrorState(owner = owner, message = "HTTP $code Error", onShowDialog = onShowDialog) } } } } } } private fun untilDestroyJob(owner: LifecycleOwner): Job { val job = Job() owner.lifecycle.addObserver(object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job } private fun doCookiesTest(): Int { return try { val timeout = PrefsUtil.timeoutTime.toInt() * 1000 val response = Jsoup.connect(BypassUtil.testLink) .cookies(BypassUtil.getMapCookie(App.context)) .userAgent(BypassUtil.userAgent) .timeout(if (timeout == 0) 30000 else timeout) .method(Connection.Method.GET) .execute() response.statusCode() } catch (e: HttpStatusException) { e.statusCode } catch (e: SocketTimeoutException) { -2 } catch (e: Exception) { -1 } } private fun tryTimeoutFix(): Boolean { val timeout = when (PrefsUtil.timeoutTime) { 10L -> { PrefsUtil.timeoutTime = 20L 20L } 20L -> { PrefsUtil.timeoutTime = 30L 30L } else -> return false } * 1000 return try { Jsoup.connect("https://www3.animeflv.net") .cookies(BypassUtil.getMapCookie(App.context)) .userAgent(BypassUtil.userAgent) .timeout(timeout.toInt()) .method(Connection.Method.GET) .execute() true } catch (e: SocketTimeoutException) { false } catch (e: Exception) { true } } private fun normalState() { binding.container.setBackgroundColor(bgColor) binding.progress.visibility = VISIBLE binding.icon.visibility = GONE binding.message.text = "Comprobando..." binding.message.textColor = ContextCompat.getColor(context, R.color.textSecondary) binding.container.setOnClickListener(null) binding.container.setOnLongClickListener(null) } private fun noNetworkState() { binding.container.setBackgroundColor(bgColor) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_no_network) binding.icon.visibility = VISIBLE binding.message.text = "Sin internet!" binding.message.textColor = ContextCompat.getColor(context, R.color.textSecondary) binding.container.setOnClickListener(null) binding.container.setOnLongClickListener(null) } private fun errorDownState(owner: LifecycleOwner, onShowDialog: (message: String) -> Unit) { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccent)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_error) binding.icon.visibility = VISIBLE binding.message.text = "Animeflv caido" binding.message.textColor = Color.WHITE binding.container.onClick { onShowDialog("Animeflv parece estar caido por el momento, intenta de nuevo mas tarde") } binding.container.onLongClick { setUp(owner, onShowDialog) } } private fun errorBlockedState(errorCode: Int, onShowDialog: (message: String) -> Unit) { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccent)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_error) binding.icon.visibility = VISIBLE binding.message.text = "Error HTTP $errorCode!" binding.message.textColor = Color.WHITE binding.container.onClick { PrefsUtil.isForbiddenTipShown = true //context.startActivity(Intent(context, Diagnostic.FullBypass::class.java)) (context.findActivity() as? AppCompatActivity)?.startBypass( 4157, BypassUtil.createRequest() ) } binding.container.setOnLongClickListener(null) if (isFullMode && !PrefsUtil.isForbiddenTipShown) { noCrash { Tooltip.Builder(context).apply { arrow(true) text("Haz click en la barra roja para resolver el captcha!") overlay(true) styleId(R.style.ToolTipAltStyle) anchor(this@ConnectionState) closePolicy(ClosePolicy.TOUCH_ANYWHERE_CONSUME) }.create().show(this, Tooltip.Gravity.TOP) } } } private fun errorCountryState(owner: LifecycleOwner, onShowDialog: (message: String) -> Unit) { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccent)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_error) binding.icon.visibility = VISIBLE binding.message.text = "VPN necesario" binding.message.textColor = Color.WHITE binding.container.onClick { onShowDialog("Tu pais ha bloqueado la conexion con Animeflv por lo que es necesario usar una VPN para seguir usando la app") } binding.container.onLongClick { setUp(owner, onShowDialog) } } private fun networkErrorState(owner: LifecycleOwner, message: String = "Error desconocido!", onShowDialog: (message: String) -> Unit) { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccent)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_error) binding.icon.visibility = VISIBLE binding.message.text = message binding.message.textColor = Color.WHITE binding.container.onClick { onShowDialog("Hubo un error haciendo las pruebas de conexion!") } binding.container.onLongClick { setUp(owner, onShowDialog) } } private fun networkTimeoutState(owner: LifecycleOwner, onShowDialog: (message: String) -> Unit) { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccent)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_error) binding.icon.visibility = VISIBLE binding.message.text = "Animeflv lento!" binding.message.textColor = Color.WHITE binding.container.onClick { onShowDialog("Se detectó un problema con la página de Animeflv, es posible que esté en mantenimiento, este problema se solucionará solo, no es necesario reportarlo!") } binding.container.onLongClick { setUp(owner, onShowDialog) } owner.doAsync { var isFixed = false repeat(3) { if (!isFixed && tryTimeoutFix()) { isFixed = true setUp(owner, onShowDialog) } } } } private fun warningState(onShowDialog: (message: String) -> Unit) { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccentAmber)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_warning) binding.icon.visibility = VISIBLE binding.message.text = "Cloudflare activado!" binding.message.textColor = Color.WHITE binding.container.onClick { onShowDialog("Cloudflare activado, espera al bypass") } binding.container.onLongClick { context.startActivity(Intent(context, Diagnostic.FullBypass::class.java)) } } private fun warningCreatingState() { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccentAmber)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_warning) binding.icon.visibility = VISIBLE binding.message.text = "Actualizando bypass!" binding.message.textColor = Color.WHITE binding.container.setOnClickListener(null) binding.container.setOnLongClickListener(null) } private suspend fun directoryListenState() { (context.findActivity() as? AppCompatActivity)?.let { owner -> binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccentAmber)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_warning) binding.icon.visibility = VISIBLE binding.message.textColor = Color.WHITE binding.container.setOnClickListener(null) binding.container.setOnLongClickListener(null) val maxPages = withContext(Dispatchers.IO) { noCrashLet("3200~") { val main = jsoupCookies("https://www3.animeflv.net/browse").get() val lastPage = main.select("ul.pagination li:matches(\\d+)").last().text().trim().toInt() val last = try { jsoupCookies("https://www3.animeflv.net/browse?page=$lastPage").get().select("article").size } catch (e: Exception) { 0 } ((24 * (lastPage - 1)) + last).toString() } } var msg = "Calculando" CacheDB.INSTANCE.animeDAO().countLive.distinct.observe(owner, Observer { binding.message.text = "$msg: $it/$maxPages" }) DirectoryService.getLiveStatus().observe(owner, Observer { when (it) { DirectoryService.STATE_CACHED -> { msg = "Descargando directorio" } in DirectoryService.STATE_PARTIAL..DirectoryService.STATE_FULL -> { msg = "Actualizando directorio" } DirectoryService.STATE_FINISHED -> { owner.lifecycleScope.launch(Dispatchers.Main) { okState() dismiss() } } } }) } ?: run { okState() GlobalScope.launch(Dispatchers.Main) { this@ConnectionState.dismiss() } } } private fun okState() { binding.container.setBackgroundColor(ContextCompat.getColor(context, R.color.colorAccentGreen)) binding.progress.visibility = GONE binding.icon.setImageResource(R.drawable.ic_check) binding.icon.visibility = VISIBLE binding.message.text = "Todo en orden!" binding.message.textColor = Color.WHITE binding.container.setOnClickListener(null) binding.container.setOnLongClickListener(null) } private fun show() { val height = 24.asPx layoutParams = layoutParams.apply { this.height = 0 } visibility = VISIBLE ValueAnimator.ofInt(0, height).apply { addUpdateListener { layoutParams = layoutParams.apply { this.height = it.animatedValue as Int } } duration = 350 }.start() } private suspend fun dismiss() { delay(1000) val height = 24.asPx ValueAnimator.ofInt(height, 0).apply { addUpdateListener { layoutParams = layoutParams.apply { this.height = it.animatedValue as Int } } addListener(object : Animator.AnimatorListener { override fun onAnimationRepeat(animation: Animator) { } override fun onAnimationEnd(animation: Animator) { visibility = GONE layoutParams = layoutParams.apply { this.height = height } } override fun onAnimationCancel(animation: Animator) { } override fun onAnimationStart(animation: Animator) { } }) duration = 350 }.start() } } ================================================ FILE: app/src/main/java/knf/kuma/custom/ExpandableTV.kt ================================================ package knf.kuma.custom import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.TimeInterpolator import android.animation.ValueAnimator import android.content.Context import android.graphics.Paint import android.graphics.Rect import android.util.AttributeSet import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator import android.widget.ImageButton import android.widget.TextView import androidx.appcompat.widget.AppCompatTextView class ExpandableTV @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : AppCompatTextView(context, attrs, defStyle) { private val onExpandListeners: MutableList private var mMaxLines = 4 private var indicator: ImageButton? = null private var needIndicator = true private var expandInterpolator: TimeInterpolator? = null private var collapseInterpolator: TimeInterpolator? = null private var animationDuration: Long = 0 private var animating: Boolean = false /** * Is this [ExpandableTV] expanded or not? * * @return true if expanded, false if collapsed. */ var isExpanded: Boolean = false private set private var collapsedHeight: Int = 0 init { // keep the original value of mMaxLines this.mMaxLines = this.maxLines // create bucket of OnExpandListener instances this.onExpandListeners = ArrayList() // create default interpolators this.expandInterpolator = AccelerateDecelerateInterpolator() this.collapseInterpolator = AccelerateDecelerateInterpolator() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { var measureSpec = heightMeasureSpec // if this TextView is collapsed and mMaxLines = 0, // than make its height equals to zero if (this.mMaxLines == 0 && !this.isExpanded && !this.animating) { measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY) } super.onMeasure(widthMeasureSpec, measureSpec) } //region public helper methods /** * Toggle the expanded state of this [ExpandableTV]. * * @return true if toggled, false otherwise. */ fun toggle(): Boolean { return if (this.isExpanded) this.collapse() else this.expand() } /** * Expand this [ExpandableTV]. * * @return true if expanded, false otherwise. */ private fun expand(): Boolean { if (!this.isExpanded && !this.animating && this.mMaxLines >= 0) { // notify listener this.notifyOnExpand() // measure collapsed height this.measure( MeasureSpec.makeMeasureSpec(this.measuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) ) this.collapsedHeight = this.measuredHeight // indicate that we are now animating this.animating = true // set mMaxLines to MAX Integer, so we can calculate the expanded height this.maxLines = Integer.MAX_VALUE // measure expanded height this.measure( MeasureSpec.makeMeasureSpec(this.measuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) ) val expandedHeight = this.measuredHeight // animate from collapsed height to expanded height val valueAnimator = ValueAnimator.ofInt(this.collapsedHeight, expandedHeight) valueAnimator.addUpdateListener { animation -> this@ExpandableTV.height = animation.animatedValue as Int } // wait for the animation to end valueAnimator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { // reset min & max height (previously set with setHeight() method) this@ExpandableTV.maxHeight = Integer.MAX_VALUE this@ExpandableTV.minHeight = 0 // if fully expanded, set height to WRAP_CONTENT, because when rotating the device // the height calculated with this ValueAnimator isn't correct anymore val layoutParams = this@ExpandableTV.layoutParams layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT this@ExpandableTV.layoutParams = layoutParams // keep track of current status this@ExpandableTV.isExpanded = true this@ExpandableTV.animating = false } }) // set interpolator valueAnimator.interpolator = this.expandInterpolator // start the animation valueAnimator .setDuration(this.animationDuration) .start() return true } return false } /** * Collapse this [TextView]. * * @return true if collapsed, false otherwise. */ private fun collapse(): Boolean { if (this.isExpanded && !this.animating && this.mMaxLines >= 0) { // notify listener this.notifyOnCollapse() // measure expanded height val expandedHeight = this.measuredHeight // indicate that we are now animating this.animating = true // animate from expanded height to collapsed height val valueAnimator = ValueAnimator.ofInt(expandedHeight, this.collapsedHeight) valueAnimator.addUpdateListener { animation -> this@ExpandableTV.height = animation.animatedValue as Int } // wait for the animation to end valueAnimator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { // keep track of current status this@ExpandableTV.isExpanded = false this@ExpandableTV.animating = false // set mMaxLines back to original value this@ExpandableTV.maxLines = this@ExpandableTV.mMaxLines // if fully collapsed, set height back to WRAP_CONTENT, because when rotating the device // the height previously calculated with this ValueAnimator isn't correct anymore val layoutParams = this@ExpandableTV.layoutParams layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT this@ExpandableTV.layoutParams = layoutParams } }) // set interpolator valueAnimator.interpolator = this.collapseInterpolator // start the animation valueAnimator .setDuration(this.animationDuration) .start() return true } return false } //endregion //region public getters and setters /** * Sets the duration of the expand / collapse animation. * * @param animationDuration duration in milliseconds. */ fun setAnimationDuration(animationDuration: Long) { this.animationDuration = animationDuration } /** * Adds a listener which receives updates about this [ExpandableTV]. * * @param onExpandListener the listener. */ fun addOnExpandListener(onExpandListener: OnExpandListener) { this.onExpandListeners.add(onExpandListener) } /** * Removes a listener which receives updates about this [ExpandableTV]. * * @param onExpandListener the listener. */ fun removeOnExpandListener(onExpandListener: OnExpandListener) { this.onExpandListeners.remove(onExpandListener) } /** * Sets a [TimeInterpolator] for expanding and collapsing. * * @param interpolator the interpolator */ fun setInterpolator(interpolator: TimeInterpolator) { this.expandInterpolator = interpolator this.collapseInterpolator = interpolator } //endregion /** * This method will notify the listener about this view being expanded. */ private fun notifyOnCollapse() { for (onExpandListener in this.onExpandListeners) { onExpandListener.onCollapse(this) } } /** * This method will notify the listener about this view being collapsed. */ private fun notifyOnExpand() { for (onExpandListener in this.onExpandListeners) { onExpandListener.onExpand(this) } } //region public interfaces fun setIndicator(indicator: ImageButton) { this.indicator = indicator } fun setTextAndIndicator(charSequence: CharSequence, indicator: ImageButton) { this.indicator = indicator text = charSequence /*getViewTreeObserver().addOnGlobalLayoutListener(() -> { needIndicator=getLineCount()>4; *//*this.measure ( MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) ); int nH = getMeasuredHeight();*//* setMaxLines(4); mMaxLines=4; *//*this.measure ( MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) ); int cH = getMeasuredHeight(); needIndicator = nH>cH;*//* if (!needIndicator) indicator.post(() -> indicator.setVisibility(GONE)); });*/ } fun checkIndicator() { val bounds = Rect() val paint = Paint() paint.textSize = textSize paint.getTextBounds(text.toString(), 0, text.length, bounds) val numLines = Math.ceil((bounds.width().toFloat() / textSize).toDouble()).toInt() needIndicator = numLines > 4 maxLines = 4 mMaxLines = 4 if (!needIndicator) indicator?.post { indicator?.visibility = GONE } } /** * Interface definition for a callback to be invoked when * a [ExpandableTV] is expanded or collapsed. */ interface OnExpandListener { /** * The [ExpandableTV] is being expanded. * * @param view the textview */ fun onExpand(view: ExpandableTV) /** * The [ExpandableTV] is being collapsed. * * @param view the textview */ fun onCollapse(view: ExpandableTV) } /** * Simple implementation of the [ExpandableTV.OnExpandListener] interface with stub * implementations of each method. Extend this if you do not intend to override * every method of [ExpandableTV.OnExpandListener]. */ class SimpleOnExpandListener : OnExpandListener { override fun onExpand(view: ExpandableTV) { // empty implementation } override fun onCollapse(view: ExpandableTV) { // empty implementation } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/ExpandableTextView.kt ================================================ package knf.kuma.custom import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable import android.os.Handler import android.os.Message import android.util.AttributeSet import android.util.Log import android.view.LayoutInflater import android.view.View import android.widget.ImageButton import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import knf.kuma.R class ExpandableTextView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs), View.OnClickListener { private val WHAT = 2 private val WHAT_ANIMATION_END = 3 private val WHAT_EXPAND_ONLY = 4 private var textView: TextView? = null private var ivExpandOrShrink: ImageButton? = null private var drawableShrink: Drawable? = null private var drawableExpand: Drawable? = null private var textViewStateColor: Int = 0 private var isShrink = false private var isExpandNeeded = false private var isInitTextView = true private var expandLines: Int = 0 private var textLines: Int = 0 private var textContent: CharSequence? = null private val textContentColor: Int = 0 private var textContentSize: Float = 0.toFloat() private var thread: Thread? = null private var sleepTime = 22 @SuppressLint("HandlerLeak") private val handler = object : Handler() { override fun handleMessage(msg: Message) { when { WHAT == msg.what -> { textView?.maxLines = msg.arg1 textView?.invalidate() } WHAT_ANIMATION_END == msg.what -> setExpandState(msg.arg1) WHAT_EXPAND_ONLY == msg.what -> changeExpandState(msg.arg1) } super.handleMessage(msg) } } init { initValue(context, attrs) initView(context) initClick() } private fun initValue(context: Context, attrs: AttributeSet) { val ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView) expandLines = ta.getInteger( R.styleable.ExpandableTextView_tvea_expandLines, 5) drawableShrink = ta .getDrawable(R.styleable.ExpandableTextView_tvea_shrinkBitmap) drawableExpand = ta .getDrawable(R.styleable.ExpandableTextView_tvea_expandBitmap) textViewStateColor = ta.getColor(R.styleable.ExpandableTextView_tvea_textStateColor, ContextCompat.getColor(context, R.color.colorPrimary)) textContentSize = ta.getDimension(R.styleable.ExpandableTextView_tvea_textContentSize, 18f) ta.recycle() } private fun initView(context: Context) { val inflater = context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater inflater?.inflate(R.layout.layout_expandable_textview, this) textView = findViewById(R.id.tv_expand_text_view_animation) //textView.setTextColor(textContentColor); //textView.getPaint().setTextSize(textContentSize); textView?.maxLines = expandLines } fun setExpandIndicatorButton(button: ImageButton) { ivExpandOrShrink = button ivExpandOrShrink?.setOnClickListener(this) } private fun initClick() { textView?.setOnClickListener(this) } fun setTextColor(@ColorInt color: Int) { textView?.setTextColor(color) } fun setStateColorFilter(@ColorInt color: Int) { if (ivExpandOrShrink != null) ivExpandOrShrink?.setColorFilter(color) } fun setText(charSequence: CharSequence) { textContent = charSequence textView?.text = charSequence.toString() val viewTreeObserver = textView?.viewTreeObserver viewTreeObserver?.addOnPreDrawListener { if (!isInitTextView) { return@addOnPreDrawListener true } textLines = textView?.lineCount ?: 0 Log.e("Expandable", "Lines: $textLines") isExpandNeeded = textLines > expandLines isInitTextView = false if (isExpandNeeded) { isShrink = true doAnimation(textLines, expandLines, WHAT_ANIMATION_END) } else { isShrink = false doNotExpand() } true } } private fun doAnimation(startIndex: Int, endIndex: Int, what: Int) { thread = Thread { if (startIndex < endIndex) { // 如果起止行数小于结束行数,那么往下展开至结束行数 // if open index smaller than end index ,do expand action var count = startIndex while (count++ < endIndex) { val msg = handler.obtainMessage(WHAT, count, 0) try { Thread.sleep(sleepTime.toLong()) } catch (e: InterruptedException) { e.printStackTrace() } handler.sendMessage(msg) } } else if (startIndex > endIndex) { // 如果起止行数大于结束行数,那么往上折叠至结束行数 // if open index bigger than end index ,do shrink action var count = startIndex while (count-- > endIndex) { val msg = handler.obtainMessage(WHAT, count, 0) try { Thread.sleep(sleepTime.toLong()) } catch (e: InterruptedException) { e.printStackTrace() } handler.sendMessage(msg) } } // 动画结束后发送结束的信号 // animation end,send signal val msg = handler.obtainMessage(what, endIndex, 0) handler.sendMessage(msg) } thread?.start() } private fun changeExpandState(endIndex: Int) { if (endIndex < textLines) { if (ivExpandOrShrink != null) ivExpandOrShrink?.setImageDrawable(drawableExpand) } else { if (ivExpandOrShrink != null) ivExpandOrShrink?.setImageDrawable(drawableShrink) } } private fun setExpandState(endIndex: Int) { if (endIndex < textLines) { isShrink = true if (ivExpandOrShrink != null) ivExpandOrShrink?.setImageDrawable(drawableExpand) textView?.setOnClickListener(this) } else { isShrink = false if (ivExpandOrShrink != null) ivExpandOrShrink?.setImageDrawable(drawableShrink) textView?.setOnClickListener(null) } } private fun doNotExpand() { textView?.maxLines = expandLines textView?.setOnClickListener(null) if (ivExpandOrShrink != null) { ivExpandOrShrink?.setOnClickListener(null) ivExpandOrShrink?.visibility = GONE } } override fun onClick(v: View) { clickImageToggle() } private fun clickImageToggle() { if (isShrink) { // 如果是已经折叠,那么进行非折叠处理 // do shrink action doAnimation(expandLines, textLines, WHAT_EXPAND_ONLY) } else { // 如果是非折叠,那么进行折叠处理 // do expand action doAnimation(textLines, expandLines, WHAT_EXPAND_ONLY) } // 切换状态 // set flag isShrink = !isShrink } fun getExpandLines(): Int { return expandLines } fun setExpandLines(newExpandLines: Int) { val start = if (isShrink) this.expandLines else textLines val end = if (textLines < newExpandLines) textLines else newExpandLines doAnimation(start, end, WHAT_ANIMATION_END) this.expandLines = newExpandLines } } ================================================ FILE: app/src/main/java/knf/kuma/custom/FSGridRecyclerView.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.view.animation.GridLayoutAnimationController import androidx.recyclerview.widget.GridLayoutManager class FSGridRecyclerView : FSRecyclerView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) override fun attachLayoutAnimationParameters(child: View, params: ViewGroup.LayoutParams, index: Int, count: Int) { val layoutManager = layoutManager if (adapter != null && layoutManager is GridLayoutManager) { var animationParams: GridLayoutAnimationController.AnimationParameters? = null if (params.layoutAnimationParameters != null) animationParams = params.layoutAnimationParameters as? GridLayoutAnimationController.AnimationParameters if (animationParams == null) { // If there are no animation parameters, create new once and attach them to // the LayoutParams. animationParams = GridLayoutAnimationController.AnimationParameters() params.layoutAnimationParameters = animationParams } // Next we are updating the parameters // Set the number of items in the RecyclerView and the index of this item animationParams.count = count animationParams.index = index // Calculate the number of columns and rows in the grid val columns = layoutManager.spanCount animationParams.columnsCount = columns animationParams.rowsCount = count / columns // Calculate the column/row position in the grid val invertedIndex = count - 1 - index animationParams.column = columns - 1 - invertedIndex % columns animationParams.row = animationParams.rowsCount - 1 - invertedIndex / columns } else { // Proceed as normal if using another type of LayoutManager super.attachLayoutAnimationParameters(child, params, index, count) } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/FSRecyclerView.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView open class FSRecyclerView : FastScrollRecyclerView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) override fun scrollToPositionAtProgress(touchFraction: Float): String { return try { super.scrollToPositionAtProgress(touchFraction) } catch (e: Exception) { "" } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/FixedGridLayoutManager.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import knf.kuma.commons.noCrash class FixedGridLayoutManager : GridLayoutManager { constructor(context: Context, spanCount: Int) : super(context, spanCount) constructor(context: Context, spanCount: Int, orientation: Int, reverseLayout: Boolean) : super(context, spanCount, orientation, reverseLayout) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) { noCrash { super.onLayoutChildren(recycler, state) } } override fun supportsPredictiveItemAnimations(): Boolean { return false } } ================================================ FILE: app/src/main/java/knf/kuma/custom/GenericActivity.kt ================================================ package knf.kuma.custom import android.app.ActivityManager import android.content.Intent import android.graphics.BitmapFactory import android.os.Build import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.R import knf.kuma.commons.BypassUtil import knf.kuma.commons.PicassoSingle import knf.kuma.commons.PrefsUtil import knf.kuma.commons.isFullMode import knf.kuma.commons.noCrash import knf.kuma.commons.toastLong import knf.kuma.directory.DirManager import knf.kuma.directory.DirectoryService import knf.kuma.retrofit.Repository import knf.kuma.uagen.randomUA import knf.kuma.videoservers.FileActions import knf.kuma.videoservers.ServersFactory import knf.tools.bypass.startBypass import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync open class GenericActivity : AppCompatActivity() { override fun onResume() { noCrash { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { val appIcon = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) setTaskDescription(ActivityManager.TaskDescription(getString(R.string.app_name), appIcon, ContextCompat.getColor(this, R.color.colorPrimary))) } } logText("On Resume check") noCrash { super.onResume() } } open fun getSnackbarAnchor(): View? = null open fun onBypassUpdated() { } open fun forceCreation(): Boolean = false open fun logText(text: String) { Log.e("Bypass", text) } fun checkBypass() { if (BypassUtil.isChecking) { logText("Already checking") return } BypassUtil.isChecking = true doAsync(exceptionHandler = { it.also { FirebaseCrashlytics.getInstance().recordException(it) logText("Error: ${it.message}") }.message?.toastLong() }) { var flag: Int if ((BypassUtil.isNeededFlag().also { flag = it } >= 1 || forceCreation()).also { logText("Is needed or forced: $it") } && !BypassUtil.isLoading.also { logText("Is already loading: $it") }) { BypassUtil.isChecking = false logText("Flag: $flag") val runBypass = { lifecycleScope.launch(Dispatchers.Main) { bypassLive.value = Pair(true, true) BypassUtil.isLoading = true startBypass( 4157, BypassUtil.createRequest() ) } } if (isFullMode && !PrefsUtil.isBypassWarningShown){ lifecycleScope.launch(Dispatchers.Main){ MaterialDialog(this@GenericActivity).show { lifecycleOwner(this@GenericActivity) title(text = "Bypass necesario") message(text = "La app necesita saltarse la proteccion de animeflv asi que necesita crear un bypass, esto puede tardar varios minutos, la pantalla cambiara automaticamente una vez terminado el proceso") cancelable(false) positiveButton(text = "OK"){ PrefsUtil.isBypassWarningShown = true runBypass() } } } }else{ runBypass() } } else { BypassUtil.isChecking = false logText("Creation not needed, aborting") bypassLive.postValue(Pair(false, false)) Log.e("CloudflareBypass", "Not needed") } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 4157) { val cookiesUpdated = data?.let { PrefsUtil.useDefaultUserAgent = false PrefsUtil.userAgent = it.getStringExtra("user_agent") ?: randomUA() BypassUtil.saveCookies(this, it.getStringExtra("cookies") ?: "null") } ?: false BypassUtil.isLoading = false bypassLive.value = Pair(first = cookiesUpdated, second = false) Repository().reloadAllRecents() onBypassUpdated() PicassoSingle.clear() //ThumbsDownloader.start(this) if (!PrefsUtil.isDirectoryFinished) { lifecycleScope.launch(Dispatchers.IO) { DirManager.checkPreDir() DirectoryService.run(this@GenericActivity) } } } } override fun onPause() { super.onPause() BypassUtil.isLoading = false } override fun onDestroy() { super.onDestroy() ServersFactory.clear() FileActions.reset() if (forceCreation()) bypassLive.value = Pair(false, false) } companion object { private val observersList = mutableMapOf>>() val bypassLive: MutableLiveData> = MutableLiveData() fun addBypassObserver(id: String, owner: LifecycleOwner, observer: Observer>) { removeBypassObserver(id) observersList[id] = observer bypassLive.observe(owner, observer) } fun removeBypassObserver(id: String) { if (observersList.containsKey(id)) { bypassLive.removeObserver(observersList[id]!!) observersList.remove(id) } } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/GridRecyclerView.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.view.animation.GridLayoutAnimationController import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView class GridRecyclerView : RecyclerView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) override fun attachLayoutAnimationParameters(child: View, params: ViewGroup.LayoutParams, index: Int, count: Int) { val layoutManager = layoutManager if (adapter != null && layoutManager is GridLayoutManager) { var animationParams: GridLayoutAnimationController.AnimationParameters? = null if (params.layoutAnimationParameters != null) animationParams = params.layoutAnimationParameters as GridLayoutAnimationController.AnimationParameters if (animationParams == null) { // If there are no animation parameters, create new once and attach them to // the LayoutParams. animationParams = GridLayoutAnimationController.AnimationParameters() params.layoutAnimationParameters = animationParams } // Next we are updating the parameters // Set the number of items in the RecyclerView and the index of this item animationParams.count = count animationParams.index = index // Calculate the number of columns and rows in the grid val columns = layoutManager.spanCount animationParams.columnsCount = columns animationParams.rowsCount = count / columns // Calculate the column/row position in the grid val invertedIndex = count - 1 - index animationParams.column = columns - 1 - invertedIndex % columns animationParams.row = animationParams.rowsCount - 1 - invertedIndex / columns } else { // Proceed as normal if using another type of LayoutManager super.attachLayoutAnimationParameters(child, params, index, count) } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/HiddenOverlay.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.LinearLayout import knf.kuma.R class HiddenOverlay : LinearLayout { constructor(context: Context) : super(context) { inflate(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { inflate(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { inflate(context) } private fun inflate(context: Context) { val inflater = context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater inflater.inflate(R.layout.view_hidden_overlay, this) } fun setHidden(hidden: Boolean, animate: Boolean) { setState(hidden) if (animate) post { val animation = AnimationUtils.loadAnimation(context, if (hidden) R.anim.fadein else R.anim.fadeout) animation.duration = 200 animation.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation) { } override fun onAnimationEnd(animation: Animation) { } override fun onAnimationRepeat(animation: Animation) { } }) startAnimation(animation) } } private fun setState(hidden: Boolean) { post { visibility = if (hidden) VISIBLE else GONE } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/HomeList.kt ================================================ package knf.kuma.custom import android.content.Context import android.content.Intent import android.util.AttributeSet import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import knf.kuma.R import knf.kuma.commons.bind import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.inflate import knf.kuma.home.UpdateableAdapter import org.jetbrains.anko.sdk27.coroutines.onClick import androidx.core.content.withStyledAttributes class HomeList : LinearLayout { private var showAll = false private var showAllText = "Ver Todos" private var isLarge = false private var startHidden = false private var showError = false private var subheaderText = "Subheader" private var errorText: String? = null private var isHidden = false private val subheader: TextView by bind(R.id.subheader) private val viewAll: MaterialButton by bind(R.id.viewAll) private val progress: ProgressBar by bind(R.id.progress) private val errorTV: TextView by bind(R.id.errorTV) private val recyclerView: RecyclerView by bind(R.id.recycler) private var adapter: UpdateableAdapter<*>? = null constructor(context: Context) : super(context) { inflate() } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { inflate(attrs) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { inflate(attrs) } private fun loadVars(attrs: AttributeSet?) { attrs?.let { context.withStyledAttributes(it, R.styleable.HomeList) { showAll = getBoolean(R.styleable.HomeList_hm_showViewAll, false) showAllText = getString(R.styleable.HomeList_hm_viewAllText) ?: "Ver Todos" isLarge = getBoolean(R.styleable.HomeList_hm_isLarge, false) startHidden = getBoolean(R.styleable.HomeList_hm_startHidden, false) subheaderText = getString(R.styleable.HomeList_hm_subheader) ?: "Subheader" errorText = getString(R.styleable.HomeList_hm_errorText) showError = !errorText.isNullOrEmpty() } } } private fun inflate(attrs: AttributeSet? = null) { loadVars(attrs) this.inflate(if (isLarge) R.layout.view_home_list_large else R.layout.view_home_list, true) } override fun onFinishInflate() { super.onFinishInflate() if (startHidden) visibility = GONE viewAll.text = showAllText subheader.text = subheaderText errorTV.text = errorText if (showAll) viewAll.visibility = VISIBLE } fun hide() { doOnUIGlobal { isHidden = true visibility = GONE } } fun show() { doOnUIGlobal { isHidden = false visibility = VISIBLE } } fun setSubheader(text: String) { doOnUIGlobal { subheader.text = text.also { subheaderText = it } } } fun setError(text: String) { doOnUIGlobal { errorTV.text = text.also { errorText = it } } } fun setViewAllClass(clazz: Class) { viewAll.onClick { context.startActivity(Intent(context, clazz)) } } fun setViewAllOnClick(func: () -> Unit) { viewAll.onClick { func() } } fun setAdapter(adapter: UpdateableAdapter<*>) { doOnUIGlobal { recyclerView.adapter = adapter.also { this.adapter = it } } } fun updateList(list: List) { doOnUIGlobal { if (showError) errorTV.visibility = if (list.isEmpty()) VISIBLE else GONE else visibility = if (list.isNotEmpty() && !isHidden) VISIBLE else GONE progress.visibility = GONE adapter?.updateList(list) } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/ListPreferenceDialogFragmentCompat.kt ================================================ package knf.kuma.custom import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.preference.ListPreference import androidx.preference.PreferenceDialogFragmentCompat import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.afollestad.materialdialogs.list.listItemsSingleChoice class ListPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() { internal /* synthetic access */ var mClickedDialogEntryIndex: Int = 0 private var mEntries: Array? = null private var mEntryValues: Array? = null private val listPreference: ListPreference get() = preference as ListPreference override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { val preference = listPreference if (preference.entries == null || preference.entryValues == null) { throw IllegalStateException( "ListPreference requires an entries array and an entryValues array.") } mClickedDialogEntryIndex = preference.findIndexOfValue(preference.value) mEntries = preference.entries mEntryValues = preference.entryValues } else { mClickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0) mEntries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES) mEntryValues = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putInt(SAVE_STATE_INDEX, mClickedDialogEntryIndex) outState.putCharSequenceArray(SAVE_STATE_ENTRIES, mEntries) outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues) } override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { super.onPrepareDialogBuilder(builder) builder.setSingleChoiceItems( mEntries, mClickedDialogEntryIndex ) { dialog, which -> mClickedDialogEntryIndex = which // Clicking on an item simulates the positive button click, and dismisses // the dialog. this@ListPreferenceDialogFragmentCompat.onClick( dialog, DialogInterface.BUTTON_POSITIVE ) dialog.dismiss() } // The typical interaction for list-based dialogs is to have click-on-an-item dismiss the // dialog instead of the user having to press 'Ok'. builder.setPositiveButton(null, null) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return activity?.let { try { val entryList = mutableListOf() mEntries?.forEach { entry -> entryList.add(entry.toString()) } MaterialDialog(it).apply { lifecycleOwner() title(text = preference.dialogTitle.toString()) listItemsSingleChoice(items = entryList, initialSelection = mClickedDialogEntryIndex, waitForPositiveButton = false) { dialog, index, _ -> mClickedDialogEntryIndex = index this@ListPreferenceDialogFragmentCompat.onClick(dialog, DialogInterface.BUTTON_POSITIVE) dialog.dismiss() } positiveButton(android.R.string.ok) } } catch (e: Exception) { super.onCreateDialog(savedInstanceState) } } ?: super.onCreateDialog(savedInstanceState) } override fun onDialogClosed(positiveResult: Boolean) { if (positiveResult && mClickedDialogEntryIndex >= 0) { val value = mEntryValues!![mClickedDialogEntryIndex].toString() val preference = listPreference if (preference.callChangeListener(value)) { preference.value = value } } } companion object { private const val SAVE_STATE_INDEX = "ListPreferenceDialogFragment.index" private const val SAVE_STATE_ENTRIES = "ListPreferenceDialogFragment.entries" private const val SAVE_STATE_ENTRY_VALUES = "ListPreferenceDialogFragment.entryValues" fun newInstance(key: String): ListPreferenceDialogFragmentCompat { val fragment = ListPreferenceDialogFragmentCompat() val b = Bundle(1) b.putString(ARG_KEY, key) fragment.arguments = b return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/MainExecutor.kt ================================================ package knf.kuma.custom import android.os.Handler import android.os.Looper import java.util.concurrent.Executor class MainExecutor : Executor { override fun execute(command: Runnable?) { command ?: return Handler(Looper.getMainLooper()).post(command) } } ================================================ FILE: app/src/main/java/knf/kuma/custom/PreferenceFragmentCompat.java ================================================ package knf.kuma.custom; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.XmlRes; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.preference.DialogPreference; import androidx.preference.EditTextPreference; import androidx.preference.EditTextPreferenceDialogFragmentCompat; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroupAdapter; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceRecyclerViewAccessibilityDelegate; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import androidx.recyclerview.widget.RecyclerView; import knf.kuma.R; /** * Shows a hierarchy of {@link Preference} objects as * lists. These preferences will * automatically save to {@link android.content.SharedPreferences} as the user interacts with * them. To retrieve an instance of {@link android.content.SharedPreferences} that the * preference hierarchy in this fragment will use, call * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} * with a context in the same package as this fragment. *

* Furthermore, the preferences shown will follow the visual style of system * preferences. It is easy to create a hierarchy of preferences (that can be * shown on multiple screens) via XML. For these reasons, it is recommended to * use this fragment (as a superclass) to deal with preferences in applications. *

* A {@link PreferenceScreen} object should be at the top of the preference * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy * denote a screen break--that is the preferences contained within subsequent * {@link PreferenceScreen} should be shown on another screen. The preference * framework handles this by calling {@link #onNavigateToScreen(PreferenceScreen)}. *

* The preference hierarchy can be formed in multiple ways: *

  • From an XML file specifying the hierarchy *
  • From different {@link android.app.Activity Activities} that each specify its own * preferences in an XML file via {@link android.app.Activity} meta-data *
  • From an object hierarchy rooted with {@link PreferenceScreen} *

    * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The * root element should be a {@link PreferenceScreen}. Subsequent elements can point * to actual {@link Preference} subclasses. As mentioned above, subsequent * {@link PreferenceScreen} in the hierarchy will result in the screen break. *

    * To specify an object hierarchy rooted with {@link PreferenceScreen}, use * {@link #setPreferenceScreen(PreferenceScreen)}. *

    * As a convenience, this fragment implements a click listener for any * preference in the current hierarchy, see * {@link #onPreferenceTreeClick(Preference)}. * *

    *

    Developer Guides

    *

    For information about using {@code PreferenceFragment}, * read the Settings * guide.

    *
    * * *

    Sample Code

    * *

    The following sample code shows a simple preference fragment that is * populated from a resource. The resource it loads is:

    *

    * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences} * *

    The fragment implementation itself simply populates the preferences * when created. Note that the preferences framework takes care of loading * the current values out of the app preferences and writing them when changed:

    *

    * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCompat.java * support_fragment_compat} * * @see Preference * @see PreferenceScreen */ @SuppressLint("RestrictedApi") public abstract class PreferenceFragmentCompat extends Fragment implements PreferenceManager.OnPreferenceTreeClickListener, PreferenceManager.OnDisplayPreferenceDialogListener, PreferenceManager.OnNavigateToScreenListener, DialogPreference.TargetFragment { /** * Fragment argument used to specify the tag of the desired root * {@link androidx.preference.PreferenceScreen} object. */ public static final String ARG_PREFERENCE_ROOT = "androidx.preference.PreferenceFragmentCompat.PREFERENCE_ROOT"; private static final String PREFERENCES_TAG = "android:preferences"; private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; private static final int MSG_BIND_PREFERENCES = 1; private final DividerDecoration mDividerDecoration = new DividerDecoration(); @SuppressWarnings("WeakerAccess") /* synthetic access */ RecyclerView mList; final private Runnable mRequestFocus = new Runnable() { @Override public void run() { mList.focusableViewAvailable(mList); } }; private PreferenceManager mPreferenceManager; private boolean mHavePrefs; private boolean mInitDone; private Context mStyledContext; private int mLayoutResId = R.layout.preference_list_fragment; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_BIND_PREFERENCES: bindPreferences(); break; } } }; private Runnable mSelectPreferenceRunnable; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final TypedValue tv = new TypedValue(); getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true); int theme = tv.resourceId; if (theme == 0) { // Fallback to default theme. theme = R.style.PreferenceThemeOverlay; } mStyledContext = new ContextThemeWrapper(getActivity(), theme); mPreferenceManager = new PreferenceManager(mStyledContext); mPreferenceManager.setOnNavigateToScreenListener(this); final Bundle args = getArguments(); final String rootKey; if (args != null) { rootKey = getArguments().getString(ARG_PREFERENCE_ROOT); } else { rootKey = null; } onCreatePreferences(savedInstanceState, rootKey); } /** * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment. * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either * directly or via helper methods such as {@link #addPreferencesFromResource(int)}. * * @param savedInstanceState If the fragment is being re-created from * a previous saved state, this is the state. * @param rootKey If non-null, this preference fragment should be rooted at the * {@link androidx.preference.PreferenceScreen} with this key. */ public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { TypedArray a = mStyledContext.obtainStyledAttributes(null, R.styleable.PreferenceFragmentCompat, R.attr.preferenceFragmentCompatStyle, 0); mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_android_layout, mLayoutResId); final Drawable divider = a.getDrawable( R.styleable.PreferenceFragmentCompat_android_divider); final int dividerHeight = a.getDimensionPixelSize( R.styleable.PreferenceFragmentCompat_android_dividerHeight, -1); final boolean allowDividerAfterLastItem = a.getBoolean( R.styleable.PreferenceFragmentCompat_allowDividerAfterLastItem, true); a.recycle(); final LayoutInflater themedInflater = inflater.cloneInContext(mStyledContext); final View view = themedInflater.inflate(mLayoutResId, container, false); final View rawListContainer = view.findViewById(android.R.id.list_container); if (!(rawListContainer instanceof ViewGroup listContainer)) { throw new RuntimeException("Content has view with id attribute " + "'android.R.id.list_container' that is not a ViewGroup class"); } final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer, savedInstanceState); if (listView == null) { throw new RuntimeException("Could not create RecyclerView"); } mList = listView; listView.addItemDecoration(mDividerDecoration); setDivider(divider); if (dividerHeight != -1) { setDividerHeight(dividerHeight); } mDividerDecoration.setAllowDividerAfterLastItem(allowDividerAfterLastItem); // If mList isn't present in the view hierarchy, add it. mList is automatically inflated // on an Auto device so don't need to add it. if (mList.getParent() == null) { listContainer.addView(mList); } mHandler.post(mRequestFocus); return view; } /** * Sets the drawable that will be drawn between each item in the list. *

    * Note: If the drawable does not have an intrinsic * height, you should also call {@link #setDividerHeight(int)}. * * @param divider the drawable to use * @attr ref R.styleable#PreferenceFragmentCompat_android_divider */ public void setDivider(Drawable divider) { mDividerDecoration.setDivider(divider); } /** * Sets the height of the divider that will be drawn between each item in the list. Calling * this will override the intrinsic height as set by {@link #setDivider(Drawable)} * * @param height The new height of the divider in pixels. * @attr ref R.styleable#PreferenceFragmentCompat_android_dividerHeight */ public void setDividerHeight(int height) { mDividerDecoration.setDividerHeight(height); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (savedInstanceState != null) { Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG); if (container != null) { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { preferenceScreen.restoreHierarchyState(container); } } } if (mHavePrefs) { bindPreferences(); if (mSelectPreferenceRunnable != null) { mSelectPreferenceRunnable.run(); mSelectPreferenceRunnable = null; } } mInitDone = true; } @Override public void onStart() { super.onStart(); mPreferenceManager.setOnPreferenceTreeClickListener(this); mPreferenceManager.setOnDisplayPreferenceDialogListener(this); } @Override public void onStop() { super.onStop(); mPreferenceManager.setOnPreferenceTreeClickListener(null); mPreferenceManager.setOnDisplayPreferenceDialogListener(null); } @Override public void onDestroyView() { mHandler.removeCallbacks(mRequestFocus); mHandler.removeMessages(MSG_BIND_PREFERENCES); if (mHavePrefs) { unbindPreferences(); } mList = null; super.onDestroyView(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { Bundle container = new Bundle(); preferenceScreen.saveHierarchyState(container); outState.putBundle(PREFERENCES_TAG, container); } } /** * Returns the {@link PreferenceManager} used by this fragment. * * @return The {@link PreferenceManager}. */ public PreferenceManager getPreferenceManager() { return mPreferenceManager; } /** * Gets the root of the preference hierarchy that this fragment is showing. * * @return The {@link PreferenceScreen} that is the root of the preference * hierarchy. */ public PreferenceScreen getPreferenceScreen() { return mPreferenceManager.getPreferenceScreen(); } /** * Sets the root of the preference hierarchy that this fragment is showing. * * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. */ public void setPreferenceScreen(PreferenceScreen preferenceScreen) { if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { onUnbindPreferences(); mHavePrefs = true; if (mInitDone) { postBindPreferences(); } } } /** * Inflates the given XML resource and adds the preference hierarchy to the current * preference hierarchy. * * @param preferencesResId The XML resource ID to inflate. */ public void addPreferencesFromResource(@XmlRes int preferencesResId) { requirePreferenceManager(); setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext, preferencesResId, getPreferenceScreen())); } /** * Inflates the given XML resource and replaces the current preference hierarchy (if any) with * the preference hierarchy rooted at {@code key}. * * @param preferencesResId The XML resource ID to inflate. * @param key The preference key of the {@link androidx.preference.PreferenceScreen} * to use as the root of the preference hierarchy, or null to use the root * {@link androidx.preference.PreferenceScreen}. */ public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) { requirePreferenceManager(); final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(mStyledContext, preferencesResId, null); final Preference root; if (key != null) { root = xmlRoot.findPreference(key); if (!(root instanceof PreferenceScreen)) { throw new IllegalArgumentException("Preference object with key " + key + " is not a PreferenceScreen"); } } else { root = xmlRoot; } setPreferenceScreen((PreferenceScreen) root); } /** * {@inheritDoc} */ @Override public boolean onPreferenceTreeClick(Preference preference) { if (preference.getFragment() != null) { boolean handled = false; if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) { handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment()) .onPreferenceStartFragment(this, preference); } if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) { handled = ((OnPreferenceStartFragmentCallback) getActivity()) .onPreferenceStartFragment(this, preference); } return handled; } return false; } @Override public void onNavigateToScreen(PreferenceScreen preferenceScreen) { boolean handled = false; if (getCallbackFragment() instanceof OnPreferenceStartScreenCallback) { handled = ((OnPreferenceStartScreenCallback) getCallbackFragment()) .onPreferenceStartScreen(this, preferenceScreen); } if (!handled && getActivity() instanceof OnPreferenceStartScreenCallback) { ((OnPreferenceStartScreenCallback) getActivity()) .onPreferenceStartScreen(this, preferenceScreen); } } @Override public Preference findPreference(CharSequence key) { if (mPreferenceManager == null) { return null; } return mPreferenceManager.findPreference(key); } private void requirePreferenceManager() { if (mPreferenceManager == null) { throw new RuntimeException("This should be called after super.onCreate."); } } private void postBindPreferences() { if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); } @SuppressWarnings("WeakerAccess") /* synthetic access */ void bindPreferences() { try { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { getListView().setAdapter(onCreateAdapter(preferenceScreen)); preferenceScreen.onAttached(); } onBindPreferences(); } catch (Exception e) { e.printStackTrace(); } } private void unbindPreferences() { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { preferenceScreen.onDetached(); } onUnbindPreferences(); } /** * @hide */ @RestrictTo(LIBRARY_GROUP) protected void onBindPreferences() { } /** * @hide */ @RestrictTo(LIBRARY_GROUP) protected void onUnbindPreferences() { } public final RecyclerView getListView() { return mList; } /** * Creates the {@link RecyclerView} used to display the preferences. * Subclasses may override this to return a customized * {@link RecyclerView}. * * @param inflater The LayoutInflater object that can be used to inflate the * {@link RecyclerView}. * @param parent The parent {@link android.view.View} that the RecyclerView will be attached to. * This method should not add the view itself, but this can be used to generate * the LayoutParams of the view. * @param savedInstanceState If non-null, this view is being re-constructed from a previous * saved state as given here * @return A new RecyclerView object to be placed into the view hierarchy */ @SuppressLint("RestrictedApi") public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { // If device detected is Auto, use Auto's custom layout that contains a custom ViewGroup // wrapping a RecyclerView if (mStyledContext.getPackageManager().hasSystemFeature(PackageManager .FEATURE_AUTOMOTIVE)) { RecyclerView recyclerView = parent.findViewById(R.id.recycler_view); if (recyclerView != null) { return recyclerView; } } RecyclerView recyclerView = (RecyclerView) inflater .inflate(R.layout.preference_recyclerview, parent, false); recyclerView.setLayoutManager(onCreateLayoutManager()); recyclerView.setAccessibilityDelegateCompat( new PreferenceRecyclerViewAccessibilityDelegate(recyclerView)); return recyclerView; } /** * Called from {@link #onCreateRecyclerView} to create the * {@link RecyclerView.LayoutManager} for the created * {@link RecyclerView}. * * @return A new {@link RecyclerView.LayoutManager} instance. */ public RecyclerView.LayoutManager onCreateLayoutManager() { return new VariantLinearLayoutManager(getActivity()); } /** * Creates the root adapter. * * @param preferenceScreen Preference screen object to create the adapter for. * @return An adapter that contains the preferences contained in this {@link PreferenceScreen}. */ protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { return new PreferenceGroupAdapter(preferenceScreen); } /** * Called when a preference in the tree requests to display a dialog. Subclasses should * override this method to display custom dialogs or to handle dialogs for custom preference * classes. * * @param preference The Preference object requesting the dialog. */ @Override public void onDisplayPreferenceDialog(Preference preference) { boolean handled = false; if (getCallbackFragment() instanceof OnPreferenceDisplayDialogCallback) { handled = ((OnPreferenceDisplayDialogCallback) getCallbackFragment()) .onPreferenceDisplayDialog(this, preference); } if (!handled && getActivity() instanceof OnPreferenceDisplayDialogCallback) { handled = ((OnPreferenceDisplayDialogCallback) getActivity()) .onPreferenceDisplayDialog(this, preference); } if (handled) { return; } // check if dialog is already showing if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) { return; } final DialogFragment f; if (preference instanceof EditTextPreference) { f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey()); } else if (preference instanceof ListPreference) { f = ListPreferenceDialogFragmentCompat.Companion.newInstance(preference.getKey()); } else { throw new IllegalArgumentException("Tried to display dialog for unknown " + "preference type. Did you forget to override onDisplayPreferenceDialog()?"); } f.setTargetFragment(this, 0); f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); } /** * Basically a wrapper for getParentFragment which is v17+. Used by the leanback preference lib. * * @return Fragment to possibly use as a callback * @hide */ @RestrictTo(LIBRARY_GROUP) public Fragment getCallbackFragment() { return null; } public void scrollToPreference(final String key) { scrollToPreferenceInternal(null, key); } public void scrollToPreference(final Preference preference) { scrollToPreferenceInternal(preference, null); } private void scrollToPreferenceInternal(final Preference preference, final String key) { final Runnable r = new Runnable() { @Override public void run() { final RecyclerView.Adapter adapter = mList.getAdapter(); if (!(adapter instanceof PreferenceGroup.PreferencePositionCallback)) { if (adapter != null) { throw new IllegalStateException("Adapter must implement " + "PreferencePositionCallback"); } else { // Adapter was set to null, so don't scroll I guess? return; } } final int position; if (preference != null) { position = ((PreferenceGroup.PreferencePositionCallback) adapter) .getPreferenceAdapterPosition(preference); } else { position = ((PreferenceGroup.PreferencePositionCallback) adapter) .getPreferenceAdapterPosition(key); } if (position != RecyclerView.NO_POSITION) { mList.scrollToPosition(position); } else { // Item not found, wait for an update and try again adapter.registerAdapterDataObserver( new ScrollToPreferenceObserver(adapter, mList, preference, key)); } } }; if (mList == null) { mSelectPreferenceRunnable = r; } else { r.run(); } } /** * Interface that PreferenceFragment's containing activity should * implement to be able to process preference items that wish to * switch to a specified fragment. */ public interface OnPreferenceStartFragmentCallback { /** * Called when the user has clicked on a Preference that has * a fragment class name associated with it. The implementation * should instantiate and switch to an instance of the given * fragment. * * @param caller The fragment requesting navigation. * @param pref The preference requesting the fragment. * @return true if the fragment creation has been handled */ boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref); } /** * Interface that PreferenceFragment's containing activity should * implement to be able to process preference items that wish to * switch to a new screen of preferences. */ public interface OnPreferenceStartScreenCallback { /** * Called when the user has clicked on a PreferenceScreen item in order to navigate to a new * screen of preferences. * * @param caller The fragment requesting navigation. * @param pref The preference screen to navigate to. * @return true if the screen navigation has been handled */ boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref); } public interface OnPreferenceDisplayDialogCallback { /** * @param caller The fragment containing the preference requesting the dialog. * @param pref The preference requesting the dialog. * @return true if the dialog creation has been handled. */ boolean onPreferenceDisplayDialog(@NonNull PreferenceFragmentCompat caller, Preference pref); } private static class ScrollToPreferenceObserver extends RecyclerView.AdapterDataObserver { private final RecyclerView.Adapter mAdapter; private final RecyclerView mList; private final Preference mPreference; private final String mKey; public ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list, Preference preference, String key) { mAdapter = adapter; mList = list; mPreference = preference; mKey = key; } private void scrollToPreference() { mAdapter.unregisterAdapterDataObserver(this); final int position; if (mPreference != null) { position = ((PreferenceGroup.PreferencePositionCallback) mAdapter) .getPreferenceAdapterPosition(mPreference); } else { position = ((PreferenceGroup.PreferencePositionCallback) mAdapter) .getPreferenceAdapterPosition(mKey); } if (position != RecyclerView.NO_POSITION) { mList.scrollToPosition(position); } } @Override public void onChanged() { scrollToPreference(); } @Override public void onItemRangeChanged(int positionStart, int itemCount) { scrollToPreference(); } @Override public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { scrollToPreference(); } @Override public void onItemRangeInserted(int positionStart, int itemCount) { scrollToPreference(); } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { scrollToPreference(); } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { scrollToPreference(); } } private class DividerDecoration extends RecyclerView.ItemDecoration { private Drawable mDivider; private int mDividerHeight; private boolean mAllowDividerAfterLastItem = true; DividerDecoration() { } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { if (mDivider == null) { return; } final int childCount = parent.getChildCount(); final int width = parent.getWidth(); for (int childViewIndex = 0; childViewIndex < childCount; childViewIndex++) { final View view = parent.getChildAt(childViewIndex); if (shouldDrawDividerBelow(view, parent)) { int top = (int) view.getY() + view.getHeight(); mDivider.setBounds(0, top, width, top + mDividerHeight); mDivider.draw(c); } } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { if (shouldDrawDividerBelow(view, parent)) { outRect.bottom = mDividerHeight; } } private boolean shouldDrawDividerBelow(View view, RecyclerView parent) { final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view); final boolean dividerAllowedBelow = holder instanceof PreferenceViewHolder && ((PreferenceViewHolder) holder).isDividerAllowedBelow(); if (!dividerAllowedBelow) { return false; } boolean nextAllowed = mAllowDividerAfterLastItem; int index = parent.indexOfChild(view); if (index < parent.getChildCount() - 1) { final View nextView = parent.getChildAt(index + 1); final RecyclerView.ViewHolder nextHolder = parent.getChildViewHolder(nextView); nextAllowed = nextHolder instanceof PreferenceViewHolder && ((PreferenceViewHolder) nextHolder).isDividerAllowedAbove(); } return nextAllowed; } public void setDivider(Drawable divider) { if (divider != null) { mDividerHeight = divider.getIntrinsicHeight(); } else { mDividerHeight = 0; } mDivider = divider; mList.invalidateItemDecorations(); } public void setDividerHeight(int dividerHeight) { mDividerHeight = dividerHeight; mList.invalidateItemDecorations(); } public void setAllowDividerAfterLastItem(boolean allowDividerAfterLastItem) { mAllowDividerAfterLastItem = allowDividerAfterLastItem; } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/SSLManager.kt ================================================ package knf.kuma.custom import knf.kuma.commons.isMIUI import knf.kuma.commons.noCrash import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager object SSLManager { fun disable() { val trustAllCerts = arrayOf(object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array, authType: String) { noCrash(false) { chain.forEach { it.checkValidity() } } } @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array, authType: String) { noCrash(false) { chain.forEach { it.checkValidity() } } } override fun getAcceptedIssuers(): Array { return arrayOf() } }) try { val sslContext = SSLContext.getInstance("SSL").apply { init(null, trustAllCerts, SecureRandom()) } val factory = if (isMIUI) { TlsOnlySocketFactory(sslContext.socketFactory) } else { sslContext.socketFactory } HttpsURLConnection.setDefaultSSLSocketFactory(factory) } catch (e: Exception) { e.printStackTrace() } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/SeenAnimeOverlay.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.LinearLayout import knf.kuma.R class SeenAnimeOverlay : LinearLayout { constructor(context: Context) : super(context) { inflate(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { inflate(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { inflate(context) } private fun inflate(context: Context) { val inflater = context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater inflater.inflate(R.layout.view_seen_overlay, this) } fun setSeen(seen: Boolean, animate: Boolean) { setState(seen) if (animate) { post { val animation = AnimationUtils.loadAnimation(context, if (seen) R.anim.fadein else R.anim.fadeout) animation.duration = 200 animation.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation) { } override fun onAnimationEnd(animation: Animation) { } override fun onAnimationRepeat(animation: Animation) { } }) startAnimation(animation) } } } private fun setState(seen: Boolean) { post { visibility = if (seen) VISIBLE else GONE } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/SingleFragmentActivity.kt ================================================ package knf.kuma.custom import android.os.Bundle import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import knf.kuma.R import knf.kuma.commons.EAHelper import org.jetbrains.anko.find abstract class SingleFragmentActivity : AppCompatActivity() { private val layoutResId: Int @LayoutRes get() = R.layout.activity_fragment protected abstract fun createFragment(): Fragment abstract fun getActivityTitle(): String override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(layoutResId) with(find(R.id.toolbar)) { setSupportActionBar(this) setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } } supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.title = getActivityTitle() val fm = supportFragmentManager var fragment = fm.findFragmentById(R.id.fragment_container) if (fragment == null) { fragment = createFragment() fm.beginTransaction() .add(R.id.fragment_container, fragment) .commit() } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/SingleFragmentMaterialActivity.kt ================================================ package knf.kuma.custom import android.os.Bundle import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import knf.kuma.R import knf.kuma.commons.EAHelper import knf.kuma.commons.setSurfaceBars import org.jetbrains.anko.find abstract class SingleFragmentMaterialActivity : AppCompatActivity() { private val layoutResId: Int @LayoutRes get() = R.layout.activity_fragment_material protected abstract fun createFragment(): Fragment abstract fun getActivityTitle(): String override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(layoutResId) with(find(R.id.toolbar)) { setSupportActionBar(this) setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } } supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.title = getActivityTitle() val fm = supportFragmentManager var fragment = fm.findFragmentById(R.id.fragment_container) if (fragment == null) { fragment = createFragment() fm.beginTransaction() .add(R.id.fragment_container, fragment) .commit() } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/StateView.kt ================================================ package knf.kuma.custom import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import knf.kuma.R import knf.kuma.commons.doOnUIGlobal import org.jetbrains.anko.find import org.jetbrains.anko.textColor class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : LinearLayout(context, attrs, defStyle) { private var titleText = "PlaceHolder" private var isSetted = false init { attrs?.let { val array = context.obtainStyledAttributes(it, R.styleable.StateView) titleText = array.getString(R.styleable.StateView_sv_title) ?: titleText array.recycle() } inflate(context, R.layout.layout_loading_text, this) } override fun onFinishInflate() { super.onFinishInflate() find(R.id.title).text = titleText } fun load(contentText: String, state: Int = STATE_NORMAL) { val shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() doOnUIGlobal { visibility = VISIBLE val textView = find(R.id.text) val loading = find(R.id.loading) when (state) { STATE_OK -> textView.textColor = ContextCompat.getColor(context, R.color.stateOk) STATE_WARNING -> textView.textColor = ContextCompat.getColor(context, R.color.stateWarning) STATE_ERROR -> textView.textColor = ContextCompat.getColor(context, R.color.stateError) } textView.apply { text = contentText if (!isSetted) { alpha = 0f visibility = VISIBLE animate() .alpha(1f) .setDuration(shortAnimationDuration) .setListener(null) } } if (!isSetted) loading.animate() .alpha(0f) .setDuration(shortAnimationDuration) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { loading.visibility = GONE } }) isSetted = true } } companion object { const val STATE_NORMAL = 0 const val STATE_OK = 1 const val STATE_WARNING = 2 const val STATE_ERROR = 3 } } ================================================ FILE: app/src/main/java/knf/kuma/custom/StateViewMaterial.kt ================================================ package knf.kuma.custom import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes import knf.kuma.R import knf.kuma.commons.doOnUIGlobal import org.jetbrains.anko.find import org.jetbrains.anko.textColor class StateViewMaterial @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : LinearLayout(context, attrs, defStyle) { private var titleText = "PlaceHolder" private var isSetted = false init { attrs?.let { context.withStyledAttributes(it, R.styleable.StateViewMaterial) { titleText = getString(R.styleable.StateViewMaterial_svm_title) ?: titleText } } inflate(context, R.layout.layout_loading_text_material, this) } override fun onFinishInflate() { super.onFinishInflate() find(R.id.title).text = titleText } fun load(contentText: String, state: Int = STATE_NORMAL) { val shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() doOnUIGlobal { visibility = VISIBLE val textView = find(R.id.text) val loading = find(R.id.loading) when (state) { STATE_OK -> textView.textColor = ContextCompat.getColor(context, R.color.stateOk) STATE_WARNING -> textView.textColor = ContextCompat.getColor(context, R.color.stateWarning) STATE_ERROR -> textView.textColor = ContextCompat.getColor(context, R.color.stateError) } textView.apply { text = contentText if (!isSetted) { alpha = 0f visibility = VISIBLE animate() .alpha(1f) .setDuration(shortAnimationDuration) .setListener(null) } } if (!isSetted) loading.animate() .alpha(0f) .setDuration(shortAnimationDuration) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { loading.visibility = GONE } }) isSetted = true } } companion object { const val STATE_NORMAL = 0 const val STATE_OK = 1 const val STATE_WARNING = 2 const val STATE_ERROR = 3 } } ================================================ FILE: app/src/main/java/knf/kuma/custom/SyncItemView.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.RelativeLayout import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.backup.Backups import knf.kuma.backup.framework.BackupService import knf.kuma.backup.objects.BackupObject import knf.kuma.commons.Network import knf.kuma.commons.noCrash import knf.kuma.databinding.SyncItemLayoutBinding import org.jetbrains.anko.sdk27.coroutines.onLongClick import xdroid.toaster.Toaster class SyncItemView : RelativeLayout { private var cardTitle: String? = "Error" private var showDivider = true private var hideBackup = false private var actionId: String = "neutral" var backupObj: BackupObject<*>? = null private set constructor(context: Context) : super(context) { inflate(context) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { inflate(context) setDefaults(context, attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { inflate(context) setDefaults(context, attrs) } private lateinit var binding: SyncItemLayoutBinding private fun inflate(context: Context) { val inflater = context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater inflater.inflate(R.layout.sync_item_layout, this) binding = SyncItemLayoutBinding.bind(this) } private fun setDefaults(context: Context, attrs: AttributeSet) { val array = context.obtainStyledAttributes(attrs, R.styleable.SyncItemView) cardTitle = array.getString(R.styleable.SyncItemView_si_title) showDivider = array.getBoolean(R.styleable.SyncItemView_si_showDivider, true) hideBackup = array.getBoolean(R.styleable.SyncItemView_si_hideBackup, false) actionId = array.getString(R.styleable.SyncItemView_si_actionId) ?: "neutral" array.recycle() } override fun onFinishInflate() { super.onFinishInflate() binding.title.text = cardTitle if (!showDivider) binding.separator.visibility = GONE if (hideBackup) binding.backup.isEnabled = false } fun enableBackup(backupObject: BackupObject<*>?, onClick: OnClick) { post { noCrash { if (Network.isConnected) { if (!hideBackup) binding.backup.isEnabled = true if (backupObject == null) binding.date.text = "Sin respaldo" else { binding.date.text = backupObject.date binding.restore.isEnabled = true } binding.backup.onLongClick(returnValue = true) { Toaster.toast("Respaldar a la nube") } binding.backup.setOnClickListener { noCrash { onClick.onAction(this@SyncItemView, actionId, true) AchievementManager.onBackup() } } binding.restore.onLongClick(returnValue = true) { Toaster.toast("Restaurar desde la nube") } binding.restore.setOnClickListener { noCrash { onClick.onAction(this@SyncItemView, actionId, false) } } } else { binding.date.text = "Sin internet" } } } } fun clear() { backupObj = null post { binding.backup.isEnabled = false binding.restore.isEnabled = false binding.date.text = "Cargando..." } } fun init(service: BackupService?, onClick: OnClick) { Backups.search(service, actionId) { backupObj = it enableBackup(backupObj, onClick) } } interface OnClick { fun onAction(syncItemView: SyncItemView, id: String, isBackup: Boolean) } } ================================================ FILE: app/src/main/java/knf/kuma/custom/SyncStaticItemView.kt ================================================ package knf.kuma.custom import android.content.Context import android.graphics.drawable.AnimatedVectorDrawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.animation.Animation import android.view.animation.LinearInterpolator import android.view.animation.RotateAnimation import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.github.marlonlom.utilities.timeago.TimeAgo import knf.kuma.R import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.databinding.ViewSyncFirestoreBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.defaultSharedPreferences class SyncStaticItemView : RelativeLayout { private var cardTitle: String? = "Error" private var showDivider = true private var prefId: String = "neutral" private lateinit var lastState: FirestoreManager.State private var rotateAnimation: RotateAnimation? = null private var isRotating = false private var stopRotating = false constructor(context: Context) : super(context) { inflate(context) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { inflate(context) setDefaults(context, attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { inflate(context) setDefaults(context, attrs) } private lateinit var binding: ViewSyncFirestoreBinding private fun inflate(context: Context) { val inflater = context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater binding = ViewSyncFirestoreBinding.inflate(inflater, this, true) } private fun setDefaults(context: Context, attrs: AttributeSet) { val array = context.obtainStyledAttributes(attrs, R.styleable.SyncStaticItemView) cardTitle = array.getString(R.styleable.SyncStaticItemView_ssi_title) showDivider = array.getBoolean(R.styleable.SyncStaticItemView_ssi_showDivider, true) prefId = array.getString(R.styleable.SyncStaticItemView_ssi_prefId) ?: "neutral" array.recycle() } fun suscribe(owner: LifecycleOwner, liveData: LiveData) { liveData.observe(owner, Observer { when (it) { FirestoreManager.State.IDLE -> stateOk(it) FirestoreManager.State.UPLOAD, FirestoreManager.State.SYNC -> stateSync(it) else -> stateOk(FirestoreManager.State.IDLE) } }) } private fun stateOk(state: FirestoreManager.State) { GlobalScope.launch(Dispatchers.Main) { if (!::lastState.isInitialized) { lastState = state binding.indicator.setImageResource(R.drawable.ic_check_bold) binding.stateText.text = "Última sincronización: ${timeAgo()}" return@launch } else if (lastState != FirestoreManager.State.IDLE) { lastState = state stopRotating = true while (isRotating) { delay(100) } val transform = ContextCompat.getDrawable(context, R.drawable.anim_sync_check) as? AnimatedVectorDrawable binding.indicator.setImageDrawable(transform) transform?.start() delay(600) binding.stateText.text = "Última sincronización: ${timeAgo()}" binding.indicator.setImageResource(R.drawable.ic_check_bold) } } } private fun timeAgo() = context.defaultSharedPreferences.getLong(prefId, -1L).let { if (it == -1L) "Sin registros" else TimeAgo.using(it) } private fun stateSync(state: FirestoreManager.State) { GlobalScope.launch(Dispatchers.Main) { if (!::lastState.isInitialized) { lastState = state binding.stateText.text = "Sincronizando..." binding.indicator.setImageResource(R.drawable.ic_sync_rotate) } else if (lastState == FirestoreManager.State.IDLE) { lastState = state val transform = ContextCompat.getDrawable(context, R.drawable.anim_check_sync) as? AnimatedVectorDrawable binding.indicator.setImageDrawable(transform) transform?.start() delay(600) binding.stateText.text = "Sincronizando..." binding.indicator.setImageResource(R.drawable.ic_sync_rotate) } rotateAnimation = RotateAnimation(180f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f).apply { duration = 500 interpolator = LinearInterpolator() repeatCount = Animation.INFINITE setAnimationListener(object : Animation.AnimationListener { override fun onAnimationRepeat(p0: Animation?) { if (stopRotating) { p0?.repeatCount = 1 stopRotating = false } } override fun onAnimationEnd(p0: Animation?) { isRotating = false } override fun onAnimationStart(p0: Animation?) { } }) }.also { isRotating = true binding.indicator.startAnimation(it) } } } override fun onFinishInflate() { super.onFinishInflate() binding.title.text = cardTitle if (!showDivider) binding.separator.visibility = GONE } } ================================================ FILE: app/src/main/java/knf/kuma/custom/ThemedControlsActivity.kt ================================================ package knf.kuma.custom import android.os.Bundle import android.widget.RelativeLayout import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import es.munix.multidisplaycast.CastControlsActivity import knf.kuma.App import knf.kuma.ads.implBannerCast import knf.kuma.commons.BypassUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil class ThemedControlsActivity : CastControlsActivity() { override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) } override fun createImgUrl(url: String): GlideUrl { return GlideUrl(url, LazyHeaders.Builder().apply { addHeader("Cookie", BypassUtil.getStringCookie(App.context)) addHeader("User-Agent", BypassUtil.userAgent) }.build()) } override fun setUpAd(placeholder: RelativeLayout) { if (PrefsUtil.isAdsEnabled) placeholder.implBannerCast() } } ================================================ FILE: app/src/main/java/knf/kuma/custom/TlsOnlySocketFactory.java ================================================ /* * Copyright 2015 Bhavit Singh Sengar * Copyright 2015-2016 Hans-Christoph Steiner * Copyright 2015-2016 Nathan Freitas * * 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. * * From https://stackoverflow.com/a/29946540 */ package knf.kuma.custom; import android.net.SSLCertificateSocketFactory; import android.os.Build; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; /** * While making a secure connection, Android's {@link HttpsURLConnection} falls * back to SSLv3 from TLSv1. This is a bug in android versions < 4.4. It can be * fixed by removing the SSLv3 protocol from Enabled Protocols list. Use this as * the {@link SSLSocketFactory} for * {@link HttpsURLConnection#setDefaultSSLSocketFactory(SSLSocketFactory)} * * @author Bhavit S. Sengar * @author Hans-Christoph Steiner * @see SSLSocket table of protocols and ciphers Android supports * @see source of protocol name constants */ public class TlsOnlySocketFactory extends SSLSocketFactory { public static final String TLSV1_2 = "TLSv1.2"; public static final String TLSV1_1 = "TLSv1.1"; public static final String TLSV1 = "TLSv1"; public static final String SSLV3 = "SSLv3"; public static final String SSLV2 = "SSLv2"; private static final int HANDSHAKE_TIMEOUT = 0; private static final String TAG = "TlsOnlySocketFactory"; private final SSLSocketFactory delegate; private final boolean compatible; public TlsOnlySocketFactory() { this.delegate = SSLCertificateSocketFactory.getDefault(HANDSHAKE_TIMEOUT, null); this.compatible = false; } public TlsOnlySocketFactory(SSLSocketFactory delegate) { this.delegate = delegate; this.compatible = false; } /** * Make {@link SSLSocket}s that are compatible with outdated servers. * * @param delegate * @param compatible */ public TlsOnlySocketFactory(SSLSocketFactory delegate, boolean compatible) { this.delegate = delegate; this.compatible = compatible; } @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } /** * @see The sad state of server-side TLS Session Resumption implementations */ private Socket makeSocketSafe(Socket socket, String host) { if (socket instanceof SSLSocket) { TlsOnlySSLSocket tempSocket = new TlsOnlySSLSocket((SSLSocket) socket, compatible); if (delegate instanceof SSLCertificateSocketFactory factory && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { factory.setHostname(socket, host); factory.setUseSessionTickets(socket, false); } else { tempSocket.setHostname(host); } socket = tempSocket; } return socket; } @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return makeSocketSafe(delegate.createSocket(s, host, port, autoClose), host); } @Override public Socket createSocket(String host, int port) throws IOException { return makeSocketSafe(delegate.createSocket(host, port), host); } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort), host); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { return makeSocketSafe(delegate.createSocket(host, port), host.getHostName()); } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort), address.getHostName()); } private class TlsOnlySSLSocket extends DelegateSSLSocket { final boolean compatible; private TlsOnlySSLSocket(SSLSocket delegate, boolean compatible) { super(delegate); this.compatible = compatible; // badly configured servers can't handle a good config if (compatible) { ArrayList protocols = new ArrayList(Arrays.asList(delegate .getEnabledProtocols())); protocols.remove(SSLV2); protocols.remove(SSLV3); super.setEnabledProtocols(protocols.toArray(new String[protocols.size()])); /* * Exclude extremely weak EXPORT ciphers. NULL ciphers should * never even have been an option in TLS. */ ArrayList enabled = new ArrayList(10); Pattern exclude = Pattern.compile(".*(EXPORT|NULL).*"); for (String cipher : delegate.getEnabledCipherSuites()) { if (!exclude.matcher(cipher).matches()) { enabled.add(cipher); } } super.setEnabledCipherSuites(enabled.toArray(new String[enabled.size()])); return; } // else // 16-19 support v1.1 and v1.2 but only by default starting in 20+ // https://developer.android.com/reference/javax/net/ssl/SSLSocket.html ArrayList protocols = new ArrayList(Arrays.asList(delegate .getSupportedProtocols())); protocols.remove(SSLV2); protocols.remove(SSLV3); if (Build.VERSION.SDK_INT >= 24) { protocols.remove(TLSV1); protocols.remove(TLSV1_1); } super.setEnabledProtocols(protocols.toArray(new String[protocols.size()])); /* * Exclude weak ciphers, like EXPORT, MD5, DES, and DH. NULL ciphers * should never even have been an option in TLS. */ ArrayList enabledCiphers = new ArrayList(10); Pattern exclude = Pattern.compile(".*(_DES|DH_|DSS|EXPORT|MD5|NULL|RC4|TLS_FALLBACK_SCSV).*"); for (String cipher : delegate.getSupportedCipherSuites()) { if (!exclude.matcher(cipher).matches()) { enabledCiphers.add(cipher); } } super.setEnabledCipherSuites(enabledCiphers.toArray(new String[enabledCiphers.size()])); } /** * This works around a bug in Android < 19 where SSLv3 is forced */ @Override public void setEnabledProtocols(String[] protocols) { if (protocols != null && protocols.length == 1 && SSLV3.equals(protocols[0])) { List systemProtocols; if (this.compatible) { systemProtocols = Arrays.asList(delegate.getEnabledProtocols()); } else { systemProtocols = Arrays.asList(delegate.getSupportedProtocols()); } List enabledProtocols = new ArrayList(systemProtocols); if (enabledProtocols.size() > 1) { enabledProtocols.remove(SSLV2); enabledProtocols.remove(SSLV3); } else { Log.w(TAG, "SSL stuck with protocol available for " + enabledProtocols); } protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]); } super.setEnabledProtocols(protocols); } } public class DelegateSSLSocket extends SSLSocket { protected final SSLSocket delegate; DelegateSSLSocket(SSLSocket delegate) { this.delegate = delegate; } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public String[] getEnabledCipherSuites() { return delegate.getEnabledCipherSuites(); } @Override public void setEnabledCipherSuites(String[] suites) { delegate.setEnabledCipherSuites(suites); } @Override public String[] getSupportedProtocols() { return delegate.getSupportedProtocols(); } @Override public String[] getEnabledProtocols() { return delegate.getEnabledProtocols(); } @Override public void setEnabledProtocols(String[] protocols) { delegate.setEnabledProtocols(protocols); } @Override public SSLSession getSession() { return delegate.getSession(); } @Override public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { delegate.addHandshakeCompletedListener(listener); } @Override public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { delegate.removeHandshakeCompletedListener(listener); } @Override public void startHandshake() throws IOException { delegate.startHandshake(); } @Override public boolean getUseClientMode() { return delegate.getUseClientMode(); } @Override public void setUseClientMode(boolean mode) { delegate.setUseClientMode(mode); } @Override public boolean getNeedClientAuth() { return delegate.getNeedClientAuth(); } @Override public void setNeedClientAuth(boolean need) { delegate.setNeedClientAuth(need); } @Override public boolean getWantClientAuth() { return delegate.getWantClientAuth(); } @Override public void setWantClientAuth(boolean want) { delegate.setWantClientAuth(want); } @Override public boolean getEnableSessionCreation() { return delegate.getEnableSessionCreation(); } @Override public void setEnableSessionCreation(boolean flag) { delegate.setEnableSessionCreation(flag); } @Override public void bind(SocketAddress localAddr) throws IOException { delegate.bind(localAddr); } @Override public synchronized void close() throws IOException { delegate.close(); } @Override public void connect(SocketAddress remoteAddr) throws IOException { delegate.connect(remoteAddr); } @Override public void connect(SocketAddress remoteAddr, int timeout) throws IOException { delegate.connect(remoteAddr, timeout); } @Override public SocketChannel getChannel() { return delegate.getChannel(); } @Override public InetAddress getInetAddress() { return delegate.getInetAddress(); } @Override public InputStream getInputStream() throws IOException { return delegate.getInputStream(); } @Override public boolean getKeepAlive() throws SocketException { return delegate.getKeepAlive(); } @Override public void setKeepAlive(boolean keepAlive) throws SocketException { delegate.setKeepAlive(keepAlive); } @Override public InetAddress getLocalAddress() { return delegate.getLocalAddress(); } @Override public int getLocalPort() { return delegate.getLocalPort(); } @Override public SocketAddress getLocalSocketAddress() { return delegate.getLocalSocketAddress(); } @Override public boolean getOOBInline() throws SocketException { return delegate.getOOBInline(); } @Override public void setOOBInline(boolean oobinline) throws SocketException { delegate.setOOBInline(oobinline); } @Override public OutputStream getOutputStream() throws IOException { return delegate.getOutputStream(); } @Override public int getPort() { return delegate.getPort(); } @Override public synchronized int getReceiveBufferSize() throws SocketException { return delegate.getReceiveBufferSize(); } @Override public synchronized void setReceiveBufferSize(int size) throws SocketException { delegate.setReceiveBufferSize(size); } @Override public SocketAddress getRemoteSocketAddress() { return delegate.getRemoteSocketAddress(); } @Override public boolean getReuseAddress() throws SocketException { return delegate.getReuseAddress(); } @Override public void setReuseAddress(boolean reuse) throws SocketException { delegate.setReuseAddress(reuse); } @Override public synchronized int getSendBufferSize() throws SocketException { return delegate.getSendBufferSize(); } @Override public synchronized void setSendBufferSize(int size) throws SocketException { delegate.setSendBufferSize(size); } @Override public int getSoLinger() throws SocketException { return delegate.getSoLinger(); } @Override public synchronized int getSoTimeout() throws SocketException { return delegate.getSoTimeout(); } @Override public synchronized void setSoTimeout(int timeout) throws SocketException { delegate.setSoTimeout(timeout); } @Override public boolean getTcpNoDelay() throws SocketException { return delegate.getTcpNoDelay(); } @Override public void setTcpNoDelay(boolean on) throws SocketException { delegate.setTcpNoDelay(on); } @Override public int getTrafficClass() throws SocketException { return delegate.getTrafficClass(); } @Override public void setTrafficClass(int value) throws SocketException { delegate.setTrafficClass(value); } @Override public boolean isBound() { return delegate.isBound(); } @Override public boolean isClosed() { return delegate.isClosed(); } @Override public boolean isConnected() { return delegate.isConnected(); } @Override public boolean isInputShutdown() { return delegate.isInputShutdown(); } @Override public boolean isOutputShutdown() { return delegate.isOutputShutdown(); } @Override public void sendUrgentData(int value) throws IOException { delegate.sendUrgentData(value); } @Override public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { delegate.setPerformancePreferences(connectionTime, latency, bandwidth); } @Override public void setSoLinger(boolean on, int timeout) throws SocketException { delegate.setSoLinger(on, timeout); } @Override public void shutdownInput() throws IOException { delegate.shutdownInput(); } @Override public void shutdownOutput() throws IOException { delegate.shutdownOutput(); } // inspired by https://github.com/k9mail/k-9/commit/54f9fd36a77423a55f63fbf9b1bcea055a239768 public DelegateSSLSocket setHostname(String host) { try { delegate .getClass() .getMethod("setHostname", String.class) .invoke(delegate, host); } catch (Exception e) { //throw new IllegalStateException("Could not enable SNI", e); } return (this); } @Override public String toString() { return delegate.toString(); } @Override public boolean equals(Object o) { return delegate.equals(o); } } } ================================================ FILE: app/src/main/java/knf/kuma/custom/VariantGridLayoutManager.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import androidx.annotation.Keep import androidx.recyclerview.widget.GridLayoutManager class VariantGridLayoutManager : GridLayoutManager { @Keep constructor(context: Context, spanCount: Int) : super(context, spanCount) @Keep constructor(context: Context, spanCount: Int, orientation: Int, reverseLayout: Boolean) : super(context, spanCount, orientation, reverseLayout) @Keep constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) override fun supportsPredictiveItemAnimations(): Boolean = false } ================================================ FILE: app/src/main/java/knf/kuma/custom/VariantLinearLayoutManager.kt ================================================ package knf.kuma.custom import android.content.Context import android.util.AttributeSet import androidx.annotation.Keep import androidx.recyclerview.widget.LinearLayoutManager class VariantLinearLayoutManager : LinearLayoutManager { @Keep constructor(context: Context) : super(context) @Keep constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout) @Keep constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) override fun supportsPredictiveItemAnimations(): Boolean = false } ================================================ FILE: app/src/main/java/knf/kuma/custom/WrapWebView.kt ================================================ package knf.kuma.custom import android.content.Context import android.content.res.Configuration import android.os.Build import android.util.AttributeSet import android.webkit.WebView class WrapWebView : WebView { constructor(context: Context) : super(context.configurated) constructor(context: Context, attrs: AttributeSet) : super(context.configurated, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context.configurated, attrs, defStyleAttr) } val Context.configurated: Context get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) createConfigurationContext(Configuration()) else this ================================================ FILE: app/src/main/java/knf/kuma/custom/exceptions/EJNFException.kt ================================================ package knf.kuma.custom.exceptions class EJNFException(message: String? = null) : IllegalStateException(message) ================================================ FILE: app/src/main/java/knf/kuma/custom/snackbar/SnackProgressBar.kt ================================================ package knf.kuma.custom.snackbar import android.graphics.Bitmap import android.os.Bundle import androidx.annotation.DrawableRes import androidx.annotation.IntDef import androidx.annotation.IntRange import androidx.annotation.Keep import knf.kuma.custom.snackbar.SnackProgressBar.Companion.TYPE_CIRCULAR import knf.kuma.custom.snackbar.SnackProgressBar.Companion.TYPE_HORIZONTAL import knf.kuma.custom.snackbar.SnackProgressBar.Companion.TYPE_NORMAL /** * Main class containing the display information of SnackProgressBar to be displayed * via SnackProgressBarManager. * * @property type SnackProgressBar of either * [TYPE_NORMAL], [TYPE_HORIZONTAL] or [TYPE_CIRCULAR] * @property message Message of SnackProgressBar. */ @Keep class SnackProgressBar(@SnackProgressBarType private var type: Int, private var message: String) { @Retention(AnnotationRetention.SOURCE) @IntDef(TYPE_NORMAL, TYPE_HORIZONTAL, TYPE_CIRCULAR) annotation class SnackProgressBarType companion object { /** * SnackProgressBar layout with message only. */ const val TYPE_NORMAL = 100 /** * SnackProgressBar layout with message and horizontal progressBar. */ const val TYPE_HORIZONTAL = 200 /** * SnackProgressBar layout with message and circular progressBar. */ const val TYPE_CIRCULAR = 300 internal const val DEFAULT_ICON_RES_ID = -1 } /** * Interface definition for a callback to be invoked when an action is clicked. */ interface OnActionClickListener { /** * Called when an action is clicked. */ fun onActionClick() } /* variables */ private var action: String = "" private var onActionClickListener: OnActionClickListener? = null private var iconBitmap: Bitmap? = null private var iconResId: Int = DEFAULT_ICON_RES_ID private var progressMax: Int = 100 private var allowUserInput: Boolean = false private var swipeToDismiss: Boolean = false private var isIndeterminate: Boolean = false private var showProgressPercentage: Boolean = false private var bundle: Bundle? = null /** * Internal constructor for duplicating SnackProgressBar. */ internal constructor (type: Int, message: String, action: String, onActionClickListener: OnActionClickListener?, iconBitmap: Bitmap?, iconResId: Int, progressMax: Int, allowUserInput: Boolean, swipeToDismiss: Boolean, isIndeterminate: Boolean, showProgressPercentage: Boolean, bundle: Bundle?) : this(type, message) { this.action = action this.onActionClickListener = onActionClickListener this.iconBitmap = iconBitmap this.iconResId = iconResId this.progressMax = progressMax this.allowUserInput = allowUserInput this.swipeToDismiss = swipeToDismiss this.isIndeterminate = isIndeterminate this.showProgressPercentage = showProgressPercentage this.bundle = bundle } /** * Sets the type of SnackProgressBar. * * @param type SnackProgressBar of either * [TYPE_NORMAL], [TYPE_HORIZONTAL] or [TYPE_CIRCULAR] */ fun setType(type: Int) { this.type = type } internal fun getType(): Int { return type } /** * Sets the message of SnackProgressBar. * * @param message Message of SnackProgressBar. */ fun setMessage(message: String): SnackProgressBar { this.message = message return this } internal fun getMessage(): String { return message } /** * Sets the action of SnackProgressBar. * * @param action Action to be displayed. */ fun setAction(action: String, onActionClickListener: OnActionClickListener?): SnackProgressBar { this.action = action this.onActionClickListener = onActionClickListener return this } internal fun getAction(): String { return action } internal fun getOnActionClickListener(): OnActionClickListener? { return onActionClickListener } /** * Sets the icon of SnackProgressBar. Only a bitmap or a resId can be specified at any one time. * * @param bitmap Bitmap of icon. */ fun setIconBitmap(bitmap: Bitmap): SnackProgressBar { iconBitmap = bitmap iconResId = DEFAULT_ICON_RES_ID return this } internal fun getIconBitmap(): Bitmap? { return iconBitmap } /** * Sets the icon of SnackProgressBar. Only a bitmap or a resId can be specified at any one time. * * @param iconResId The resource identifier of the icon to be displayed. */ fun setIconResource(@DrawableRes iconResId: Int): SnackProgressBar { iconBitmap = null this.iconResId = iconResId return this } internal fun getIconResource(): Int { return iconResId } /** * Sets the max progress for determinate ProgressBar. * * @param progressMax Max progress for determinate ProgressBar. Default = 100. */ fun setProgressMax(@IntRange(from = 1) progressMax: Int): SnackProgressBar { this.progressMax = progressMax return this } internal fun getProgressMax(): Int { return progressMax } /** * Sets whether user input is allowed. Setting to FALSE will display an OverlayLayout which blocks user input. * * @param allowUserInput Whether to allow user input. Default = FALSE. */ fun setAllowUserInput(allowUserInput: Boolean): SnackProgressBar { this.allowUserInput = allowUserInput return this } internal fun isAllowUserInput(): Boolean { return allowUserInput } /** * Sets whether user can swipe to dismiss. * * @param swipeToDismiss Whether user can swipe to dismiss. Default = FALSE. */ fun setSwipeToDismiss(swipeToDismiss: Boolean): SnackProgressBar { this.swipeToDismiss = swipeToDismiss return this } internal fun isSwipeToDismiss(): Boolean { return swipeToDismiss } /** * Sets whether the progressBar is indeterminate. * * @param isIndeterminate Whether the progressBar is indeterminate. Default = FALSE. */ fun setIsIndeterminate(isIndeterminate: Boolean): SnackProgressBar { this.isIndeterminate = isIndeterminate return this } internal fun isIndeterminate(): Boolean { return isIndeterminate } /** * Sets whether to show progress in percentage. * * @param showProgressPercentage Whether to show progressText. Default = FALSE. */ fun setShowProgressPercentage(showProgressPercentage: Boolean): SnackProgressBar { this.showProgressPercentage = showProgressPercentage return this } internal fun isShowProgressPercentage(): Boolean { return showProgressPercentage } /** * Sets the additional bundle of SnackProgressBar. * * @param bundle Bundle of SnackProgressBar. */ fun putBundle(bundle: Bundle): SnackProgressBar { this.bundle = bundle return this } /** * Gets the additional bundle of SnackProgressBar. This value may be null. */ fun getBundle(): Bundle? { return bundle } // toString override fun toString(): String { val typeString = when (type) { TYPE_CIRCULAR -> "TYPE_CIRCULAR" TYPE_HORIZONTAL -> "TYPE_HORIZONTAL" else -> "TYPE_NORMAL" } val hasIcon = iconBitmap != null && iconResId != DEFAULT_ICON_RES_ID return "SnackProgressBar(type='$typeString', " + "message='$message', " + "action='$action', " + "hasIcon=$hasIcon, " + "progressMax=$progressMax, " + "allowUserInput=$allowUserInput, " + "swipeToDismiss=$swipeToDismiss, " + "isIndeterminate=$isIndeterminate, " + "showProgressPercentage=$showProgressPercentage)" } } ================================================ FILE: app/src/main/java/knf/kuma/custom/snackbar/SnackProgressBarCore.kt ================================================ package knf.kuma.custom.snackbar import android.os.Handler import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.IntRange import androidx.annotation.Keep import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import com.google.android.material.snackbar.BaseTransientBottomBar import knf.kuma.R import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.noCrash import knf.kuma.custom.snackbar.SnackProgressBar.Companion.TYPE_CIRCULAR import knf.kuma.custom.snackbar.SnackProgressBar.Companion.TYPE_HORIZONTAL import knf.kuma.custom.snackbar.SnackProgressBar.Companion.TYPE_NORMAL import org.jetbrains.anko.sdk27.coroutines.onClick import java.util.Locale /** * Core class constructing the SnackProgressBar. */ @Keep internal class SnackProgressBarCore private constructor( private val parentView: ViewGroup, private val snackProgressBarLayout: SnackProgressBarLayout, private val overlayLayout: View, private var showDuration: Int, private var snackProgressBar: SnackProgressBar) : BaseTransientBottomBar(parentView, snackProgressBarLayout, snackProgressBarLayout) { /* variables */ private val shortDurationMillis = 1500 // as per SnackbarManager private val longDurationMillis = 2750 // as per SnackbarManager private val handler = Handler() private val runnable = Runnable { dismiss() } companion object { /** * Prepares SnackProgressBarCore. * * @param parentView View to hold the SnackProgressBar, prepared by SnackProgressBarManager * @param snackProgressBar SnackProgressBar to be shown. * @param showDuration Duration to show the SnackProgressBar. * @param viewsToMove View to be animated along with the SnackProgressBar. */ internal fun make(parentView: ViewGroup, snackProgressBar: SnackProgressBar, showDuration: Int, viewsToMove: Array?): SnackProgressBarCore { // get inflater from parent val inflater = LayoutInflater.from(parentView.context) // add overlayLayout as background val overlayLayout = inflater.inflate(R.layout.overlay, parentView, false) overlayLayout.tag = "Overlay" parentView.addView(overlayLayout) // inflate SnackProgressBarLayout and pass viewsToMove val snackProgressBarLayout = inflater.inflate( R.layout.snackprogressbar, parentView, false) as SnackProgressBarLayout snackProgressBarLayout.setViewsToMove(viewsToMove) // create SnackProgressBarCore val snackProgressBarCore = SnackProgressBarCore( parentView, snackProgressBarLayout, overlayLayout, showDuration, snackProgressBar) snackProgressBarCore.updateTo(snackProgressBar) return snackProgressBarCore } } init { overlayLayout.onClick { noCrash { if (!isShown) parentView.removeView(overlayLayout) } } } /** * Gets the attached snackProgressBar. */ internal fun getSnackProgressBar(): SnackProgressBar { return snackProgressBar } /** * Updates the SnackProgressBar without dismissing it to the new SnackProgressBar. * * @param snackProgressBar SnackProgressBar to be updated to. */ internal fun updateTo(snackProgressBar: SnackProgressBar) { this.snackProgressBar = snackProgressBar setType() setIcon() setAction() showProgressPercentage() setProgressMax() setSwipeToDismiss() setMessage() // only toggle overlayLayout visibility if already shown if (isShown) { showOverlayLayout() } } /** * Updates the color and alpha of overlayLayout. * * @param overlayColor R.color id. * @param overlayLayoutAlpha Alpha between 0f to 1f. Default = 0.8f. */ internal fun setOverlayLayout(overlayColor: Int, overlayLayoutAlpha: Float): SnackProgressBarCore { overlayLayout.setBackgroundColor(ContextCompat.getColor(context, overlayColor)) overlayLayout.alpha = overlayLayoutAlpha return this } /** * Updates the color of the layout. * * @param backgroundColor R.color id. * @param messageTextColor R.color id. * @param actionTextColor R.color id. * @param progressBarColor R.color id. * @param progressTextColor R.color id. */ internal fun setColor(backgroundColor: Int, messageTextColor: Int, actionTextColor: Int, progressBarColor: Int, progressTextColor: Int): SnackProgressBarCore { snackProgressBarLayout.setColor(backgroundColor, messageTextColor, actionTextColor, progressBarColor, progressTextColor) return this } /** * Sets the text size of SnackProgressBar. * * @param px Font size in pixels. */ internal fun setTextSize(px: Float): SnackProgressBarCore { snackProgressBarLayout.setTextSize(px) return this } /** * Sets the max lines for message. * * @param maxLines Number of lines. */ internal fun setMaxLines(maxLines: Int): SnackProgressBarCore { snackProgressBarLayout.setMaxLines(maxLines) return this } /** * Sets the progress for SnackProgressBar. * * @param progress Progress of the ProgressBar. */ internal fun setProgress(@IntRange(from = 0) progress: Int): SnackProgressBarCore { val progressBar = when (snackProgressBar.getType()) { TYPE_HORIZONTAL -> snackProgressBarLayout.horizontalProgressBar TYPE_CIRCULAR -> snackProgressBarLayout.circularDeterminateProgressBar else -> null } if (progressBar != null) { progressBar.progress = progress val progress100 = (progress.toFloat() / progressBar.max * 100).toInt() var progressString = progress100.toString() snackProgressBarLayout.progressTextCircular.text = progressString // include % for progressText progressString += "%" snackProgressBarLayout.progressText.text = progressString } return this } /** * Show the SnackProgressBar */ override fun show() { // show overLayLayout showOverlayLayout() // use default SnackManager if it is CoordinatorLayout if (parentView is CoordinatorLayout) { duration = showDuration } // else, set up own handler for dismiss countdown else { setOnBarTouchListener() // disable SnackManager by stopping countdown duration = LENGTH_INDEFINITE // assign the actual showDuration if dismiss is required if (showDuration != LENGTH_INDEFINITE) { when (showDuration) { LENGTH_SHORT -> showDuration = shortDurationMillis LENGTH_LONG -> showDuration = longDurationMillis } handler.postDelayed(runnable, showDuration.toLong()) } } super.show() } override fun dismiss() { removeOverlayLayout() super.dismiss() } /** * Sets the layout based on SnackProgressBar type. * Note: Layout positioning for action is handled by [SnackProgressBarLayout] */ private fun setType(): SnackProgressBarCore { // update view when (snackProgressBar.getType()) { TYPE_NORMAL -> { snackProgressBarLayout.horizontalProgressBar.visibility = View.GONE snackProgressBarLayout.circularDeterminateProgressBar.visibility = View.GONE snackProgressBarLayout.circularIndeterminateProgressBar.visibility = View.GONE } TYPE_HORIZONTAL -> { snackProgressBarLayout.horizontalProgressBar.visibility = View.VISIBLE snackProgressBarLayout.circularDeterminateProgressBar.visibility = View.GONE snackProgressBarLayout.circularIndeterminateProgressBar.visibility = View.GONE snackProgressBarLayout.horizontalProgressBar.isIndeterminate = snackProgressBar.isIndeterminate() } TYPE_CIRCULAR -> { snackProgressBarLayout.horizontalProgressBar.visibility = View.GONE if (snackProgressBar.isIndeterminate()) { snackProgressBarLayout.circularDeterminateProgressBar.visibility = View.GONE snackProgressBarLayout.circularIndeterminateProgressBar.visibility = View.VISIBLE } else { snackProgressBarLayout.circularDeterminateProgressBar.visibility = View.VISIBLE snackProgressBarLayout.circularIndeterminateProgressBar.visibility = View.GONE } } } return this } /** * Sets the icon of SnackProgressBar. */ private fun setIcon(): SnackProgressBarCore { val iconBitmap = snackProgressBar.getIconBitmap() val iconResId = snackProgressBar.getIconResource() when { iconBitmap != null -> { snackProgressBarLayout.iconImage.setImageBitmap(iconBitmap) snackProgressBarLayout.iconImage.visibility = View.VISIBLE } iconResId != SnackProgressBar.DEFAULT_ICON_RES_ID -> { snackProgressBarLayout.iconImage.setImageResource(iconResId) snackProgressBarLayout.iconImage.visibility = View.VISIBLE } else -> snackProgressBarLayout.iconImage.visibility = View.GONE } return this } /** * Sets the action to be displayed. */ private fun setAction(): SnackProgressBarCore { val action = snackProgressBar.getAction() val onActionClickListener = snackProgressBar.getOnActionClickListener() // set the text snackProgressBarLayout.actionText.text = action.uppercase(Locale.getDefault()) snackProgressBarLayout.actionNextLineText.text = action.uppercase(Locale.getDefault()) // set the onClickListener val onClickListener = View.OnClickListener { onActionClickListener?.onActionClick() dismiss() } snackProgressBarLayout.actionText.setOnClickListener(onClickListener) snackProgressBarLayout.actionNextLineText.setOnClickListener(onClickListener) return this } /** * Sets whether to show progressText. */ private fun showProgressPercentage(): SnackProgressBarCore { if (snackProgressBar.isShowProgressPercentage()) { if (snackProgressBar.getType() == TYPE_CIRCULAR) { snackProgressBarLayout.progressText.visibility = View.GONE snackProgressBarLayout.progressTextCircular.visibility = View.VISIBLE } else { snackProgressBarLayout.progressText.visibility = View.VISIBLE snackProgressBarLayout.progressTextCircular.visibility = View.GONE } } else { snackProgressBarLayout.progressText.visibility = View.GONE snackProgressBarLayout.progressTextCircular.visibility = View.GONE } return this } /** * Sets the max progress for progressBar. */ private fun setProgressMax(): SnackProgressBarCore { snackProgressBarLayout.horizontalProgressBar.max = snackProgressBar.getProgressMax() snackProgressBarLayout.circularDeterminateProgressBar.max = snackProgressBar.getProgressMax() return this } /** * Sets whether user can swipe to dismiss. */ private fun setSwipeToDismiss(): SnackProgressBarCore { snackProgressBarLayout.setSwipeToDismiss(snackProgressBar.isSwipeToDismiss()) return this } /** * Sets the message of SnackProgressBar. */ private fun setMessage(): SnackProgressBarCore { snackProgressBarLayout.messageText.text = snackProgressBar.getMessage() return this } /** * Shows the overlayLayout based on whether user input is allowed. */ private fun showOverlayLayout(): SnackProgressBarCore { if (!snackProgressBar.isAllowUserInput()) { overlayLayout.visibility = View.VISIBLE } else { overlayLayout.visibility = View.GONE } return this } /** * Removes the overlayLayout */ internal fun removeOverlayLayout() { doOnUIGlobal { parentView.removeView(overlayLayout) } } /** * Registers a callback to be invoked when the SnackProgressBar is touched. * This is only applicable when swipe to dismiss behaviour is true and CoordinatorLayout is not used. */ private fun setOnBarTouchListener() { snackProgressBarLayout.setOnBarTouchListener(object : SnackProgressBarLayout.OnBarTouchListener { override fun onTouch(event: Int) { when (event) { SnackProgressBarLayout.ACTION_DOWN -> // when user touched the SnackProgressBar, stop the dismiss countdown handler.removeCallbacks(runnable) SnackProgressBarLayout.SWIPE_OUT -> // once the SnackProgressBar is swiped out, dismiss after animation ends handler.postDelayed(runnable, SnackProgressBarLayout.ANIMATION_DURATION) SnackProgressBarLayout.SWIPE_IN -> // once the SnackProgressBar is swiped in, resume dismiss countdown if (showDuration != SnackProgressBarManager.LENGTH_INDEFINITE) { handler.postDelayed(runnable, showDuration.toLong()) } } } }) } } ================================================ FILE: app/src/main/java/knf/kuma/custom/snackbar/SnackProgressBarLayout.kt ================================================ package knf.kuma.custom.snackbar import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.util.TypedValue import android.view.MotionEvent import android.view.VelocityTracker import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.annotation.ColorRes import androidx.annotation.Keep import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.google.android.material.snackbar.ContentViewCallback import knf.kuma.R import knf.kuma.databinding.SnackprogressbarBinding /** * Layout class for SnackProgressBar. */ @Keep internal class SnackProgressBarLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr), ContentViewCallback { /* variables */ companion object { internal const val ACTION_DOWN = 123 internal const val SWIPE_OUT = 456 internal const val SWIPE_IN = 789 internal const val ANIMATION_DURATION = 250L // animation duration as per BaseTransientBottomBar } private val binding by lazy { SnackprogressbarBinding.bind(this) } private val backgroundLayout by lazy { binding.snackProgressBarLayoutBackground } private val mainLayout by lazy { binding.snackProgressBarLayoutMain } private val actionNextLineLayout by lazy { binding.snackProgressBarLayoutActionNextLine } internal val iconImage by lazy { binding.snackProgressBarImgIcon } internal val messageText by lazy { binding.snackProgressBarTxtMessage } internal val actionText by lazy { binding.snackProgressBarTxtAction } internal val actionNextLineText by lazy { binding.snackProgressBarTxtActionNextLine } internal val progressText by lazy { binding.snackProgressBarTxtProgress } internal val progressTextCircular by lazy { binding.snackProgressBarTxtProgressCircular } internal val horizontalProgressBar by lazy { binding.snackProgressBarProgressbarHorizontal } internal val circularDeterminateProgressBar by lazy { binding.snackProgressBarProgressbarCircularDeterminate } internal val circularIndeterminateProgressBar by lazy { binding.snackProgressBarProgressbarCircularIndeterminate } private val startAlphaSwipeDistance = 0.1f // as per Behavior in BaseTransientBottomBar private val endAlphaSwipeDistance = 0.6f // as per Behavior in BaseTransientBottomBar private val swipeOutVelocity = 800f private val heightSingle = resources.getDimension(R.dimen.snackProgressBar_height_single).toInt() // height as per Material Design private val heightMulti = resources.getDimension(R.dimen.snackProgressBar_height_multi).toInt() // height as per Material Design private val heightActionNextLine = resources.getDimension(R.dimen.snackProgressBar_height_actionNextLine).toInt() private val defaultTextSizeDp = resources.getDimension(R.dimen.text_body_dp).toInt() // use fixed dp for comparison purpose private var isCoordinatorLayout: Boolean = false private var swipeToDismiss: Boolean = false private var viewsToMove: Array? = null private var onBarTouchListener: OnBarTouchListener? = null /** * Interface definition for a callback to be invoked when the SnackProgressBar is touched. */ interface OnBarTouchListener { /** * Called when the SnackProgressBar is touched. * * @param event Type of touch event. */ fun onTouch(event: Int) } /** * Registers a callback to be invoked when the SnackProgressBar is touched. * * @param onBarTouchListener The callback that will run. This value may be null. */ internal fun setOnBarTouchListener(onBarTouchListener: OnBarTouchListener?) { this.onBarTouchListener = onBarTouchListener } /** * Passes the view (e.g. FloatingActionButton) to move up or down as SnackProgressBar is shown or dismissed. * * @param viewsToMove Views to animate when the SnackProgressBar is shown or dismissed. */ internal fun setViewsToMove(viewsToMove: Array?) { this.viewsToMove = viewsToMove } /** * Updates the color of the layout. * * @param backgroundColor R.color id. * @param messageTextColor R.color id. * @param actionTextColor R.color id. * @param progressBarColor R.color id. * @param progressTextColor R.color id. */ internal fun setColor(@ColorRes backgroundColor: Int, @ColorRes messageTextColor: Int, @ColorRes actionTextColor: Int, @ColorRes progressBarColor: Int, @ColorRes progressTextColor: Int) { backgroundLayout.setBackgroundColor(ContextCompat.getColor(context, backgroundColor)) messageText.setTextColor(ContextCompat.getColor(context, messageTextColor)) actionText.setTextColor(ContextCompat.getColor(context, actionTextColor)) actionNextLineText.setTextColor(ContextCompat.getColor(context, actionTextColor)) horizontalProgressBar.progressDrawable.setColorFilter( ContextCompat.getColor(context, progressBarColor), android.graphics.PorterDuff.Mode.SRC_IN) circularDeterminateProgressBar.progressDrawable.setColorFilter( ContextCompat.getColor(context, progressBarColor), android.graphics.PorterDuff.Mode.SRC_IN) horizontalProgressBar.indeterminateDrawable.setColorFilter( ContextCompat.getColor(context, progressBarColor), android.graphics.PorterDuff.Mode.SRC_IN) circularIndeterminateProgressBar.indeterminateDrawable.setColorFilter( ContextCompat.getColor(context, progressBarColor), android.graphics.PorterDuff.Mode.SRC_IN) progressText.setTextColor(ContextCompat.getColor(context, progressTextColor)) progressTextCircular.setTextColor(ContextCompat.getColor(context, progressTextColor)) } /** * Sets the text size of SnackProgressBar. * * @param px Font size in pixels. */ internal fun setTextSize(px: Float) { messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, px) actionText.setTextSize(TypedValue.COMPLEX_UNIT_PX, px) actionNextLineText.setTextSize(TypedValue.COMPLEX_UNIT_PX, px) progressText.setTextSize(TypedValue.COMPLEX_UNIT_PX, px) // not changed for progressTextCircular as it will be out of the progressBar. } /** * Sets the max lines for message. * * @param maxLines Number of lines. */ internal fun setMaxLines(maxLines: Int) { messageText.maxLines = maxLines } /** * Sets whether user can swipe to dismiss. * * @param swipeToDismiss Whether user can swipe to dismiss. * @see .configureSwipeToDismiss */ internal fun setSwipeToDismiss(swipeToDismiss: Boolean) { this.swipeToDismiss = swipeToDismiss } // onMeasure for determining actionNextLine and message height override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val lineCount = messageText.lineCount val textSize = messageText.textSize.toInt() val hasAction = actionText.text.toString().isNotEmpty() // put the action into next line if width is more than 25% of total width, or if other element is taking the space val isActionNextLine = (actionText.measuredWidth.toFloat() / backgroundLayout.measuredWidth.toFloat() > 0.25f) || circularDeterminateProgressBar.isVisible || circularIndeterminateProgressBar.isVisible || progressText.isVisible || progressTextCircular.isVisible if (hasAction) { if (isActionNextLine) { actionText.visibility = GONE // set actionNextLineLayout height val height = if (textSize <= defaultTextSizeDp) { heightActionNextLine } else { heightActionNextLine + (textSize - defaultTextSizeDp) } val layoutParams = actionNextLineLayout.layoutParams as LayoutParams if (layoutParams.height != height) { layoutParams.height = height actionNextLineLayout.layoutParams = layoutParams } actionNextLineLayout.visibility = VISIBLE } else { actionText.visibility = VISIBLE actionNextLineLayout.visibility = GONE } } else { actionText.visibility = GONE actionNextLineLayout.visibility = GONE } // set layout height according to message length val height: Int = when (lineCount) { 1 -> { if (textSize <= defaultTextSizeDp) { heightSingle } else { heightSingle + (textSize - defaultTextSizeDp) } } 2 -> { if (textSize <= defaultTextSizeDp) { heightMulti } else { heightMulti + 2 * (textSize - defaultTextSizeDp) } } else -> (heightMulti + (lineCount * textSize - 2 * defaultTextSizeDp)) } val layoutParams = mainLayout.layoutParams as LayoutParams if (layoutParams.height != height) { layoutParams.height = height mainLayout.layoutParams = layoutParams // remeasure after height change super.onMeasure(widthMeasureSpec, heightMeasureSpec) } } // onAttachedToWindow override fun onAttachedToWindow() { super.onAttachedToWindow() // clear the padding of the parent that hold this view val parentView = parent as View parentView.setPadding(0, 0, 0, 0) // check if it is CoordinatorLayout and configure swipe to dismiss isCoordinatorLayout = parentView.parent is CoordinatorLayout configureSwipeToDismiss() } // animation for when updateTo() is called by SnackProgressBarManager override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) { viewsToMove?.run { if (oldH != 0 && oldH != h) { for (viewToMove in this) { ViewCompat.animate(viewToMove).translationYBy((oldH - h).toFloat()).setDuration(ANIMATION_DURATION).start() } } } } // animation as per original Snackbar class override fun animateContentIn(delay: Int, duration: Int) { val viewsToAnimate = arrayOf( messageText, actionText, actionNextLineText, progressText, horizontalProgressBar, circularDeterminateProgressBar, circularIndeterminateProgressBar) viewsToAnimate.forEach { viewToAnimate -> if (viewToAnimate.visibility == VISIBLE) { viewToAnimate.alpha = 0f ViewCompat.animate(viewToAnimate).alpha(1f).setDuration(duration.toLong()) .setStartDelay(delay.toLong()).start() } } viewsToMove?.run { for (viewToMove in this) { ViewCompat.animate(viewToMove).translationY((-1 * measuredHeight).toFloat()) .setDuration(ANIMATION_DURATION).start() } } } // animation as per original Snackbar class override fun animateContentOut(delay: Int, duration: Int) { val viewsToAnimate = arrayOf( messageText, actionText, actionNextLineText, progressText, horizontalProgressBar, circularDeterminateProgressBar, circularIndeterminateProgressBar) viewsToAnimate.forEach { viewToAnimate -> if (viewToAnimate.visibility == VISIBLE) { viewToAnimate.alpha = 1f ViewCompat.animate(viewToAnimate).alpha(0f).setDuration(duration.toLong()) .setStartDelay(delay.toLong()).start() } } viewsToMove?.run { for (viewToMove in this) { ViewCompat.animate(viewToMove).translationY(0f) .setDuration(ANIMATION_DURATION).start() } } } private fun configureSwipeToDismiss() { if (swipeToDismiss) { // attach touch listener if it is not a CoordinatorLayout to allow extra features if (!isCoordinatorLayout) { setOnTouchListener() } } else { // remove default behaviour specified in BaseTransientBottomBar for CoordinatorLayout if (isCoordinatorLayout) { val parentView = parent as ViewGroup val layoutParams = parentView.layoutParams as CoordinatorLayout.LayoutParams layoutParams.behavior = null } } } /** * Sets onTouchListener to allow swipe to dismiss behaviour for layouts other than CoordinatorLayout. */ @SuppressLint("ClickableViewAccessibility") private fun setOnTouchListener() { backgroundLayout.setOnTouchListener(object : OnTouchListener { // variables private val parentView = parent as View private var startX: Float = 0f private var endX: Float = 0f private lateinit var velocityTracker: VelocityTracker override fun onTouch(v: View, event: MotionEvent): Boolean { val index = event.actionIndex val pointerId = event.getPointerId(index) when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { // callback onBarTouchListener onBarTouchListener?.onTouch(ACTION_DOWN) // track initial coordinate startX = event.rawX // track velocity velocityTracker = VelocityTracker.obtain() velocityTracker.addMovement(event) } MotionEvent.ACTION_MOVE -> { // track velocity velocityTracker.addMovement(event) // track move coordinate val moveX = event.rawX // set translationX val deltaX = moveX - startX parentView.translationX = deltaX // animate alpha as per behaviour specified in BaseTransientBottomBar for CoordinatorLayout val totalWidth = parentView.measuredWidth val fractionTravelled = Math.abs(deltaX / totalWidth) when { fractionTravelled < startAlphaSwipeDistance -> parentView.alpha = 1f fractionTravelled > endAlphaSwipeDistance -> parentView.alpha = 0f else -> parentView.alpha = 1f - (fractionTravelled - startAlphaSwipeDistance) / (endAlphaSwipeDistance - startAlphaSwipeDistance) } } MotionEvent.ACTION_UP -> { // track final coordinate endX = event.rawX // get velocity and return resources velocityTracker.computeCurrentVelocity(1000) val velocity = Math.abs(velocityTracker.getXVelocity(pointerId)) velocityTracker.recycle() // animate layout var toSwipeOut = false // swipe out if layout moved more than half of the screen if (Math.abs(endX - startX) / parentView.width > 0.5) { toSwipeOut = true } // swipe out if velocity is high if (Math.abs(velocity) > swipeOutVelocity) { toSwipeOut = true } if (toSwipeOut) { swipeOut(endX - startX) } else { // else, return to original position swipeIn(endX - startX) } // to satisfy android accessibility v.performClick() } } return true } }) } /** * Swipe out animation. * * @param deltaX Difference between position of ACTION_DOWN and ACTION_UP. * Positive value means user swiped to right. */ private fun swipeOut(deltaX: Float) { // callback onBarTouchListener onBarTouchListener?.onTouch(SWIPE_OUT) val parentView = parent as View val direction = if (deltaX > 0f) 1f else -1f ViewCompat.animate(parentView) .translationX(direction * parentView.width) .setInterpolator(FastOutSlowInInterpolator()) .setDuration(ANIMATION_DURATION) .setListener(null) // remove listener that is attached in animateViewIn() of BaseTransientBottomBar .start() ViewCompat.animate(parentView) .alpha(0f) .setDuration(ANIMATION_DURATION) .start() } /** * Animation for swipe in. * * @param deltaX Difference between position of ACTION_DOWN and ACTION_UP. * Zero means a user click. */ private fun swipeIn(deltaX: Float) { val parentView = parent as View // animate if the layout has moved if (Math.abs(deltaX) >= 0f) { ViewCompat.animate(parentView) .translationX(0f) .setInterpolator(FastOutSlowInInterpolator()) .setDuration(ANIMATION_DURATION) .setListener(null) // remove listener that is attached in animateViewIn() of BaseTransientBottomBar .start() ViewCompat.animate(parentView) .alpha(1f) .setDuration(ANIMATION_DURATION) .start() } else { // else just make sure the layout is at correct position parentView.translationX = 0f parentView.alpha = 1f } // callback onBarTouchListener onBarTouchListener?.onTouch(SWIPE_IN) } } ================================================ FILE: app/src/main/java/knf/kuma/custom/snackbar/SnackProgressBarManager.kt ================================================ package knf.kuma.custom.snackbar import android.util.SparseArray import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.annotation.ColorRes import androidx.annotation.FloatRange import androidx.annotation.IntDef import androidx.annotation.IntRange import androidx.annotation.Keep import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.snackbar.BaseTransientBottomBar import knf.kuma.R import knf.kuma.custom.snackbar.SnackProgressBarManager.Companion.LENGTH_INDEFINITE import knf.kuma.custom.snackbar.SnackProgressBarManager.Companion.LENGTH_LONG import knf.kuma.custom.snackbar.SnackProgressBarManager.Companion.LENGTH_SHORT /** * Manager class handling all the SnackProgressBars added. *

    * This class queues the SnackProgressBars to be shown. * It will dismiss the SnackProgressBar according to its desired duration before showing the next in queue. *

    * * @constructor If possible, the root view of the activity should be provided and can be any type of layout. * The constructor will loop and crawl up the view hierarchy to find a suitable parent view if non-root view is provided. * *

    If a CoordinatorLayout is provided, the FloatingActionButton will animate with the SnackProgressBar. * Else, [setViewsToMove] needs to be called to select the views to be animated. * Note that [setViewsToMove] can still be used for CoordinatorLayout to move other views.

    * *

    Swipe to dismiss behaviour can be set via [SnackProgressBar.setSwipeToDismiss]. * This is provided via [BaseTransientBottomBar] for CoordinatorLayout, but provided via * [SnackProgressBarLayout] for other layout types.

    * * @param view View to hold the SnackProgressBar. */ @Keep class SnackProgressBarManager(view: View) { @Retention(AnnotationRetention.SOURCE) @IntRange(from = 1) annotation class OneUp companion object { @Retention(AnnotationRetention.SOURCE) @IntDef(LENGTH_LONG, LENGTH_SHORT, LENGTH_INDEFINITE) @IntRange(from = 1) annotation class ShowDuration /** * Show the SnackProgressBar indefinitely. * Note that this will be changed to LENGTH_SHORT and dismissed * if there is another SnackProgressBar in queue before and after. */ const val LENGTH_INDEFINITE = BaseTransientBottomBar.LENGTH_INDEFINITE /** * Show the SnackProgressBar for a short period of time. */ const val LENGTH_SHORT = BaseTransientBottomBar.LENGTH_SHORT /** * Show the SnackProgressBar for a long period of time. */ const val LENGTH_LONG = BaseTransientBottomBar.LENGTH_LONG /** * Default SnackProgressBar background color as per Material Design. */ const val BACKGROUND_COLOR_DEFAULT = R.color.background /** * Default message text color as per Material Design. */ const val MESSAGE_COLOR_DEFAULT = R.color.textWhitePrimary /** * Default action text color as per Material Design i.e. R.color.colorAccent. */ const val ACTION_COLOR_DEFAULT = R.color.colorAccent /** * Default progressBar color as per Material Design i.e. R.color.colorAccent. */ const val PROGRESSBAR_COLOR_DEFAULT = R.color.colorAccent /** * Default progressText color as per Material Design. */ const val PROGRESSTEXT_COLOR_DEFAULT = R.color.textWhitePrimary /** * Default overlayLayout color i.e. android.R.color.white. */ const val OVERLAY_COLOR_DEFAULT = android.R.color.white } /* variables */ private val onDisplayIdDefault = -1 private val storedBars = SparseArray() private val queueBars = ArrayList() private val queueOnDisplayIds = ArrayList() private val queueDurations = ArrayList() private var currentQueue = 0 private var currentCore: SnackProgressBarCore? = null private var parentView: ViewGroup = findSuitableParent(view) private var viewsToMove: Array? = null private var backgroundColor = BACKGROUND_COLOR_DEFAULT private var messageTextColor = MESSAGE_COLOR_DEFAULT private var actionTextColor = ACTION_COLOR_DEFAULT private var progressBarColor = PROGRESSBAR_COLOR_DEFAULT private var progressTextColor = PROGRESSTEXT_COLOR_DEFAULT private var overlayColor = OVERLAY_COLOR_DEFAULT private var textSize = parentView.resources.getDimension(R.dimen.text_body) private var maxLines = 2 private var overlayLayoutAlpha = 0.8f private var onDisplayListener: OnDisplayListener? = null /** * Interface definition for a callback to be invoked when the SnackProgressBar is shown or dismissed. */ interface OnDisplayListener { /** * Called when the SnackProgressBar is shown. * * @param onDisplayId OnDisplayId assigned to the SnackProgressBar which is shown. */ fun onShown(snackProgressBar: SnackProgressBar, onDisplayId: Int) /** * Called when the SnackProgressBar is dismissed. * * @param onDisplayId OnDisplayId assigned to the SnackProgressBar which is shown. */ fun onDismissed(snackProgressBar: SnackProgressBar, onDisplayId: Int) } /** * Registers a callback to be invoked when the SnackProgressBar is shown or dismissed. * * @param onDisplayListener The callback that will run. This value may be null. */ fun setOnDisplayListener(onDisplayListener: OnDisplayListener?): SnackProgressBarManager { this.onDisplayListener = onDisplayListener return this } /** * Stores a SnackProgressBar into SnackProgressBarManager to perform further action. * The SnackProgressBar is uniquely identified by the storeId and will be overwritten * by another SnackProgressBar with the same storeId. * * @param snackProgressBar SnackProgressBar to be added. * @param storeId OneUp of the SnackProgressBar to be added. * @see SnackProgressBar * * @see .getSnackProgressBar */ fun put(snackProgressBar: SnackProgressBar, @OneUp storeId: Int) { storedBars.setValueAt(storeId, snackProgressBar) } /** * Retrieves the SnackProgressBar that was previously added into SnackProgressBarManager. * * @param storeId OneUp of the SnackProgressBar stored in SnackProgressBarManager. * @return SnackProgressBar of the storeId. Can be null. * @see .put */ fun getSnackProgressBar(@OneUp storeId: Int): SnackProgressBar? { return storedBars[storeId] } /** * Retrieves the SnackProgressBar that is currently or was showing. * * @return SnackProgressBar that is currently or was showing. Return null if nothing was ever shown. */ fun getLastShown(): SnackProgressBar? { return currentCore?.getSnackProgressBar() } /** * Shows the SnackProgressBar based on its storeId with the specified duration. * If another SnackProgressBar is already showing, this SnackProgressBar will be queued * and shown accordingly after those queued are dismissed. * * @param storeId OneUp of the SnackProgressBar stored in SnackProgressBarManager. * @param duration Duration to show the SnackProgressBar of either * [LENGTH_SHORT], [LENGTH_LONG], [LENGTH_INDEFINITE] * or any positive millis. * @see .put */ fun show(@OneUp storeId: Int, @ShowDuration duration: Int) { val snackProgressBar = storedBars[storeId] snackProgressBar?.run { addToQueue(this, duration, onDisplayIdDefault) } ?: throw IllegalArgumentException("SnackProgressBar with storeId = $storeId is not found!") } /** * Shows the SnackProgressBar based on its storeId with the specified duration. * If another SnackProgressBar is already showing, this SnackProgressBar will be queued * and shown accordingly after those queued are dismissed. * * @param storeId OneUp of the SnackProgressBar stored in SnackProgressBarManager. * @param duration Duration to show the SnackProgressBar of either * [LENGTH_SHORT], [LENGTH_LONG], [LENGTH_INDEFINITE] * or any positive millis. * @param onDisplayId OnDisplayId attached to the SnackProgressBar when implementing the OnDisplayListener. * @see .put */ fun show(@OneUp storeId: Int, @ShowDuration duration: Int, @OneUp onDisplayId: Int) { val snackProgressBar = storedBars[storeId] snackProgressBar?.run { show(this, duration, onDisplayId) } ?: throw IllegalArgumentException("SnackProgressBar with storeId = $storeId is not found!") } /** * Shows the SnackProgressBar with the specified duration. * If another SnackProgressBar is already showing, this SnackProgressBar will be queued * and shown accordingly after those queued are dismissed. * * @param snackProgressBar SnackProgressBar to be shown. * @param duration Duration to show the SnackProgressBar of either * [LENGTH_SHORT], [LENGTH_LONG], [LENGTH_INDEFINITE] * or any positive millis. */ fun show(snackProgressBar: SnackProgressBar, @ShowDuration duration: Int) { addToQueue(snackProgressBar, duration, onDisplayIdDefault) } /** * Shows the SnackProgressBar with the specified duration. * If another SnackProgressBar is already showing, this SnackProgressBar will be queued * and shown accordingly after those queued are dismissed. * * @param snackProgressBar SnackProgressBar to be shown. * @param duration Duration to show the SnackProgressBar of either * [LENGTH_SHORT], [LENGTH_LONG], [LENGTH_INDEFINITE] * or any positive millis. * @param onDisplayId OnDisplayId attached to the SnackProgressBar when implementing the OnDisplayListener. */ fun show(snackProgressBar: SnackProgressBar, @ShowDuration duration: Int, @OneUp onDisplayId: Int) { addToQueue(snackProgressBar, duration, onDisplayId) } /** * Updates the currently showing SnackProgressBar without dismissing it to the SnackProgressBar * with the corresponding storeId i.e. updating without animation. * Note: This does not change the queue. * * @param storeId OneUp of the SnackProgressBar stored in SnackProgressBarManager. */ fun updateTo(@OneUp storeId: Int) { val snackProgressBar = storedBars[storeId] snackProgressBar?.run { updateTo(this) } ?: throw IllegalArgumentException("SnackProgressBar with storeId = $storeId is not found!") } /** * Updates the currently showing SnackProgressBar without dismissing it to the new SnackProgressBar * i.e. updating without animation. * Note: This does not change the queue. * * @param snackProgressBar SnackProgressBar to be updated to. */ fun updateTo(snackProgressBar: SnackProgressBar) { currentCore?.updateTo(snackProgressBar) } /** * Dismisses the currently showing SnackProgressBar and show the next in queue. */ fun dismiss() { currentCore?.dismiss() nextQueue() } /** * Dismisses all the SnackProgressBar that is in queue. * The currently shown SnackProgressBar will also be dismissed. */ fun dismissAll() { // reset queue before dismissing currentCore to prevent next in queue from showing resetQueue() currentCore?.dismiss() } /** * Passes the view (e.g. FloatingActionButton) to move up or down as SnackProgressBar is shown or dismissed. * * * This is not required for FloatingActionButton if a CoordinatorLayout is provided, but will be required for * other layout types. This can be used to animate any view besides FloatingActionButton as well. * * * @param viewToMove View to be animated along with the SnackProgressBar. */ fun setViewToMove(viewToMove: View): SnackProgressBarManager { val viewsToMove = arrayOf(viewToMove) setViewsToMove(viewsToMove) return this } /** * Passes the views (e.g. FloatingActionButton) to move up or down as SnackProgressBar is shown or dismissed. * * * This is not required for FloatingActionButton if a CoordinatorLayout is provided, but will be required for * other layout types. This can be used to animate any views besides FloatingActionButton as well. * * * @param viewsToMove Views to be animated along with the SnackProgressBar. */ private fun setViewsToMove(viewsToMove: Array): SnackProgressBarManager { this.viewsToMove = viewsToMove return this } /** * Sets the text size of SnackProgressBar. * * @param sp Font size in scale-independent pixels. */ fun setTextSize(sp: Float): SnackProgressBarManager { textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, parentView.resources.displayMetrics) currentCore?.setTextSize(textSize) return this } /** * Sets the max lines for message. * * @param maxLines Number of lines. */ fun setMessageMaxLines(maxLines: Int): SnackProgressBarManager { this.maxLines = maxLines currentCore?.setMaxLines(maxLines) return this } /** * Sets the transparency of the overlayLayout which blocks user input. * * @param alpha Alpha between 0f to 1f. Default = 0.8f. */ fun setOverlayLayoutAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float): SnackProgressBarManager { overlayLayoutAlpha = alpha currentCore?.setOverlayLayout(overlayColor, overlayLayoutAlpha) return this } /** * Sets the overlayLayout color. * * @param colorId R.color id. * @see .OVERLAY_COLOR_DEFAULT */ fun setOverlayLayoutColor(@ColorRes colorId: Int): SnackProgressBarManager { overlayColor = colorId // update the UI now if applicable currentCore?.setOverlayLayout(overlayColor, overlayLayoutAlpha) return this } /** * Sets the SnackProgressBar background color. * * @param colorId R.color id. * @see .BACKGROUND_COLOR_DEFAULT */ fun setBackgroundColor(@ColorRes colorId: Int): SnackProgressBarManager { backgroundColor = colorId // update the UI now if applicable currentCore?.setColor(backgroundColor, messageTextColor, actionTextColor, progressBarColor, progressTextColor) return this } /** * Sets the message text color. * * @param colorId R.color id. * @see .MESSAGE_COLOR_DEFAULT */ fun setMessageTextColor(@ColorRes colorId: Int): SnackProgressBarManager { messageTextColor = colorId // update the UI now if applicable currentCore?.setColor(backgroundColor, messageTextColor, actionTextColor, progressBarColor, progressTextColor) return this } /** * Sets the action text color. * * @param colorId R.color id. * @see .ACTION_COLOR_DEFAULT */ fun setActionTextColor(@ColorRes colorId: Int): SnackProgressBarManager { actionTextColor = colorId // update the UI now if applicable currentCore?.setColor(backgroundColor, messageTextColor, actionTextColor, progressBarColor, progressTextColor) return this } /** * Sets the ProgressBar color. * * @param colorId R.color id. * @see .PROGRESSBAR_COLOR_DEFAULT */ fun setProgressBarColor(@ColorRes colorId: Int): SnackProgressBarManager { progressBarColor = colorId // update the UI now if applicable currentCore?.setColor(backgroundColor, messageTextColor, actionTextColor, progressBarColor, progressTextColor) return this } /** * Sets the ProgressText color. * * @param colorId R.color id. * @see .PROGRESSTEXT_COLOR_DEFAULT */ fun setProgressTextColor(@ColorRes colorId: Int): SnackProgressBarManager { progressTextColor = colorId // update the UI now if applicable currentCore?.setColor(backgroundColor, messageTextColor, actionTextColor, progressBarColor, progressTextColor) return this } /** * Sets the progress for SnackProgressBar that is currently showing. * It will also update the progress in % if it is shown. * * @param progress Progress of the ProgressBar. */ fun setProgress(@IntRange(from = 0) progress: Int): SnackProgressBarManager { currentCore?.setProgress(progress) return this } /** * Adds the SnackProgressBar to queue. * * @param snackProgressBar SnackProgressBar to be added to queue. * @param duration Duration to show the SnackProgressBar of either * [LENGTH_SHORT], [LENGTH_LONG], [LENGTH_INDEFINITE] * or any positive millis. * @param onDisplayId OnDisplayId attached to the SnackProgressBar when implementing the OnDisplayListener. */ private fun addToQueue(snackProgressBar: SnackProgressBar, duration: Int, onDisplayId: Int) { // get the queue number as the last of queue list val queue = queueBars.size // create a new object val queueBar = SnackProgressBar(snackProgressBar.getType(), snackProgressBar.getMessage(), snackProgressBar.getAction(), snackProgressBar.getOnActionClickListener(), snackProgressBar.getIconBitmap(), snackProgressBar.getIconResource(), snackProgressBar.getProgressMax(), snackProgressBar.isAllowUserInput(), snackProgressBar.isSwipeToDismiss(), snackProgressBar.isIndeterminate(), snackProgressBar.isShowProgressPercentage(), snackProgressBar.getBundle()) // put in queue queueBars.add(queueBar) queueOnDisplayIds.add(onDisplayId) queueDurations.add(duration) // play queue if first item if (queue == 0) { playQueue(queue) } } /** * Plays the queue. * * @param queue Queue number of SnackProgressBar. */ private fun playQueue(queue: Int) { // check if queue number is bounded if (queue < queueBars.size) { // get the variables currentQueue = queue val snackProgressBar = queueBars[queue] val onDisplayId = queueOnDisplayIds[queue] var duration = queueDurations[queue] // change duration to LENGTH_SHORT if is not last item if (duration == LENGTH_INDEFINITE) { if (queue < queueBars.size - 1) { duration = LENGTH_SHORT } } // create SnackProgressBarCore val finalDuration = duration val snackProgressBarCore = SnackProgressBarCore.make(parentView, snackProgressBar, duration, viewsToMove) .setOverlayLayout(overlayColor, overlayLayoutAlpha) .setColor(backgroundColor, messageTextColor, actionTextColor, progressBarColor, progressTextColor) .setTextSize(textSize) .setMaxLines(maxLines) .addCallback(object : BaseTransientBottomBar.BaseCallback() { override fun onShown(snackProgressBarCore: SnackProgressBarCore) { // set current currentCore = snackProgressBarCore // callback onDisplayListener onDisplayListener?.run { if (onDisplayId != onDisplayIdDefault) { this.onShown(snackProgressBarCore.getSnackProgressBar(), onDisplayId) } } } override fun onDismissed(snackProgressBarCore: SnackProgressBarCore, event: Int) { // remove overlayLayout snackProgressBarCore.removeOverlayLayout() // reset current currentCore = null // callback onDisplayListener onDisplayListener?.run { if (onDisplayId != onDisplayIdDefault) { this.onDismissed(snackProgressBarCore.getSnackProgressBar(), onDisplayId) } } // play next if this item is dismissed automatically later if (finalDuration != LENGTH_INDEFINITE) { nextQueue() } } }) // reset queue if this is last item in queue with LENGTH_INDEFINITE if (duration == LENGTH_INDEFINITE) { resetQueue() } snackProgressBarCore.show() } else { // else, queue is done resetQueue() } } /** * Play the next item in queue. */ private fun nextQueue() { playQueue(currentQueue + 1) } /** * Reset queue. */ private fun resetQueue() { currentQueue = 0 queueBars.clear() queueOnDisplayIds.clear() queueDurations.clear() } /** * Loop and crawl up the providedView hierarchy to find a suitable parent providedView. * * @param providedView View to hold the SnackProgressBar. * If possible, it should be the root providedView of the activity and can be any type of layout. * @return Suitable parent providedView of the activity. */ private fun findSuitableParent(providedView: View): ViewGroup { var view: View? = providedView var fallback: ViewGroup? = null do { if (view is CoordinatorLayout) { // use the CoordinatorLayout found return view } else if (view is FrameLayout) { if (view.id == android.R.id.content) { // use the content providedView since CoordinatorLayout not found return view } else { // Non-content providedView, but use it as fallback fallback = view } } if (view != null) { // loop and crawl up the providedView hierarchy and try to find a parent val parent = view.parent view = if (parent is View) parent else null } } while (view != null) // use fallback since CoordinatorLayout and other alternative not found fallback?.run { return this } ?: throw IllegalArgumentException("No suitable parent found from the given view. " + "Please provide a valid view.") } } ================================================ FILE: app/src/main/java/knf/kuma/database/BaseConverter.kt ================================================ package knf.kuma.database import android.net.Uri import androidx.room.TypeConverter import com.google.gson.Gson import com.google.gson.reflect.TypeToken import knf.kuma.videoservers.Headers class BaseConverter { @TypeConverter fun booleanToInt(b: Boolean): Int { return if (b) 1 else 0 } @TypeConverter fun intToBoolean(i: Int): Boolean { return i == 1 } @TypeConverter fun uriToString(uri: Uri): String { return uri.toString() } @TypeConverter fun stringToUri(s: String): Uri { return Uri.parse(s) } @TypeConverter fun headersToString(headers: Headers?): String { return Gson().toJson(headers, object : TypeToken() { }.type) } @TypeConverter fun stringToHeader(json: String?): Headers? { return Gson().fromJson(json, object : TypeToken() { }.type) } } ================================================ FILE: app/src/main/java/knf/kuma/database/CacheDB.kt ================================================ package knf.kuma.database import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import knf.kuma.App import knf.kuma.database.dao.AchievementsDAO import knf.kuma.database.dao.AnimeDAO import knf.kuma.database.dao.ChaptersDAO import knf.kuma.database.dao.DownloadsDAO import knf.kuma.database.dao.ExplorerDAO import knf.kuma.database.dao.FavsDAO import knf.kuma.database.dao.GenresDAO import knf.kuma.database.dao.NotificationDAO import knf.kuma.database.dao.PlayerStateDAO import knf.kuma.database.dao.QueueDAO import knf.kuma.database.dao.RecentModelsDAO import knf.kuma.database.dao.RecentsDAO import knf.kuma.database.dao.RecordsDAO import knf.kuma.database.dao.SeeingDAO import knf.kuma.database.dao.SeenDAO import knf.kuma.player.PlayerState import knf.kuma.pojos.Achievement import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.GenreStatusObject import knf.kuma.pojos.NotificationObj import knf.kuma.pojos.QueueObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeeingObject import knf.kuma.pojos.SeenObject import knf.kuma.recents.RecentModel @Database(entities = [RecentObject::class, RecentModel::class, PlayerState::class, AnimeObject::class, FavoriteObject::class, AnimeObject.WebInfo.AnimeChapter::class, SeenObject::class, NotificationObj::class, DownloadObject::class, RecordObject::class, SeeingObject::class, ExplorerObject::class, GenreStatusObject::class, QueueObject::class, Achievement::class], version = 20) abstract class CacheDB : RoomDatabase() { abstract fun recentsDAO(): RecentsDAO abstract fun recentModelsDAO(): RecentModelsDAO abstract fun animeDAO(): AnimeDAO abstract fun favsDAO(): FavsDAO abstract fun chaptersDAO(): ChaptersDAO abstract fun seenDAO(): SeenDAO abstract fun notificationDAO(): NotificationDAO abstract fun downloadsDAO(): DownloadsDAO abstract fun recordsDAO(): RecordsDAO abstract fun seeingDAO(): SeeingDAO abstract fun explorerDAO(): ExplorerDAO abstract fun queueDAO(): QueueDAO abstract fun genresDAO(): GenresDAO abstract fun achievementsDAO(): AchievementsDAO abstract fun playerStateDAO(): PlayerStateDAO companion object { private val MIGRATION_1_2: Migration = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `genrestatusobject` (`key` INTEGER NOT NULL, " + "`name` TEXT, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))") } } private val MIGRATION_2_3: Migration = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `queueobject` (`key` INTEGER, `id` INTEGER NOT NULL," + "`number` TEXT, `eid` TEXT,`isFile` INTEGER NOT NULL,`link` TEXT,`name` TEXT,`aid` TEXT,`time` INTEGER NOT NULL, PRIMARY KEY (`id`))") } } private val MIGRATION_3_4: Migration = object : Migration(3, 4) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `queueobject` ADD COLUMN `uri` TEXT") } } private val MIGRATION_4_5: Migration = object : Migration(4, 5) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `explorerobject` ADD COLUMN `aid` TEXT") } } private val MIGRATION_5_6: Migration = object : Migration(5, 6) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `downloadobject` ADD COLUMN `headers` TEXT") } } private val MIGRATION_6_7: Migration = object : Migration(6, 7) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `downloadobject` ADD COLUMN `did` TEXT") database.execSQL("ALTER TABLE `downloadobject` ADD COLUMN `eta` TEXT") database.execSQL("ALTER TABLE `downloadobject` ADD COLUMN `speed` TEXT") } } private val MIGRATION_8_7: Migration = object : Migration(8, 7) { override fun migrate(database: SupportSQLiteDatabase) { } } private val MIGRATION_7_8: Migration = object : Migration(7, 8) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `downloadobject` ADD COLUMN `time` INTEGER NOT NULL DEFAULT 0") } } private val MIGRATION_8_9: Migration = object : Migration(8, 9) { override fun migrate(database: SupportSQLiteDatabase) { } } private val MIGRATION_9_10: Migration = object : Migration(9, 10) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `recentobject_tmp` (`key` INTEGER NOT NULL, `aid` TEXT, `eid` TEXT, `name` TEXT, `chapter` TEXT, `url` TEXT, `img` TEXT, PRIMARY KEY(`key`))") database.execSQL("DROP TABLE `recentobject`") database.execSQL("ALTER TABLE `recentobject_tmp` RENAME TO `recentobject`") } } private val MIGRATION_10_11: Migration = object : Migration(10, 11) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `seeingobject` ADD COLUMN `state` INTEGER NOT NULL DEFAULT 1") } } private val MIGRATION_11_12: Migration = object : Migration(11, 12) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `achivement` (" + "`key` INTEGER NOT NULL, " + "`name` TEXT NOT NULL, " + "`description` TEXT NOT NULL, " + "`icon` INTEGER NOT NULL, " + "`points` INTEGER NOT NULL, " + "`isSecret` INTEGER NOT NULL, " + "`group` TEXT, " + "`time` INTEGER NOT NULL, " + "`count` INTEGER NOT NULL, " + "`goal` INTEGER NOT NULL, " + "`isUnlocked` INTEGER NOT NULL, " + "PRIMARY KEY(`key`))") } } private val MIGRATION_12_13: Migration = object : Migration(12, 13) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `achivement_tmp` (" + "`key` INTEGER NOT NULL, " + "`name` TEXT NOT NULL, " + "`description` TEXT NOT NULL, " + "`points` INTEGER NOT NULL, " + "`isSecret` INTEGER NOT NULL, " + "`group` TEXT, " + "`time` INTEGER NOT NULL, " + "`count` INTEGER NOT NULL, " + "`goal` INTEGER NOT NULL, " + "`isUnlocked` INTEGER NOT NULL, " + "PRIMARY KEY(`key`))") database.execSQL("INSERT INTO `achivement_tmp` (`key`,`name`,`description`,`points`,`isSecret`,`group`,`time`,`count`,`goal`,`isUnlocked`) SELECT `key`,`name`,`description`,`points`,`isSecret`,`group`,`time`,`count`,`goal`,`isUnlocked` FROM `achivement`") database.execSQL("DROP TABLE `achivement`") database.execSQL("ALTER TABLE `achivement_tmp` RENAME TO `achievement`") } } private val MIGRATION_13_14: Migration = object : Migration(13, 14) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `animeobject` ADD COLUMN `followers` TEXT") } } private val MIGRATION_14_15: Migration = object : Migration(14, 15) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `downloadobject` ADD COLUMN `server` TEXT DEFAULT 'desconocido'") } } private val MIGRATION_15_16: Migration = object : Migration(15, 16) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `seenobject` (" + "`eid` TEXT NOT NULL, " + "`aid` TEXT NOT NULL, " + "`number` TEXT NOT NULL, " + "PRIMARY KEY(`eid`))") } } private val MIGRATION_16_17: Migration = object : Migration(16, 17) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `achievement` ADD COLUMN `isRevealed` INTEGER NOT NULL DEFAULT 0") } } private val MIGRATION_17_18: Migration = object : Migration(17, 18) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `recentmodel` (" + "`key` INTEGER NOT NULL, " + "`aid` TEXT NOT NULL, " + "`name` TEXT NOT NULL, " + "`chapter` TEXT NOT NULL, " + "`chapterUrl` TEXT NOT NULL, " + "`img` TEXT NOT NULL, " + "PRIMARY KEY(`key`))") } } private val MIGRATION_18_19: Migration = object : Migration(18, 19) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `playerstate` (" + "`title` TEXT NOT NULL, " + "`position` INTEGER NOT NULL, " + "PRIMARY KEY(`title`))") } } private val MIGRATION_19_20: Migration = object : Migration(19, 20) { override fun migrate(database: SupportSQLiteDatabase) { } } val INSTANCE: CacheDB by lazy { init(App.context) } private fun init(context: Context): CacheDB = Room.databaseBuilder(context, CacheDB::class.java, "cache-db") .addMigrations( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_7, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20 ).build() fun createAndGet(context: Context): CacheDB { return Room.databaseBuilder(context, CacheDB::class.java, "cache-db") .addMigrations( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20 ).build() } } } ================================================ FILE: app/src/main/java/knf/kuma/database/CacheDBWrap.java ================================================ package knf.kuma.database; public class CacheDBWrap { public static CacheDB INSTANCE = CacheDB.Companion.getINSTANCE(); } ================================================ FILE: app/src/main/java/knf/kuma/database/EADB.kt ================================================ package knf.kuma.database import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import knf.kuma.App import knf.kuma.database.dao.EaDAO import knf.kuma.pojos.EAObject @Database(entities = [EAObject::class], version = 1) abstract class EADB : RoomDatabase() { abstract fun eaDAO(): EaDAO companion object { val INSTANCE: EADB by lazy { init(App.context) } private fun init(context: Context): EADB = Room.databaseBuilder(context, EADB::class.java, "ee-db") .allowMainThreadQueries() .build() } } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/AchievementsDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import androidx.room.Update import knf.kuma.database.BaseConverter import knf.kuma.pojos.Achievement @Dao @TypeConverters(BaseConverter::class) interface AchievementsDAO { @get:Query("SELECT * FROM achievement") val all: List @get:Query("SELECT * FROM achievement WHERE isUnlocked = 1") val allCompleted: List @get:Query("SELECT SUM(points) FROM achievement WHERE isUnlocked = 1") val totalUnlockedPoints: LiveData @get:Query("SELECT SUM(points) FROM achievement") val totalPoints: Int @get:Query("SELECT * FROM achievement") val allListener: LiveData> @get:Query("SELECT * FROM achievement WHERE isUnlocked = 0 AND count >= goal AND NOT goal = 0") val completionListener: LiveData> @get:Query("SELECT COUNT(*) FROM achievement") val totalAchievements: Int @get:Query("SELECT * FROM achievement WHERE isUnlocked = 1 ORDER BY time ASC") val completedAchievements: List @Query("SELECT * FROM achievement WHERE isUnlocked = :isUnlocked ORDER BY points ASC, name") fun achievementList(isUnlocked: Int): LiveData> @Query("SELECT * FROM achievement WHERE `key`=:key") fun find(key: Int): Achievement? @Query("SELECT * FROM achievement WHERE `key` IN (:keys)") fun find(vararg keys: Int): List @Query("SELECT * FROM achievement WHERE `key` IN (:keys)") fun find(keys: List): List @Query("SELECT isUnlocked FROM achievement WHERE `key`=:key LIMIT 1") fun isUnlocked(key: Int): Boolean @Insert(onConflict = OnConflictStrategy.IGNORE) fun insert(vararg achievements: Achievement) @Insert(onConflict = OnConflictStrategy.REPLACE) fun update(achievements: List) @Update fun update(achievements: Achievement) @Query("DELETE FROM achievement") fun nuke() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/AnimeDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import androidx.room.TypeConverters import androidx.room.Update import knf.kuma.backup.objects.AnimeChapters import knf.kuma.database.BaseConverter import knf.kuma.directory.DirObject import knf.kuma.emision.AnimeSubObject import knf.kuma.pojos.AnimeObject import knf.kuma.random.RandomObject import knf.kuma.recommended.AnimeShortObject import knf.kuma.search.SearchAdvObject import knf.kuma.search.SearchObject import knf.kuma.slices.AnimeSliceObject import knf.kuma.tv.search.BasicAnimeObject @Dao @TypeConverters(BaseConverter::class, AnimeObject.Converter::class) interface AnimeDAO { @get:Query("SELECT * FROM AnimeObject") val all: List @get:Query("SELECT `key`,aid,name,link FROM AnimeObject ORDER BY name") val allSearch: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject ORDER BY name") val allLive: LiveData> @get:Query("SELECT link FROM AnimeObject WHERE state LIKE 'En emisión'") val allLinksInEmission: List @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Anime' ORDER BY name") val animeDir: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Anime' ORDER BY rate_stars DESC") val animeDirVotes: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Anime' AND state LIKE 'En emisión' ORDER BY rate_stars DESC, rate_count+0 DESC LIMIT 10") val emissionVotesLimited: LiveData> @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Anime' ORDER BY rate_stars DESC, rate_count+0 DESC LIMIT 20") val allVotesLimited: LiveData> @Query("SELECT `key`,name,link,aid,img,type FROM AnimeObject WHERE aid IN (:aids) ORDER BY RANDOM() LIMIT 10") fun animesWithIDRandom(aids: List): List @Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE aid IN (:aids) ORDER BY RANDOM() LIMIT 15") fun animesDirWithIDRandom(aids: List): List @Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE aid IN (:aids) ORDER BY RANDOM()") fun animesDirWithIDRandomNL(aids: List): List @Query("SELECT aid FROM animeobject WHERE link = :link") fun findAid(link: String): String? @Query("SELECT aid FROM animeobject WHERE name = :name") fun findAidByName(name: String): String? @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Anime' ORDER BY `key` ASC") val animeDirID: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Anime' ORDER BY `key` DESC") val animeDirAdded: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Anime' ORDER BY followers+0 DESC") val animeDirFollowers: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'OVA' OR type LIKE '%special' ORDER BY name") val ovaDir: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'OVA' OR type LIKE '%special' ORDER BY rate_stars DESC") val ovaDirVotes: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'OVA' OR type LIKE '%special' ORDER BY `key` ASC") val ovaDirID: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'OVA' OR type LIKE '%special' ORDER BY `key` DESC") val ovaDirAdded: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'OVA' OR type LIKE '%special' ORDER BY followers+0 DESC") val ovaDirFollowers: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Película' ORDER BY name") val movieDir: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Película' ORDER BY rate_stars DESC") val movieDirVotes: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Película' ORDER BY `key` ASC") val movieDirID: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Película' ORDER BY `key` DESC") val movieDirAdded: DataSource.Factory @get:Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE type LIKE 'Película' ORDER BY followers+0 DESC") val movieDirFollowers: DataSource.Factory @get:Query("SELECT count(*) FROM AnimeObject") val count: Int @get:Query("SELECT count(*) FROM AnimeObject") val countLive: LiveData @Query("SELECT count(*) FROM AnimeObject") fun init(): Int @Query("SELECT * FROM AnimeObject WHERE aid = :aid") fun getAnimeByAid(aid: String): AnimeObject? @Query("SELECT `key`,aid,img,link,name,type FROM AnimeObject WHERE aid IN (:aids) ORDER BY name") fun getAnimesByAids(aids: List): List @Query("SELECT * FROM AnimeObject WHERE link LIKE :link") fun getAnimeRaw(link: String): AnimeObject? @Query("SELECT `key`,name,link,aid,type FROM AnimeObject ORDER BY RANDOM() LIMIT :limit") fun getRandom(limit: Int): List @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE state LIKE 'En emisión' AND day = :day AND aid NOT IN (:list) ORDER BY name") fun getByDay(day: Int, list: Set): LiveData> @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE state LIKE 'En emisión' AND day = :day ORDER BY name") fun getByDay(day: Int): LiveData> @Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE state LIKE 'En emisión' AND day = :day ORDER BY name") fun getByDayDir(day: Int): LiveData> @Query("SELECT `key`,aid,name,link,rate_stars,type FROM AnimeObject WHERE state LIKE 'En emisión' ORDER BY name") fun getAllEmission(): List @Query("SELECT count(*) FROM AnimeObject WHERE state = 'En emisión' AND NOT day = 0 AND aid NOT IN (:list)") fun getInEmission(list: Set): LiveData @Query("SELECT `key`,link,name,aid FROM AnimeObject WHERE state LIKE 'En emisión' AND day = :day AND aid NOT IN (:list) ORDER BY name") fun getByDayDirect(day: Int, list: Set): MutableList @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE name LIKE :query ORDER BY name") fun getSearch(query: String): DataSource.Factory @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE name LIKE :query ORDER BY name") fun getSearchList(query: String): LiveData> @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE aid LIKE :query ORDER BY name") fun getSearchID(query: String): DataSource.Factory @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE genres LIKE :genres ORDER BY name") fun getSearchG(genres: String): DataSource.Factory @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE genres LIKE :genre ORDER BY name") fun getAllGenre(genre: String): DataSource.Factory @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE genres LIKE :genre ORDER BY name") fun getAllGenreLive(genre: String): LiveData> @Query("SELECT aid FROM AnimeObject WHERE genres LIKE :genres") fun getAidsByGenres(genres: String): MutableList @Query("SELECT aid FROM AnimeObject WHERE genres LIKE :genres ORDER BY RANDOM() LIMIT 15") fun getAidsByGenresLimited(genres: String): MutableList @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE name LIKE :query AND genres LIKE :genres ORDER BY name") fun getSearchTG(query: String, genres: String): DataSource.Factory @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE name LIKE :query AND state LIKE :state ORDER BY name") fun getSearchS(query: String, state: String): DataSource.Factory @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE name LIKE :query AND state LIKE :state AND genres LIKE :genres ORDER BY name") fun getSearchSG(query: String, state: String, genres: String): DataSource.Factory @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE name LIKE :query AND type LIKE :type ORDER BY name") fun getSearchTY(query: String, type: String): DataSource.Factory @Query("SELECT `key`,aid,name,link FROM AnimeObject WHERE name LIKE :query AND type LIKE :type AND genres LIKE :genres ORDER BY name") fun getSearchTYG(query: String, type: String, genres: String): DataSource.Factory @Query("SELECT `key`,link,name,aid,fileName FROM AnimeObject WHERE fileName IN (:names) OR aid IN (:names)") fun getAllByFile(names: MutableList): MutableList @Query("SELECT `key`,name,link,aid,genres FROM AnimeObject WHERE name LIKE :name ORDER BY name COLLATE NOCASE LIMIT 5") fun getByName(name: String): MutableList @Query("SELECT count(*) FROM AnimeObject WHERE link LIKE :link") fun existLink(link: String): Boolean @Query("SELECT count(*) FROM AnimeObject WHERE aid = :aid") fun existAid(aid: String): Boolean @Query("SELECT count(*) FROM animeobject WHERE aid = :aid AND genres LIKE :genre") fun hasGenre(aid: String, genre: String): Boolean @Query("SELECT count(*) FROM animeobject WHERE aid = :aid AND state = 'Finalizado'") fun isCompleted(aid: String): Boolean @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE sid = :sid") fun getBySid(sid: String): SearchObject? @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE name = :name") fun getObjByName(name: String): SearchObject? @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE link = :link") fun getByLink(link: String): SearchObject? @Query("SELECT `key`,name,link,aid,img,type FROM AnimeObject WHERE aid LIKE :aid") fun getByAid(aid: String): SearchAdvObject? @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE aid LIKE :aid") fun getByAidSimple(aid: String): SearchObject? @Query("SELECT * FROM AnimeObject WHERE aid LIKE :aid") fun getFullByAid(aid: String): AnimeObject? @Query("SELECT aid,chapters FROM AnimeObject WHERE aid = :aid") fun getChaptersByAid(aid: String): AnimeChapters @Query("SELECT `key`,name,link,aid FROM AnimeObject WHERE aid = :aid") fun getSOByAid(aid: String): SearchObject? @Query("SELECT count(*) FROM AnimeObject WHERE `key` LIKE :aid") fun getCount(aid: Int): Int @Transaction fun hasRange(aidF: String, aidL: String): Boolean { return existAid(aidF) && existAid(aidL) } @Update fun updateAnime(animeObject: AnimeObject) @Update fun updateAnimes(animeObjects: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(animeObject: AnimeObject) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(objects: List) @Query("DELETE FROM animeobject") fun nuke() @Query("DELETE FROM animeobject WHERE UPPER(genres) LIKE '%ECCHI%'") fun nukeEcchi() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/ChaptersDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import knf.kuma.database.BaseConverter import knf.kuma.pojos.AnimeObject @Dao @TypeConverters(BaseConverter::class) interface ChaptersDAO { @get:Query("SELECT * FROM animechapter") val all: MutableList @get:Query("SELECT count(*) FROM animechapter") val countLive: LiveData @get:Query("SELECT count(*) FROM animechapter") val count: Int @Query("SELECT * FROM animechapter WHERE eid = :eid LIMIT 1") fun chapterSeen(eid: String): LiveData @Query("SELECT count(*) FROM animechapter WHERE eid = :eid") fun chapterIsSeen(eid: String): Boolean @Query("SELECT * FROM animechapter WHERE eid IN (:eids) ORDER BY eid+0 DESC LIMIT 1") fun getLast(eids: MutableList): AnimeObject.WebInfo.AnimeChapter? @Query("SELECT * FROM animechapter WHERE eid IN (:eids)") fun getAllFrom(eids: MutableList): List @Query("SELECT * FROM animechapter WHERE aid = :aid ORDER BY `key` DESC LIMIT 1") fun getLastByAid(aid: String): AnimeObject.WebInfo.AnimeChapter? @Insert(onConflict = OnConflictStrategy.REPLACE) fun addChapter(chapter: AnimeObject.WebInfo.AnimeChapter) @Insert(onConflict = OnConflictStrategy.REPLACE) fun addAll(list: List) @Delete fun deleteChapter(chapter: AnimeObject.WebInfo.AnimeChapter) @Query("DELETE FROM animechapter") fun clear() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/DownloadsDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import knf.kuma.pojos.DownloadObject @Dao interface DownloadsDAO { @get:Query("SELECT * FROM downloadobject") val all: DataSource.Factory @get:Query("SELECT eid FROM downloadobject") val allEids: List @get:Query("SELECT * FROM downloadobject WHERE state < 4") val allRaw: List @get:Query("SELECT * FROM downloadobject WHERE state<=0") val active: LiveData> @Query("SELECT * FROM downloadobject WHERE eid LIKE :eid") fun getByEid(eid: String): DownloadObject? @Query("SELECT COUNT(*) FROM downloadobject WHERE eid = :eid") fun countByEid(eid: String): Int @Transaction fun existByEid(eid: String): Boolean = countByEid(eid) > 0 @Query("SELECT * FROM downloadobject WHERE did = :did") fun getByDid(did: Int): DownloadObject? @Query("SELECT * FROM downloadobject WHERE file LIKE :name") fun getByFile(name: String): DownloadObject? @Query("SELECT * FROM downloadobject WHERE eid = :eid") fun getLiveByEid(eid: String): LiveData @Query("SELECT * FROM downloadobject WHERE `key` LIKE :key") fun getLiveByKey(key: Int): LiveData @Query("SELECT count(*) FROM downloadobject WHERE state=-1") fun countPending(): Int @Query("SELECT count(*) FROM downloadobject WHERE state=-1 OR state=0") fun countActive(): Int @Query("DELETE FROM downloadobject WHERE eid LIKE :eid") fun deleteByEid(eid: String) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(downloadObject: DownloadObject) @Update(onConflict = OnConflictStrategy.REPLACE) fun update(downloadObject: DownloadObject) @Delete fun delete(downloadObject: DownloadObject) @Delete fun delete(downloadObjects: List) } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/EaDAO.kt ================================================ package knf.kuma.database.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import knf.kuma.database.BaseConverter import knf.kuma.pojos.EAObject @Dao @TypeConverters(BaseConverter::class) interface EaDAO { @get:Query("SELECT * FROM eaobject") val all: List @Query("SELECT count(*) FROM eaobject WHERE code=:code") fun isUnlocked(code: Int): Boolean @Insert(onConflict = OnConflictStrategy.IGNORE) fun unlock(eaObject: EAObject) @Insert(onConflict = OnConflictStrategy.IGNORE) fun unlock(eaObjects: List) } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/ExplorerDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import knf.kuma.pojos.ExplorerObject @Dao interface ExplorerDAO { @get:Query("SELECT * FROM explorerobject ORDER BY name") val all: LiveData> @Query("SELECT * FROM explorerobject WHERE fileName LIKE :file") fun getItem(file: String): LiveData @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(list: List) @Update fun update(explorerObject: ExplorerObject) @Delete fun delete(explorerObject: ExplorerObject) @Query("DELETE FROM explorerobject") fun deleteAll() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/FavsDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import knf.kuma.database.BaseConverter import knf.kuma.pojos.FavoriteObject import kotlinx.coroutines.flow.Flow @Dao @TypeConverters(BaseConverter::class) interface FavsDAO { @get:Query("SELECT * FROM favoriteobject ORDER BY name") val all: LiveData> @get:Query("SELECT * FROM favoriteobject ORDER BY name") val allRaw: MutableList @get:Query("SELECT aid FROM favoriteobject") val allAids: List @get:Query("SELECT * FROM favoriteobject GROUP BY category ORDER BY category") val categories: MutableList @get:Query("SELECT * FROM favoriteobject ORDER BY aid + 0 ASC") val allID: LiveData> @get:Query("SELECT * FROM favoriteobject ORDER BY category") val byCategory: MutableList @get:Query("SELECT count(*) FROM favoriteobject") val count: Int @get:Query("SELECT count(*) FROM favoriteobject") val countLive: LiveData @get:Query("SELECT count(*) FROM favoriteobject") val countFlow: Flow @Query("SELECT * FROM favoriteobject WHERE category NOT LIKE :category ORDER BY name") fun getNotInCategory(category: String): MutableList @Query("SELECT * FROM favoriteobject WHERE category LIKE :category ORDER BY name") fun getAllInCategory(category: String): MutableList @Query("SELECT count(*) FROM favoriteobject WHERE `key` = :key") fun isFav(key: Int): Boolean @Query("SELECT count(*) FROM favoriteobject WHERE aid = :aid") fun isFavAid(aid: String): Boolean @Query("SELECT count(*) FROM favoriteobject WHERE name = :name") fun isFavName(name: String): Boolean @Query("SELECT count(*) FROM favoriteobject WHERE `key` = :key") fun isFavLive(key: Int): LiveData @Query("SELECT * FROM favoriteobject WHERE `key` = :key") fun favObserver(key: Int): LiveData @Insert(onConflict = OnConflictStrategy.REPLACE) fun addFav(favoriteObject: FavoriteObject) @Insert(onConflict = OnConflictStrategy.REPLACE) fun addAll(list: List) @Delete fun deleteFav(favoriteObject: FavoriteObject) @Query("DELETE FROM favoriteobject") fun clear() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/GenresDAO.kt ================================================ package knf.kuma.database.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import knf.kuma.pojos.GenreStatusObject /** * Created by jordy on 26/03/2018. */ @Dao interface GenresDAO { @get:Query("SELECT * FROM genrestatusobject WHERE count > 0 ORDER BY count DESC LIMIT 3") val top: MutableList @get:Query("SELECT * FROM genrestatusobject WHERE count < 0 ORDER BY name DESC") val blacklist: MutableList @get:Query("SELECT * FROM genrestatusobject ORDER BY name") val all: MutableList @get:Query("SELECT * FROM genrestatusobject WHERE count > 0 ORDER BY count DESC") val ranking: MutableList @Query("SELECT * FROM genrestatusobject WHERE name LIKE :name") fun getStatus(name: String): GenreStatusObject? @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStatus(statusObject: GenreStatusObject) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStatus(statusObjects: List) @Update fun update(list: List) @Query("DELETE FROM genrestatusobject WHERE count >= 0") fun reset() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/NotificationDAO.kt ================================================ package knf.kuma.database.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import knf.kuma.database.BaseConverter import knf.kuma.pojos.NotificationObj @Dao @TypeConverters(BaseConverter::class) interface NotificationDAO { @get:Query("SELECT * FROM notificationobj") val all: MutableList @Query("SELECT * FROM notificationobj WHERE type=:type") fun getByType(type: Int): MutableList @Insert(onConflict = OnConflictStrategy.REPLACE) fun add(obj: NotificationObj) @Delete fun delete(obj: NotificationObj) @Query("DELETE FROM notificationobj") fun clear() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/PlayerStateDAO.kt ================================================ package knf.kuma.database.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import knf.kuma.player.PlayerState @Dao interface PlayerStateDAO { @Query("SELECT * FROM playerstate WHERE title = :title") fun find(title: String): PlayerState? @Insert(onConflict = OnConflictStrategy.REPLACE) fun set(state: PlayerState) } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/QueueDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import androidx.room.Update import knf.kuma.database.BaseConverter import knf.kuma.pojos.QueueObject @Dao @TypeConverters(BaseConverter::class) interface QueueDAO { @get:Query("SELECT MIN(aid) AS id,`key`,aid,name,number,eid,isFile,uri,time,link FROM queueobject ORDER BY name") val allAlone: LiveData> @get:Query("SELECT * FROM queueobject ORDER BY name") val all: LiveData> @get:Query("SELECT * FROM queueobject ORDER BY name") val allRaw: MutableList @get:Query("SELECT * FROM queueobject ORDER BY time ASC") val allAsort: LiveData> @get:Query("SELECT count(*) FROM queueobject") val countLive: LiveData @Query("SELECT count(*) FROM queueobject WHERE eid = :eid") fun isInQueue(eid: String): Boolean @Query("SELECT count(*) FROM queueobject WHERE eid = :eid") fun isInQueueLive(eid: String): LiveData @Query("SELECT count(*) FROM queueobject WHERE aid LIKE :aid") fun countAlone(aid: String): Int @Query("SELECT * FROM queueobject WHERE aid = :aid ORDER BY eid+0 ASC") fun getByAid(aid: String): LiveData> @Query("SELECT * FROM queueobject WHERE aid = :aid ORDER BY eid+0 ASC") fun getByAidUnique(aid: String): MutableList @Query("SELECT * FROM queueobject WHERE aid = :aid ORDER BY eid+0 ASC") fun getAllByAid(aid: String): List @Insert(onConflict = OnConflictStrategy.REPLACE) fun add(queueObject: QueueObject) @Insert(onConflict = OnConflictStrategy.REPLACE) fun add(queueObjects: List) @Update fun update(vararg objects: QueueObject) @Delete fun remove(queueObject: QueueObject) @Delete fun remove(list: MutableList) @Query("DELETE FROM queueobject WHERE aid LIKE :aid") fun removeByID(aid: String) @Query("DELETE FROM queueobject WHERE eid LIKE :eid") fun removeByEID(eid: String) @Query("DELETE FROM queueobject") fun nuke() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/RecentModelsDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import knf.kuma.recents.RecentModel import kotlinx.coroutines.flow.Flow @Dao interface RecentModelsDAO { @get:Query("SELECT * FROM recentmodel ORDER BY `key`") val allLive: LiveData> @get:Query("SELECT * FROM recentmodel ORDER BY `key`") val allFlow: Flow> @get:Query("SELECT * FROM recentmodel ORDER BY `key`") val all: List @Query("DELETE FROM recentmodel") fun clear() @Insert(onConflict = OnConflictStrategy.REPLACE) fun setCache(objects: List) } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/RecentsDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import knf.kuma.pojos.RecentObject import knf.kuma.recents.RecentModelCh import kotlinx.coroutines.flow.Flow @Dao interface RecentsDAO { @get:Query("SELECT * FROM recentobject ORDER BY `key`") val objects: LiveData> @get:Query("SELECT * FROM recentobject ORDER BY `key`") val objectsFlow: Flow> @get:Query("SELECT * FROM recentobject ORDER BY `key`") val all: MutableList @get:Query("SELECT name, chapter, url, aid, eid FROM recentobject ORDER BY `key`") val allSimple: MutableList @Query("DELETE FROM recentobject") fun clear() @Insert(onConflict = OnConflictStrategy.REPLACE) fun setCache(objects: MutableList) } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/RecordsDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import knf.kuma.pojos.RecordObject @Dao interface RecordsDAO { @get:Query("SELECT * FROM recordobject ORDER BY date DESC") val all: DataSource.Factory @get:Query("SELECT * FROM recordobject ORDER BY date DESC") val allLive: LiveData> @get:Query("SELECT * FROM recordobject ORDER BY date DESC") val allRaw: MutableList @Insert(onConflict = OnConflictStrategy.REPLACE) fun add(recordObject: RecordObject) @Insert(onConflict = OnConflictStrategy.REPLACE) fun addAll(list: List) @Delete fun delete(recordObject: RecordObject) @Query("DELETE FROM recordobject") fun clear() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/SeeingDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import androidx.room.Update import knf.kuma.database.BaseConverter import knf.kuma.pojos.SeeingObject @Dao @TypeConverters(BaseConverter::class) interface SeeingDAO { @get:Query("SELECT * FROM seeingobject ORDER BY title") val all: LiveData> @get:Query("SELECT * FROM seeingobject ORDER BY title") val allPaging: DataSource.Factory @get:Query("SELECT * FROM seeingobject ORDER BY title") val allRaw: MutableList @get:Query("SELECT aid FROM seeingobject") val allAids: List @Query("SELECT * FROM seeingobject WHERE state=:state ORDER BY title") fun getLiveByState(state: Int): LiveData> @Query("SELECT count(*) FROM seeingobject WHERE state=:state") suspend fun countByState(state: Int): Int @Query("SELECT * FROM seeingobject WHERE state=:state ORDER BY title") fun getLiveByStatePaging(state: Int): DataSource.Factory @get:Query("SELECT count(*) FROM seeingobject") val countLive: LiveData @get:Query("SELECT count(*) FROM seeingobject") val countAll: Int @get:Query("SELECT count(*) FROM seeingobject WHERE state=1") val countWatchingLive: LiveData @get:Query("SELECT count(*) FROM seeingobject WHERE state=3") val countCompletedLive: LiveData @get:Query("SELECT count(*) FROM seeingobject WHERE state=4") val countDroppedLive: LiveData @Query("SELECT * FROM seeingobject WHERE aid LIKE :aid") fun getByAid(aid: String): SeeingObject? @Query("SELECT * FROM seeingobject WHERE state IN (:states) ORDER BY RANDOM() LIMIT 10") fun getAllWState(vararg states: Int): LiveData> @Query("SELECT count(*) FROM seeingobject WHERE aid = :aid AND state>0 AND state <3") fun isSeeing(aid: String): Boolean @Query("SELECT count(*) FROM seeingobject WHERE aid = :aid") fun isSeeingAll(aid: String): Boolean @Query("SELECT count(*) FROM seeingobject WHERE aid IN (:list) AND state=3") fun isAnimeCompleted(list: List): LiveData @Insert(onConflict = OnConflictStrategy.REPLACE) fun add(seeingObject: SeeingObject) @Insert(onConflict = OnConflictStrategy.REPLACE) fun addAll(list: List) @Update fun update(seeingObject: SeeingObject) @Delete fun remove(seeingObject: SeeingObject) @Query("DELETE FROM seeingobject") fun clear() } ================================================ FILE: app/src/main/java/knf/kuma/database/dao/SeenDAO.kt ================================================ package knf.kuma.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.TypeConverters import knf.kuma.database.BaseConverter import knf.kuma.pojos.SeenObject @Dao @TypeConverters(BaseConverter::class) interface SeenDAO { @get:Query("SELECT * FROM seenobject") val all: MutableList @get:Query("SELECT count(*) FROM seenobject") val countLive: LiveData @get:Query("SELECT count(*) FROM seenobject") val count: Int @Query("SELECT * FROM seenobject WHERE aid = :aid AND number = :number LIMIT 1") fun chapterSeen(aid: String, number: String): LiveData @Query("SELECT count(*) FROM seenobject WHERE aid = :aid AND number = :number LIMIT 1") fun chapterIsSeenLive(aid: String, number: String): LiveData @Query("SELECT count(*) FROM seenobject WHERE aid = :aid AND number = :number") fun chapterIsSeen(aid: String, number: String): Boolean @Query("SELECT * FROM seenobject WHERE eid IN (:eids) ORDER BY eid+0 DESC LIMIT 1") fun getLast(eids: List): SeenObject? @Query("SELECT * FROM seenobject WHERE aid = :aid ORDER BY eid+0 DESC LIMIT 1") fun getLastByAid(aid: String): SeenObject? @Query("SELECT * FROM seenobject WHERE eid IN (:eids)") fun getAllFrom(eids: MutableList): List @Query("SELECT * FROM seenobject WHERE aid = :aid") fun getAllByAid(aid: String): List @Insert(onConflict = OnConflictStrategy.REPLACE) fun addChapter(chapter: SeenObject) @Insert(onConflict = OnConflictStrategy.REPLACE) fun addAll(list: List) @Query("DELETE FROM seenobject WHERE aid = :aid AND number = :number") fun deleteChapter(aid: String, number: String) @Query("DELETE FROM seenobject") fun clear() } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirManager.kt ================================================ package knf.kuma.directory import com.google.gson.Gson import com.google.gson.reflect.TypeToken import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.noCrash import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject import org.json.JSONObject import java.net.URL object DirManager { fun checkPreDir(forced: Boolean = false) { if (forced || CacheDB.INSTANCE.animeDAO().count < 3500) { doOnUIGlobal { DirectoryService.setStatus(DirectoryService.STATE_CACHED) } noCrash { val info = JSONObject(URL("https://ukiku.app/dirs/directoryInfo.json").readText()) for (index in 0 until info.keys().asSequence().count()) { val json = info.getJSONObject(index.toString()) if (!CacheDB.INSTANCE.animeDAO().hasRange(json.getString("idF"), json.getString("idL"))) { val sliceJson = URL("https://ukiku.app/dirs/directory$index.json").readText() val list: List = Gson().fromJson(sliceJson, object : TypeToken>() {}.type) CacheDB.INSTANCE.animeDAO().insertAll(list) } } PrefsUtil.isDirectoryFinished = true } } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirObject.kt ================================================ package knf.kuma.directory data class DirObject( var key: Int = 0, var aid: String = "", var name: String = "", var link: String? = null, var type: String = "", var rate_stars: String? = "" ) ================================================ FILE: app/src/main/java/knf/kuma/directory/DirObjectCompact.kt ================================================ package knf.kuma.directory import androidx.annotation.Keep import androidx.recyclerview.widget.DiffUtil import org.jsoup.nodes.Element import pl.droidsonroids.jspoon.ElementConverter import pl.droidsonroids.jspoon.annotation.Selector class DirObjectCompact { @Selector(value = ":root", converter = ImageExtractor::class) var aid: String = "" @Selector("h3.Title") var name: String = "" @Selector(value = ":root", converter = LinkExtractor::class) var link: String? = null override fun hashCode(): Int { return aid.hashCode() } override fun equals(other: Any?): Boolean { return other is DirObjectCompact && other.aid == aid } class LinkExtractor @Keep constructor() : ElementConverter { override fun convert(node: Element, selector: Selector): String { return "https://www3.animeflv.net${node.select("a").attr("href")}" } } class ImageExtractor @Keep constructor() : ElementConverter { override fun convert(node: Element, selector: Selector): String { val img = node.select("figure img").first().let { when { it.hasAttr("data-cfsrc") -> it.attr("data-cfsrc") it.hasAttr("src") -> it.attr("src") else -> "/0.jpg" } } return "/(\\d+)\\.".toRegex().find(img)?.destructured?.component1() ?: "0" } } companion object { val DIFF = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DirObjectCompact, newItem: DirObjectCompact): Boolean = oldItem.aid == newItem.aid override fun areContentsTheSame(oldItem: DirObjectCompact, newItem: DirObjectCompact): Boolean = oldItem == newItem } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirPagerAdapter.kt ================================================ package knf.kuma.directory import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter class DirPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) { private val animes = DirectoryPageFragment[DirectoryPageFragment.DirType.ANIMES] private val ovas = DirectoryPageFragment[DirectoryPageFragment.DirType.OVAS] private val movies = DirectoryPageFragment[DirectoryPageFragment.DirType.MOVIES] override fun getPageTitle(position: Int): CharSequence? { return when (position) { 1 -> "OVA" 2 -> "PELICULA" else -> "ANIME" } } fun onChangeOrder() { animes.onChangeOrder() ovas.onChangeOrder() movies.onChangeOrder() } override fun getItem(position: Int): Fragment { return when (position) { 1 -> ovas 2 -> movies else -> animes } } override fun getCount(): Int { return 3 } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirPagerAdapterMaterial.kt ================================================ package knf.kuma.directory import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter class DirPagerAdapterMaterial(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { private val animes = DirectoryPageFragmentMaterial[DirectoryPageFragmentMaterial.DirType.ANIMES] private val ovas = DirectoryPageFragmentMaterial[DirectoryPageFragmentMaterial.DirType.OVAS] private val movies = DirectoryPageFragmentMaterial[DirectoryPageFragmentMaterial.DirType.MOVIES] override fun getPageTitle(position: Int): CharSequence? { return when (position) { 1 -> "OVA" 2 -> "PELICULA" else -> "ANIME" } } fun onChangeOrder() { animes.onChangeOrder() ovas.onChangeOrder() movies.onChangeOrder() } override fun getItem(position: Int): Fragment { return when (position) { 1 -> ovas 2 -> movies else -> animes } } override fun getCount(): Int { return 3 } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirPagerAdapterOnline.kt ================================================ package knf.kuma.directory import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter class DirPagerAdapterOnline(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { private val animes = DirectoryPageFragmentOnline[DirectoryPageFragmentMaterial.DirType.ANIMES.value] private val ovas = DirectoryPageFragmentOnline[DirectoryPageFragmentMaterial.DirType.OVAS.value] private val movies = DirectoryPageFragmentOnline[DirectoryPageFragmentMaterial.DirType.MOVIES.value] override fun getPageTitle(position: Int): CharSequence? { return when (position) { 1 -> "OVA" 2 -> "PELICULA" else -> "ANIME" } } fun onChangeOrder() { } override fun getItem(position: Int): Fragment { return when (position) { 1 -> ovas 2 -> movies else -> animes } } override fun getCount(): Int { return 3 } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryDataSource.kt ================================================ package knf.kuma.directory import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingSource import androidx.paging.PagingState import knf.kuma.commons.jsoupCookiesAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DirectoryDataSource(val type: String, val retryCallback: () -> Unit) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? = state.anchorPosition override suspend fun load(params: LoadParams): LoadResult { val page = params.key?: 1 try { val dir = withContext(Dispatchers.IO) { jsoupCookiesAdapter("https://www3.animeflv.net/browse?order=title&type[]=$type&page=$page", DirectoryPageCompact::class.java) } return LoadResult.Page(dir.list, null, if (dir.hasNext) page + 1 else null) }catch (e:Exception){ e.printStackTrace() retryCallback() return LoadResult.Error(e) } } } fun createDirectoryPagedList(type: String, retryCallback: () -> Unit) = Pager( config = PagingConfig(24), pagingSourceFactory = { DirectoryDataSource(type, retryCallback) } ).flow ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryFragment.kt ================================================ package knf.kuma.directory import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.safeDismiss import knf.kuma.commons.showSnackbar import knf.kuma.custom.BannerContainerView import knf.kuma.database.CacheDB import knf.kuma.directory.viewholders.DirMainFragmentHolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.support.v4.findOptional class DirectoryFragment : BottomFragment() { private var fragmentHolder: DirMainFragmentHolder? = null private var snackbar: Snackbar? = null override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (!PrefsUtil.isDirectoryFinished) { snackbar = activity?.findViewById(R.id.root)?.showSnackbar("Creando directorio...", Snackbar.LENGTH_INDEFINITE) CacheDB.INSTANCE.animeDAO().countLive.observe(viewLifecycleOwner, Observer { try { snackbar?.setText("Agregados... $it") } catch (e: Exception) { e.printStackTrace() } }) DirectoryService.getLiveStatus().observe(viewLifecycleOwner, Observer { when (it) { DirectoryService.STATE_VERIFYING -> snackbar?.setText("Verificando directorio...") DirectoryService.STATE_INTERRUPTED, DirectoryService.STATE_FINISHED -> snackbar?.safeDismiss() } }) } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_directory, container, false) fragmentHolder = DirMainFragmentHolder(view, childFragmentManager) EAHelper.enter1("D") return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (PrefsUtil.isDirectoryFinished) lifecycleScope.launch(Dispatchers.IO) { delay(1000) findOptional(R.id.adContainer)?.implBanner(AdsType.DIRECTORY_BANNER, true) } } override fun onDestroyView() { super.onDestroyView() snackbar?.safeDismiss() } fun onChangeOrder() { fragmentHolder?.onChangeOrder() } override fun onReselect() { EAHelper.enter1("D") fragmentHolder?.onReselect() } companion object { fun get(): DirectoryFragment { return DirectoryFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryFragmentMaterial.kt ================================================ package knf.kuma.directory import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.noCrash import knf.kuma.commons.safeDismiss import knf.kuma.custom.BannerContainerView import knf.kuma.directory.viewholders.DirMainFragmentMaterialHolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.support.v4.find class DirectoryFragmentMaterial : BottomFragment() { private var fragmentHolder: DirMainFragmentMaterialHolder? = null private var snackbar: Snackbar? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_directory_material, container, false) fragmentHolder = DirMainFragmentMaterialHolder(view, childFragmentManager) EAHelper.enter1("D") return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { delay(1000) noCrash { find(R.id.adContainer).implBanner(AdsType.DIRECTORY_BANNER, true) } } } override fun onDestroyView() { super.onDestroyView() snackbar?.safeDismiss() } fun onChangeOrder() { fragmentHolder?.onChangeOrder() } override fun onReselect() { EAHelper.enter1("D") fragmentHolder?.onReselect() } companion object { fun get(): DirectoryFragmentMaterial { return DirectoryFragmentMaterial() } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryPageAdapter.kt ================================================ package knf.kuma.directory import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load class DirectoryPageAdapter internal constructor(private val fragment: Fragment) : PagingDataAdapter(DIFF_CALLBACK), FastScrollRecyclerView.SectionedAdapter { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder(LayoutInflater.from(parent.context).inflate(getLayType(), parent, false)) } @LayoutRes private fun getLayType(): Int { return if (PrefsUtil.layType == "0") { R.layout.item_dir } else { R.layout.item_dir_grid } } override fun onBindViewHolder(holder: ItemHolder, position: Int) { if (fragment.context == null) return val animeObject = getItem(position) if (animeObject?.aid != null) { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.progressView.visibility = View.GONE holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnime.open(fragment, animeObject, holder.imageView, persist = false) } } else { holder.progressView.visibility = View.VISIBLE holder.textView.text = null } } override fun getSectionName(position: Int): String { return when (PrefsUtil.dirOrder) { 1 -> "\u2605${getItem(position)?.rate_stars ?: "?.?"}" 2 -> getItem(position)?.aid ?: "" 3 -> getItem(position)?.aid ?: "" else -> getItem(position)?.name?.first()?.uppercaseChar()?.toString() ?: "" } } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar by itemView.bind(R.id.progress) val textView: TextView by itemView.bind(R.id.title) } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DirObject, newItem: DirObject): Boolean { return oldItem.key == newItem.key && oldItem.link == newItem.link } override fun areContentsTheSame(oldItem: DirObject, newItem: DirObject): Boolean { return oldItem.name == newItem.name && oldItem.link == newItem.link } } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryPageAdapterMaterial.kt ================================================ package knf.kuma.directory import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load class DirectoryPageAdapterMaterial internal constructor(private val fragment: Fragment) : PagingDataAdapter(DIFF_CALLBACK), FastScrollRecyclerView.SectionedAdapter { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder(LayoutInflater.from(parent.context).inflate(getLayType(), parent, false)) } @LayoutRes private fun getLayType(): Int { return if (PrefsUtil.layType == "0") { R.layout.item_dir_material } else { R.layout.item_dir_grid_material } } override fun onBindViewHolder(holder: ItemHolder, position: Int) { if (fragment.context == null) return val animeObject = getItem(position) if (animeObject?.aid != null) { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.progressView.visibility = View.GONE holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, animeObject) } } else { holder.progressView.visibility = View.VISIBLE holder.textView.text = null } } override fun getSectionName(position: Int): String { return when (PrefsUtil.dirOrder) { 1 -> "\u2605${getItem(position)?.rate_stars ?: "?.?"}" 2 -> getItem(position)?.aid ?: "" 3 -> getItem(position)?.aid ?: "" else -> getItem(position)?.name?.first()?.uppercaseChar()?.toString() ?: "" } } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar by itemView.bind(R.id.progress) val textView: TextView by itemView.bind(R.id.title) } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DirObject, newItem: DirObject): Boolean { return oldItem.key == newItem.key && oldItem.link == newItem.link } override fun areContentsTheSame(oldItem: DirObject, newItem: DirObject): Boolean { return oldItem.name == newItem.name && oldItem.link == newItem.link } } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryPageAdapterOnline.kt ================================================ package knf.kuma.directory import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load class DirectoryPageAdapterOnline(private val fragment: Fragment) : PagingDataAdapter(DirObjectCompact.DIFF), FastScrollRecyclerView.SectionedAdapter { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder(LayoutInflater.from(parent.context).inflate(getLayType(), parent, false)) } @LayoutRes private fun getLayType(): Int { return if (PrefsUtil.layType == "0") { R.layout.item_dir_material } else { R.layout.item_dir_grid_material } } override fun onBindViewHolder(holder: ItemHolder, position: Int) { if (fragment.context == null) return val animeObject = getItem(position) if (animeObject?.aid != null) { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.progressView.visibility = View.GONE holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, animeObject, true) } } else { holder.progressView.visibility = View.VISIBLE holder.textView.text = null } } override fun getSectionName(position: Int): String { return getItem(position)?.name?.first()?.uppercaseChar()?.toString() ?: "" } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar by itemView.bind(R.id.progress) val textView: TextView by itemView.bind(R.id.title) } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryPageCompact.kt ================================================ package knf.kuma.directory import androidx.annotation.Keep import org.jsoup.nodes.Element import pl.droidsonroids.jspoon.ElementConverter import pl.droidsonroids.jspoon.annotation.Selector class DirectoryPageCompact { @Selector("article.Anime") var list: List = emptyList() @Selector(value = "ul.pagination", converter = NextConverter::class) var hasNext: Boolean = false class NextConverter @Keep constructor() : ElementConverter { override fun convert(node: Element, selector: Selector): Boolean { val pages = node.select("li") if (pages.size <= 1) return false val last = pages.last() val child = last.child(0) return !last.hasClass("disabled") && child.hasAttr("rel") && child.attr("href") != "#" } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryPageFragment.kt ================================================ package knf.kuma.directory import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.annotation.LayoutRes import androidx.annotation.UiThread import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.paging.PagingData import androidx.recyclerview.widget.RecyclerView import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.commons.PrefsUtil import knf.kuma.commons.verifyManager import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.jetbrains.anko.find class DirectoryPageFragment : BottomFragment() { lateinit var recyclerView: RecyclerView lateinit var progress: ProgressBar private var manager: RecyclerView.LayoutManager? = null private var adapter: DirectoryPageAdapter? = null private var isFirst = true private var waitingScroll = false private var listUpdated = false private val model: DirectoryViewModel by viewModels() private var dataJob: Job? = null private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_dir } else { R.layout.recycler_dir_grid } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) activity?.let { getData { animeObjects -> hideProgress() adapter?.submitData(animeObjects) makeAnimation() } } } private fun getData(callback: suspend (PagingData) -> Unit) { dataJob?.cancel() dataJob = lifecycleScope.launch { when (arguments?.getInt("type", 0) ?: 0) { 1 -> model.getOvas() 2 -> model.getMovies() else -> model.getAnimes() }.collectLatest { callback(it) } } } fun onChangeOrder() { activity?.let { waitingScroll = true lifecycleScope.launch { adapter?.submitData(PagingData.empty()) showProgress() getData { animeObjects -> hideProgress() listUpdated = true adapter?.submitData(animeObjects) makeAnimation() } } } } private fun hideProgress() { progress.post { progress.visibility = View.GONE } } private fun showProgress() { isFirst = true progress.post { progress.visibility = View.VISIBLE } } private fun makeAnimation() { if (isFirst) { recyclerView.scheduleLayoutAnimation() isFirst = false } } @UiThread private fun scrollTop() { try { recyclerView.smoothScrollToPosition(0) } catch (e: Exception) { e.printStackTrace() } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) progress = view.find(R.id.progress) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) manager = recyclerView.layoutManager recyclerView.layoutManager = manager adapter = DirectoryPageAdapter(this) adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) if (positionStart == 0 && waitingScroll) { scrollTop() waitingScroll = false } } override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { super.onItemRangeMoved(fromPosition, toPosition, itemCount) if (toPosition == 0 && waitingScroll) { scrollTop() waitingScroll = false } } }) recyclerView.verifyManager() recyclerView.adapter = adapter isFirst = true } override fun onReselect() { manager?.smoothScrollToPosition(recyclerView, null, 0) } enum class DirType(var value: Int) { ANIMES(0), OVAS(1), MOVIES(2) } companion object { operator fun get(type: DirType): DirectoryPageFragment { val bundle = Bundle() bundle.putInt("type", type.value) val fragment = DirectoryPageFragment() fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryPageFragmentMaterial.kt ================================================ package knf.kuma.directory import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.annotation.LayoutRes import androidx.annotation.UiThread import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.paging.PagingData import androidx.recyclerview.widget.RecyclerView import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.commons.PrefsUtil import knf.kuma.commons.verifyManager import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.jetbrains.anko.find class DirectoryPageFragmentMaterial : BottomFragment() { lateinit var recyclerView: RecyclerView lateinit var progress: ProgressBar private var manager: RecyclerView.LayoutManager? = null private var adapter: DirectoryPageAdapterMaterial? = null private var isFirst = true private var waitingScroll = false private var listUpdated = false private val model: DirectoryViewModel by viewModels() private var dataJob: Job? = null private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_dir } else { R.layout.recycler_dir_grid } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) activity?.let { getData { animeObjects -> hideProgress() adapter?.submitData(animeObjects) makeAnimation() } } } private fun getData(callback: suspend (PagingData) -> Unit) { dataJob?.cancel() dataJob = lifecycleScope.launch { when (arguments?.getInt("type", 0) ?: 0) { 1 -> model.getOvas() 2 -> model.getMovies() else -> model.getAnimes() }.collectLatest { callback(it) } } } fun onChangeOrder() { activity?.let { waitingScroll = true lifecycleScope.launch { adapter?.submitData(PagingData.empty()) showProgress() getData { animeObjects -> hideProgress() listUpdated = true adapter?.submitData(animeObjects) makeAnimation() } } } } private fun hideProgress() { progress.post { progress.visibility = View.GONE } } private fun showProgress() { isFirst = true progress.post { progress.visibility = View.VISIBLE } } private fun makeAnimation() { if (isFirst) { recyclerView.scheduleLayoutAnimation() isFirst = false } } @UiThread private fun scrollTop() { try { recyclerView.smoothScrollToPosition(0) } catch (e: Exception) { e.printStackTrace() } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) progress = view.find(R.id.progress) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) manager = recyclerView.layoutManager recyclerView.layoutManager = manager adapter = DirectoryPageAdapterMaterial(this) adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) if (positionStart == 0 && waitingScroll) { scrollTop() waitingScroll = false } } override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { super.onItemRangeMoved(fromPosition, toPosition, itemCount) if (toPosition == 0 && waitingScroll) { scrollTop() waitingScroll = false } } }) recyclerView.verifyManager() recyclerView.adapter = adapter isFirst = true } override fun onReselect() { manager?.smoothScrollToPosition(recyclerView, null, 0) } enum class DirType(var value: Int) { ANIMES(0), OVAS(1), MOVIES(2) } companion object { operator fun get(type: DirType): DirectoryPageFragmentMaterial { val bundle = Bundle() bundle.putInt("type", type.value) val fragment = DirectoryPageFragmentMaterial() fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryPageFragmentOnline.kt ================================================ package knf.kuma.directory import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.annotation.LayoutRes import androidx.annotation.UiThread import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import knf.kuma.BottomFragment import knf.kuma.DiagnosticMaterial import knf.kuma.R import knf.kuma.commons.BypassUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.showSnackbar import knf.kuma.commons.verifyManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.find class DirectoryPageFragmentOnline : BottomFragment() { lateinit var recyclerView: RecyclerView lateinit var progress: ProgressBar private var manager: RecyclerView.LayoutManager? = null private val adapter: DirectoryPageAdapterOnline by lazy { DirectoryPageAdapterOnline(this) } private var isFirst = true private var waitingScroll = false private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_dir } else { R.layout.recycler_dir_grid } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) lifecycleScope.launch { createDirectoryPagedList(getType()) { try { var snack: Snackbar? = null snack = recyclerView.showSnackbar("Error al cargar directorio", Snackbar.LENGTH_INDEFINITE, "reintentar") { lifecycleScope.launch(Dispatchers.Main) { if (withContext(Dispatchers.IO) { BypassUtil.isNeeded(BypassUtil.testLink) }) { startActivity(Intent(requireContext(), DiagnosticMaterial.FullBypass::class.java)) }else { snack?.dismiss() delay(2000) adapter.retry() } } } }catch (e:Exception){ lifecycleScope.launch { delay(2000) adapter.retry() } } }.collect { adapter.submitData(it) } } progress.post { progress.visibility = View.GONE } } fun getType() = when (arguments?.getInt("type", 0)) { 1 -> "ova" 2 -> "movie" else -> "tv" } @UiThread private fun scrollTop() { try { recyclerView.smoothScrollToPosition(0) } catch (e: Exception) { e.printStackTrace() } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) progress = view.find(R.id.progress) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) manager = recyclerView.layoutManager recyclerView.layoutManager = manager adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) if (positionStart == 0 && waitingScroll) { scrollTop() waitingScroll = false } } override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { super.onItemRangeMoved(fromPosition, toPosition, itemCount) if (toPosition == 0 && waitingScroll) { scrollTop() waitingScroll = false } } }) recyclerView.verifyManager() recyclerView.adapter = adapter isFirst = true } override fun onReselect() { manager?.smoothScrollToPosition(recyclerView, null, 0) } enum class DirType(var value: Int) { ANIMES(0), OVAS(1), MOVIES(2) } companion object { operator fun get(type: Int): DirectoryPageFragmentOnline { val bundle = Bundle() bundle.putInt("type", type) val fragment = DirectoryPageFragmentOnline() fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryService.kt ================================================ package knf.kuma.directory import android.annotation.SuppressLint import android.app.IntentService import android.app.Notification import android.app.NotificationManager import android.content.Context import android.content.Intent import android.media.AudioManager import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import knf.kuma.R import knf.kuma.commons.BypassUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.SSLSkipper import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.isFullMode import knf.kuma.commons.jsoupCookies import knf.kuma.commons.jsoupCookiesDir import knf.kuma.commons.noCrash import knf.kuma.database.CacheDB import knf.kuma.database.dao.AnimeDAO import knf.kuma.download.FileAccessHelper import knf.kuma.download.foreground import knf.kuma.download.service import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.DirectoryPage import knf.kuma.widgets.emision.WEmisionProvider import org.jsoup.HttpStatusException import pl.droidsonroids.jspoon.Jspoon import kotlin.properties.Delegates class DirectoryService : IntentService("Directory update") { private val CURRENT_TIME = System.currentTimeMillis() private var manager: NotificationManager? = null private var count = 0 private var page = 0 private var maxAnimes = 3200 private val TAG = "Directory Getter" private var needCookies by Delegates.notNull() private val keyFailedPages = "failed_pages" private val startNotification: Notification get() { val notification = NotificationCompat.Builder(this, "directory_update") .setOngoing(true) .setPriority(NotificationCompat.PRIORITY_MIN) .setSmallIcon(R.drawable.ic_directory_not) .setSound(null, AudioManager.STREAM_NOTIFICATION) .setColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) .setWhen(CURRENT_TIME) if (PrefsUtil.collapseDirectoryNotification) notification.setSubText("Verificando directorio") else notification.setContentTitle("Verificando directorio") return notification.build() } override fun onCreate() { foreground(NOT_CODE, startNotification) super.onCreate() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { foreground(NOT_CODE, startNotification) return super.onStartCommand(intent, flags, startId) } override fun onHandleIntent(intent: Intent?) { foreground(NOT_CODE, startNotification) if (!Network.isConnected) { cancelForeground() stopSelf() return } needCookies = BypassUtil.isCloudflareActive(BypassUtil.testLink) isRunning = true setStatus(STATE_VERIFYING) manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val animeDAO = CacheDB.INSTANCE.animeDAO() count = animeDAO.count SSLSkipper.skip() val jspoon = Jspoon.create() calculateMax() setStatus(STATE_PARTIAL) doPartialSearch(jspoon, animeDAO) setStatus(STATE_FULL) doEcchiRemove(animeDAO) checkDownloaded(animeDAO) doFullSearch(jspoon, animeDAO) doEmissionRefresh(jspoon, animeDAO) cancelForeground() } private fun calculateMax() { noCrash { val main = jsoupCookiesDir("https://www3.animeflv.net/browse", needCookies).get() val lastPage = main.select("ul.pagination li:matches(\\d+)").last().text().trim().toInt() val last = try { jsoupCookiesDir("https://www3.animeflv.net/browse?page=$lastPage", needCookies).get().select("article").size } catch (e: Exception) { 0 } maxAnimes = ((24 * (lastPage - 1)) + last) - 1 Log.e(TAG, "Max animes = $maxAnimes") } } private fun doEcchiRemove(animeDAO: AnimeDAO) { if (!isFullMode || PrefsUtil.isFamilyFriendly) { animeDAO.nukeEcchi() } } private fun checkDownloaded(animeDAO: AnimeDAO) { val jspoon = Jspoon.create() FileAccessHelper.downloadExplorerCreator.createLinksList().forEach { if (!animeDAO.existLink("%${it.substringAfterLast(".net")}")){ try { val response = jsoupCookies(it).execute() val body = response.body() if (response.statusCode() == 200 && body != null) { val webInfo = jspoon.adapter(AnimeObject.WebInfo::class.java).fromHtml(body) animeDAO.insert(AnimeObject(it, webInfo)) } } catch (e: Exception) { e.printStackTrace() } if (needCookies) Thread.sleep(5000) } } } private fun doEmissionRefresh(jspoon: Jspoon, animeDAO: AnimeDAO) { try { animeDAO.allLinksInEmission.forEach { try { val animeObject = AnimeObject(it, jspoon.adapter(AnimeObject.WebInfo::class.java).fromHtml(jsoupCookiesDir(it,needCookies).get().outerHtml())) val current = animeDAO.getAnimeByAid(animeObject.aid) if (current == null || current != animeObject) animeDAO.updateAnime(animeObject) WEmisionProvider.update(this) } catch (e: Exception) { return@forEach } } }catch (e:Exception){ // } } @SuppressLint("ApplySharedPref") private fun doPartialSearch(jspoon: Jspoon, animeDAO: AnimeDAO) { val strings = PreferenceManager.getDefaultSharedPreferences(this).getStringSet(keyFailedPages, LinkedHashSet()) val newStrings = LinkedHashSet() var partialCount = 0 if (strings?.size == 0) Log.e(TAG, "No pending pages") for (s in LinkedHashSet(strings)) { partialCount++ if (!Network.isConnected) { Log.e(TAG, "Processed $partialCount pages before disconnection") stopSelf() return } try { if (needCookies) Thread.sleep(6000) else Thread.sleep(1000) val document = jsoupCookiesDir("https://www3.animeflv.net/browse?order=added&page=$s", needCookies).get() if (document.select("article").size != 0) { val animeObjects = jspoon.adapter(DirectoryPage::class.java).fromHtml(document.outerHtml()).getAnimes(animeDAO, jspoon, object : DirectoryPage.UpdateInterface { override fun onAdd() { count++ updateNotification() } override fun onError() { Log.e(TAG, "Error at page: $s") if (!newStrings.contains(s)) newStrings.add(s.toString()) } }, needCookies) if (animeObjects.isNotEmpty()) animeDAO.insertAll(animeObjects) } } catch (e: HttpStatusException) { if (e.statusCode == 403 || e.statusCode == 503) { setStatus(STATE_INTERRUPTED) stopSelf() cancelForeground() } } catch (e: Exception) { Log.e(TAG, "Page error: $s | ${e.message}") if (!newStrings.contains(s.toString())) newStrings.add(s.toString()) } } PreferenceManager.getDefaultSharedPreferences(this).edit().putStringSet(keyFailedPages, newStrings).commit() } private fun doFullSearch(jspoon: Jspoon, animeDAO: AnimeDAO) { page = 1 var skipCount = 0 var finished = false val strings = PreferenceManager.getDefaultSharedPreferences(this).getStringSet(keyFailedPages, LinkedHashSet()) while (!finished) { if (!Network.isConnected) { Log.e(TAG, "Processed $page pages before disconnection") stopSelf() return } try { if (needCookies) Thread.sleep(6000) val document = jsoupCookiesDir("https://www3.animeflv.net/browse?order=added&page=$page", needCookies).get() Log.e(TAG, "Read page $page") if (document.select("ul.ListAnimes").isNotEmpty() && document.select("article").isNotEmpty()) { page++ val animeObjects = jspoon.adapter(DirectoryPage::class.java).fromHtml(document.outerHtml()).getAnimes(animeDAO, jspoon, object : DirectoryPage.UpdateInterface { override fun onAdd() { count++ updateNotification() } override fun onError() { Log.e(TAG, "At page: $page") if (strings?.contains(page.toString()) == false) strings.add(page.toString()) } }, needCookies) if (animeObjects.isNotEmpty()) { animeDAO.insertAll(animeObjects) } else if (PrefsUtil.isDirectoryFinished || animeDAO.count >= maxAnimes || skipCount >= 3) { PrefsUtil.isDirectoryFinished = (animeDAO.count in (maxAnimes - 5)..(maxAnimes + 5)) Log.e(TAG, "Stop searching at page $page") setStatus(STATE_FINISHED) cancelForeground() break } else { skipCount++ } } else { finished = true Log.e(TAG, "Processed $page pages") PrefsUtil.isDirectoryFinished = animeDAO.count in (maxAnimes - 5)..(maxAnimes + 5) PreferenceManager.getDefaultSharedPreferences(this).edit().putStringSet(keyFailedPages, strings).apply() DirUpdateWork.schedule(this) setStatus(STATE_FINISHED) } } catch (e: HttpStatusException) { Log.e(TAG, "Page error: $page | Code: ${e.statusCode}") if (e.statusCode == 403 || e.statusCode == 503) { e.printStackTrace() finished = true setStatus(STATE_INTERRUPTED) stopSelf() cancelForeground() } } catch (e: Exception) { Log.e(TAG, "Page error: $page | ${e.message}") if (strings?.contains(page.toString()) == false) strings.add(page.toString()) page++ if (page > maxAnimes / 24){ finished = true setStatus(STATE_INTERRUPTED) } } } } private fun cancelForeground() { noCrash { isRunning = false stopForeground(true) notCancel(NOT_CODE) } } private fun updateNotification() { val notification = NotificationCompat.Builder(this, CHANNEL).apply { setOngoing(true) priority = NotificationCompat.PRIORITY_MIN setSmallIcon(R.drawable.ic_directory_not) color = ContextCompat.getColor(this@DirectoryService, EAHelper.getThemeColor()) setWhen(CURRENT_TIME) setSound(null) if (PrefsUtil.collapseDirectoryNotification) setSubText("Creando directorio: $count/$maxAnimes~") else { setContentTitle("Creando directorio") setContentText("Agregados: $count/$maxAnimes~") if (maxAnimes > 0) setProgress(maxAnimes, count, false) } } notShow(NOT_CODE, notification.build()) } private fun notShow(code: Int, notification: Notification) { manager?.notify(code, notification) } private fun notCancel(code: Int) { manager?.cancel(code) } override fun onTaskRemoved(rootIntent: Intent) { cancelForeground() super.onTaskRemoved(rootIntent) } interface OnDirStatus { fun onFinished() } companion object { const val STATE_CACHED = 0 const val STATE_PARTIAL = 1 const val STATE_FULL = 2 const val STATE_FINISHED = 3 const val STATE_INTERRUPTED = 4 const val STATE_VERIFYING = 5 var NOT_CODE = 5598 var CHANNEL = "directory_update" var isRunning = false private set private val liveStatus = MutableLiveData() fun run(context: Context?) { if (context == null) return if (!isRunning) context.service(Intent(context, DirectoryService::class.java)) } fun setStatus(status: Int) { doOnUIGlobal { liveStatus.value = status } } fun getLiveStatus(): LiveData { return liveStatus } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryUpdateService.kt ================================================ package knf.kuma.directory import android.app.IntentService import android.app.Notification import android.app.NotificationManager import android.content.Context import android.content.Intent import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import knf.kuma.R import knf.kuma.commons.BypassUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.jsoupCookiesDir import knf.kuma.database.CacheDB import knf.kuma.database.dao.AnimeDAO import knf.kuma.download.foreground import knf.kuma.pojos.DirectoryPage import org.jsoup.HttpStatusException import pl.droidsonroids.jspoon.Jspoon import kotlin.properties.Delegates class DirectoryUpdateService : IntentService("Directory re-update") { private val CURRENT_TIME = System.currentTimeMillis() private var manager: NotificationManager? = null private var count = 0 private var page = 0 private val maxAnimes = 72 private var needCookies by Delegates.notNull() private val startNotification: Notification get() { val notification = NotificationCompat.Builder(this, CHANNEL) .setOngoing(true) .setSubText("Actualizando directorio") .setPriority(NotificationCompat.PRIORITY_MIN) .setSmallIcon(R.drawable.ic_dir_update) .setColor(ContextCompat.getColor(this, EAHelper.getThemeColor())) .setWhen(CURRENT_TIME) return notification.build() } override fun onCreate() { foreground(NOT_CODE, startNotification) super.onCreate() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { foreground(NOT_CODE, startNotification) return super.onStartCommand(intent, flags, startId) } override fun onHandleIntent(intent: Intent?) { foreground(NOT_CODE, startNotification) if (!Network.isConnected) { stopSelf() cancelForeground() return } isRunning = true manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val animeDAO = CacheDB.INSTANCE.animeDAO() needCookies = BypassUtil.isCloudflareActive() DirManager.checkPreDir(true) val jspoon = Jspoon.create() doFullSearch(jspoon, animeDAO) cancelForeground() } private fun doFullSearch(jspoon: Jspoon, animeDAO: AnimeDAO) { page = 1 var finished = false while (!finished) { if (!Network.isConnected) { Log.e("Directory Getter", "Processed $page pages before disconnection") stopSelf() cancelForeground() return } try { if (needCookies) Thread.sleep(6000) else Thread.sleep(1000) val document = jsoupCookiesDir("https://www3.animeflv.net/browse?order=added&page=$page", needCookies).get() if (document.select("article").size != 0) { val animeObjects = jspoon.adapter(DirectoryPage::class.java).fromHtml(document.outerHtml()).getAnimesRecreate(jspoon, object : DirectoryPage.UpdateInterface { override fun onAdd() { count++ updateNotification() } override fun onError() { Log.e("Directory Getter", "At page: $page") } }, needCookies) if (animeObjects.isNotEmpty()) animeDAO.insertAll(animeObjects) page++ if (page >= 4) finished = true } else { finished = true Log.e("Directory Getter", "Processed ${page - 1} pages") } } catch (e: HttpStatusException) { if (e.statusCode == 403 || e.statusCode == 503) { Log.e("Directory Getter", "Processed $page pages before interrupted") finished = true } } catch (e: Exception) { Log.e("Directory Getter", "Page error: $page | ${e.message}") page++ if (page >= 4) finished = true } } cancelForeground() } private fun cancelForeground() { isRunning = false stopForeground(true) manager?.cancel(NOT_CODE) } private fun updateNotification() { val notification = NotificationCompat.Builder(this, CHANNEL).apply { setOngoing(true) priority = NotificationCompat.PRIORITY_MIN setSmallIcon(R.drawable.ic_dir_update) color = ContextCompat.getColor(this@DirectoryUpdateService, EAHelper.getThemeColor()) setWhen(CURRENT_TIME) setSound(null) if (PrefsUtil.collapseDirectoryNotification) setSubText("Actualizando directorio: $count/$maxAnimes~") else { setContentTitle("Actualizando directorio") setContentText("Actualizados: $count/$maxAnimes~") if (maxAnimes > 0) setProgress(maxAnimes, count, false) } } manager?.notify(NOT_CODE, notification.build()) } override fun onTaskRemoved(rootIntent: Intent) { cancelForeground() super.onTaskRemoved(rootIntent) } companion object { var NOT_CODE = 5599 var CHANNEL = "directory_update" var isRunning = false private set fun run(context: Context) { if (!isRunning) ContextCompat.startForegroundService(context, Intent(context, DirectoryUpdateService::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/directory/DirectoryViewModel.kt ================================================ package knf.kuma.directory import androidx.lifecycle.ViewModel import androidx.paging.PagingData import knf.kuma.retrofit.Repository import kotlinx.coroutines.flow.Flow class DirectoryViewModel : ViewModel() { private val repository = Repository() fun getAnimes(): Flow> { return repository.getAnimeDir() } fun getOvas(): Flow> { return repository.getOvaDir() } fun getMovies(): Flow> { return repository.getMovieDir() } } ================================================ FILE: app/src/main/java/knf/kuma/directory/viewholders/DirMainFragmentHolder.kt ================================================ package knf.kuma.directory.viewholders import android.view.View import androidx.fragment.app.FragmentManager import androidx.viewpager.widget.ViewPager import com.google.android.material.tabs.TabLayout import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.directory.DirPagerAdapter import org.jetbrains.anko.find class DirMainFragmentHolder(view: View, manager: FragmentManager) { private val tabLayout: TabLayout = view.find(R.id.tabs) internal val pager: ViewPager = view.find(R.id.pager) private val adapter: DirPagerAdapter init { pager.offscreenPageLimit = 3 adapter = DirPagerAdapter(manager) pager.adapter = adapter tabLayout.setupWithViewPager(pager) } fun onChangeOrder() { adapter.onChangeOrder() } fun onReselect() { (adapter.getItem(pager.currentItem) as BottomFragment).onReselect() } } ================================================ FILE: app/src/main/java/knf/kuma/directory/viewholders/DirMainFragmentMaterialHolder.kt ================================================ package knf.kuma.directory.viewholders import android.view.View import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import androidx.viewpager.widget.ViewPager import com.google.android.material.tabs.TabLayout import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.directory.DirPagerAdapterMaterial import knf.kuma.directory.DirPagerAdapterOnline import org.jetbrains.anko.find class DirMainFragmentMaterialHolder(view: View, manager: FragmentManager) { private val tabLayout: TabLayout = view.find(R.id.tabs) internal val pager: ViewPager = view.find(R.id.pager) private val adapter: FragmentPagerAdapter init { pager.offscreenPageLimit = 3 adapter = if (PrefsUtil.isDirectoryFinished || !Network.isConnected) DirPagerAdapterMaterial(manager) else DirPagerAdapterOnline(manager) pager.adapter = adapter tabLayout.setupWithViewPager(pager) } fun onChangeOrder() { (adapter as? DirPagerAdapterMaterial)?.onChangeOrder() } fun onReselect() { (adapter.getItem(pager.currentItem) as BottomFragment).onReselect() } } ================================================ FILE: app/src/main/java/knf/kuma/download/DownloadDialogActivity.kt ================================================ package knf.kuma.download import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.view.View import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItems import knf.kuma.commons.EAHelper import knf.kuma.commons.PatternUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.jsoupCookies import knf.kuma.commons.safeDismiss import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.NotificationObj import knf.kuma.videoservers.ServersFactory import org.jetbrains.anko.doAsync import java.util.regex.Pattern class DownloadDialogActivity : GenericActivity() { private lateinit var downloadObject: DownloadObject override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getThemeDialog()) super.onCreate(savedInstanceState) title = " " window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setFinishOnTouchOutside(false) val dialog = MaterialDialog(this).safeShow { message(text = "Obteniendo informacion...") cancelable(false) cancelOnTouchOutside(false) } doAsync { try { val document = jsoupCookies(intent.dataString).get() val name = PatternUtil.fromHtml(document.select("nav.Brdcrmb.fa-home a[href^=/anime/]").first().text()) lateinit var aid: String lateinit var num: String val matcher = Pattern.compile("var (.*) = (\\d+);").matcher(document.html()) while (matcher.find()) { when (matcher.group(1)) { "anime_id" -> aid = matcher.group(2) "episode_number" -> num = matcher.group(2) } } val eid = "${aid}Episodio $num".hashCode().toString() val chapter = AnimeObject.WebInfo.AnimeChapter(Integer.parseInt(aid), "Episodio $num", eid, intent.dataString ?: "", name, aid) downloadObject = DownloadObject.fromChapter(chapter, false) doOnUI { dialog.safeDismiss() try { showSelectDialog() } catch (e: Exception) { e.printStackTrace() finish() } } } catch (e: Exception) { e.printStackTrace() finish() } } } private fun showSelectDialog() { MaterialDialog(this).safeShow { listItems(items = listOf("Descarga", "Streaming")) { _, index, _ -> ServersFactory.start(this@DownloadDialogActivity, intent.dataString ?: "", downloadObject, index == 1, object : ServersFactory.ServersInterface { override fun onFinish(started: Boolean, success: Boolean) { if (success) removeNotification() finish() } override fun onCast(url: String?) { } override fun onProgressIndicator(boolean: Boolean) { } override fun getView(): View? { return null } }) } setOnCancelListener { finish() } } } private fun extract(st: String?, regex: String): String { val matcher = Pattern.compile(regex).matcher(st) matcher.find() return matcher.group(1) } private fun removeNotification() { if (intent.getBooleanExtra("notification", false)) sendBroadcast(NotificationObj.fromIntent(intent).getBroadcast(this)) } } ================================================ FILE: app/src/main/java/knf/kuma/download/DownloadManager.kt ================================================ package knf.kuma.download import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent import android.media.MediaScannerConnection import android.net.Uri import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.EnqueueAction import com.tonyodev.fetch2.Error import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.FetchConfiguration import com.tonyodev.fetch2.FetchListener import com.tonyodev.fetch2.Request import com.tonyodev.fetch2.Status import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.Func import com.tonyodev.fetch2okhttp.OkHttpDownloader import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.commons.AllSSLOkHttpClient import knf.kuma.commons.EAHelper import knf.kuma.commons.FileUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrash import knf.kuma.database.CacheDB import knf.kuma.pojos.DownloadObject import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jetbrains.anko.notificationManager import xdroid.toaster.Toaster import java.util.Locale class DownloadManager : Service() { override fun onBind(intent: Intent): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { foreground(23498, foregroundGroupNotification()) if (intent != null && intent.action != null && intent.action == "stop.foreground") { stopForeground(true) stopSelf() } return START_STICKY } override fun onCreate() { super.onCreate() foreground(23498, foregroundGroupNotification()) //notificationManager?.notify(22498, foregroundGroupNotification()) } companion object { const val CHANNEL_FOREGROUND = "service.LifeSaver" internal const val ACTION_PAUSE = 0 internal const val ACTION_RESUME = 1 internal const val ACTION_CANCEL = 2 private const val CHANNEL = "service.Downloads" private const val CHANNEL_ONGOING = "service.Downloads.Ongoing" @SuppressLint("StaticFieldLeak") private val context: Context = App.context private var fetch: Fetch? = null private val fetchConfiguration: FetchConfiguration.Builder by lazy { FetchConfiguration.Builder(context) .setDownloadConcurrentLimit(PrefsUtil.maxParallelDownloads) .enableLogging(BuildConfig.DEBUG) .enableRetryOnNetworkGain(true) .setAutoRetryMaxAttempts(3) .createDownloadFileOnEnqueue(false) .setHttpDownloader(OkHttpDownloader(AllSSLOkHttpClient.get())) } private val downloadDao = CacheDB.INSTANCE.downloadsDAO() private val notificationManager: NotificationManager by lazy { context.notificationManager } fun setParallelDownloads(newValue: String?) { if (newValue.isNullOrEmpty()) return fetch?.pauseAll() fetchConfiguration.setDownloadConcurrentLimit(Integer.parseInt(newValue)) fetch = Fetch.getInstance(fetchConfiguration.build()).apply { fetch?.getListenerSet()?.forEach { addListener(it, autoStart = true) } } fetch?.resumeAll() } init { fetch = Fetch.getInstance(fetchConfiguration.build()).addListener(object : FetchListener { override fun onAdded(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PENDING downloadDao.update(downloadObject) } } } override fun onQueued(download: Download, waitingOnNetwork: Boolean) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PENDING downloadDao.update(downloadObject) } } } override fun onWaitingNetwork(download: Download) { } override fun onCompleted(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { if (FileAccessHelper.isTempFile(download.file)) { if (FileAccessHelper.getTmpFile(downloadObject.file).length() < 5) { Log.e("Download", "Damaged tmp file, aborting") errorNotification(downloadObject) downloadDao.delete(downloadObject) fetch?.delete(download.id) stopIfNeeded() return@doAsync } Log.e("Download", "Moving temp") downloadObject.setEta(-2) downloadObject.progress = 0 downloadDao.update(downloadObject) FileUtil.moveFile( downloadObject.file, object : FileUtil.MoveCallback { override fun onProgress(pair: android.util.Pair) { if (!pair.second) { downloadObject.progress = pair.first updateNotification(downloadObject, false) downloadDao.update(downloadObject) } else if (pair.first == -1) { downloadDao.delete(downloadObject) errorNotification(downloadObject) } else { downloadObject.progress = 100 downloadObject.state = DownloadObject.COMPLETED downloadDao.update(downloadObject) notificationManager.cancel(downloadObject.eid.toInt()) completedNotification(downloadObject) } stopIfNeeded() } }) } else { downloadObject.state = DownloadObject.COMPLETED downloadDao.update(downloadObject) completedNotification(downloadObject) } } stopIfNeeded() } } override fun onError(download: Download, error: Error, throwable: Throwable?) { doAsync { Log.e("Download", "Error downloader") if (throwable != null) { throwable.printStackTrace() FirebaseCrashlytics.getInstance().recordException(throwable) } val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { errorNotification(downloadObject) downloadDao.delete(downloadObject) fetch?.delete(download.id) stopIfNeeded() } } } override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) { } override fun onStarted(download: Download, downloadBlocks: List, totalBlocks: Int) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.DOWNLOADING downloadDao.update(downloadObject) updateNotification(downloadObject, false) } context.service(Intent(context, DownloadManager::class.java)) } } override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.DOWNLOADING downloadObject.setEta(etaInMilliSeconds) downloadObject.setSpeed(downloadedBytesPerSecond) downloadObject.progress = download.progress downloadObject.t_bytes = download.total downloadObject.d_bytes = download.downloaded downloadDao.update(downloadObject) updateNotification(downloadObject, false) } } } override fun onPaused(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PAUSED downloadObject.setEta(-1) downloadDao.update(downloadObject) updateNotification(downloadObject, true) } stopIfNeeded() } } override fun onResumed(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PENDING downloadObject.time = System.currentTimeMillis() downloadDao.update(downloadObject) updateNotification(downloadObject, false) } } } override fun onCancelled(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) notificationManager.cancel(downloadObject.getDid()) stopIfNeeded() } } override fun onRemoved(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) notificationManager.cancel(downloadObject.getDid()) stopIfNeeded() } } override fun onDeleted(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) notificationManager.cancel(downloadObject.getDid()) stopIfNeeded() } } }, autoStart = true) } fun start(downloadObject: DownloadObject): Boolean { try { val file = FileAccessHelper.getFileCreate(downloadObject.file) file?.let { val request = Request(downloadObject.link, file.absolutePath) if (downloadObject.headers != null) for (header in downloadObject.headers?.createHeaders() ?: listOf()) request.addHeader(header.first, header.second) request.enqueueAction = EnqueueAction.REPLACE_EXISTING downloadObject.setDid(request.id) downloadObject.canResume = true downloadDao.insert(downloadObject) fetch?.enqueue(request, { Log.e("Download", "Queued " + it.id) }, { it.throwable?.printStackTrace() GlobalScope.launch(Dispatchers.IO) { downloadDao.delete(downloadObject) } }) fetch?.resumeAll() } ?: return false return true } catch (e: Exception) { e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) Toaster.toast("Error al iniciar descarga: ${e.message}") return false } } fun cancel(eid: String) { doAsync { val downloadObject = downloadDao.getByEid(eid) if (downloadObject != null) { downloadDao.delete(downloadObject) notificationManager.cancel(downloadObject.eid?.toInt() ?: 0) if (downloadObject.did != null) fetch?.delete(downloadObject.getDid()) } } } fun cancelAll() { doAsync { noCrash { val downloads = downloadDao.allRaw val dids = mutableListOf() downloads.forEach { dids.add(it.getDid()) notificationManager.cancel(it.eid?.toInt() ?: 0) } fetch?.delete(dids) downloadDao.delete(downloads) stopIfNeeded() } } } fun pause(downloadObject: DownloadObject) { pause(downloadObject.getDid()) } fun pauseAll() { fetch?.getDownloadsWithStatus(Status.DOWNLOADING, Func { val list = mutableListOf() it.forEach { download -> list.add(download.id) } fetch?.pause(list) }) } fun pause(did: Int) { doAsync { fetch?.pause(did) } } fun resume(downloadObject: DownloadObject) { resume(downloadObject.getDid()) } internal fun resume(did: Int) { doAsync { fetch?.resume(did) } } private fun updateNotification(downloadObject: DownloadObject?, isPaused: Boolean) { if (downloadObject == null) return val notification = NotificationCompat.Builder(context, CHANNEL_ONGOING).apply { setSmallIcon(if (isPaused) R.drawable.ic_pause_not else if (downloadObject.eta.toLong() == -2L) R.drawable.ic_move else android.R.drawable.stat_sys_download) setContentTitle(downloadObject.name) setContentText(downloadObject.chapter) setOnlyAlertOnce(!isPaused || downloadObject.eta.toLong() == -2L) setProgress(100, downloadObject.progress, downloadObject.state == DownloadObject.PENDING) color = ContextCompat.getColor(App.context, EAHelper.getThemeColor()) setGroup("manager") setOngoing(!isPaused) setSound(null) setWhen(downloadObject.time) priority = NotificationCompat.PRIORITY_LOW if (downloadObject.eta.toLong() != -2L) { if (isPaused) addAction(R.drawable.ic_play_not, "Reanudar", getPending(downloadObject, ACTION_RESUME)) else addAction(R.drawable.ic_pause_not, "Pausar", getPending(downloadObject, ACTION_PAUSE)) addAction(R.drawable.ic_delete, "Cancelar", getPending(downloadObject, ACTION_CANCEL)) } if (!isPaused) setSubText(downloadObject.subtext) } notificationManager.notify(downloadObject.eid?.toInt() ?: 0, notification.build()) } private fun completedNotification(downloadObject: DownloadObject) { val notification = NotificationCompat.Builder(context, CHANNEL) .setColor(ContextCompat.getColor(context, android.R.color.holo_green_dark)) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setContentTitle(downloadObject.name) .setContentText(downloadObject.chapter) .setContentIntent(ServersFactory.getPlayIntent(context, downloadObject.name, downloadObject.file)) .setOngoing(false) .setAutoCancel(true) .setWhen(downloadObject.time) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() notificationManager.notify(downloadObject.eid?.toInt() ?: 0, notification) updateMedia(downloadObject) } private fun errorNotification(downloadObject: DownloadObject) { val notification = NotificationCompat.Builder(context, CHANNEL) .setColor(ContextCompat.getColor(context, android.R.color.holo_red_dark)) .setSmallIcon(android.R.drawable.stat_notify_error) .setContentTitle(downloadObject.name) .setContentText("Error al descargar " + downloadObject.chapter.lowercase(Locale.ENGLISH)) .setOngoing(false) .setWhen(downloadObject.time) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() notificationManager.notify(downloadObject.eid?.toInt() ?: 0, notification) } private fun foregroundGroupNotification(): Notification { return NotificationCompat.Builder(context, CHANNEL_FOREGROUND).apply { setSmallIcon(R.drawable.ic_service) setOngoing(true) priority = NotificationCompat.PRIORITY_MIN if (PrefsUtil.isGroupingEnabled) { setGroup("manager") setGroupSummary(true) } if (PrefsUtil.collapseDirectoryNotification) setSubText("Descargas en progreso") else setContentTitle("Descargas en progreso") }.build() } private fun updateMedia(downloadObject: DownloadObject) { try { val file = downloadObject.file context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(FileAccessHelper.getFile(file)))) MediaScannerConnection.scanFile(context, arrayOf(FileAccessHelper.getFile(file).absolutePath), arrayOf("video/mp4"), null) } catch (e: Exception) { e.printStackTrace() } } private fun getPending(downloadObject: DownloadObject, action: Int): PendingIntent { return try { val intent = Intent(context, DownloadReceiver::class.java) .putExtra("did", downloadObject.getDid()) .putExtra("eid", downloadObject.eid) .putExtra("action", action) PendingIntent.getBroadcast( context, downloadObject.key + action, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } catch (e: IllegalStateException) { PendingIntent.getBroadcast( context, 0, Intent(), PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } } private fun stopIfNeeded() { GlobalScope.launch(Dispatchers.IO){ if (downloadDao.countActive() == 0 && context.isServiceRunning(DownloadManager::class.java)) { launch(Dispatchers.Main){ context.service(Intent(context, DownloadManager::class.java).setAction("stop.foreground")) } notificationManager.cancel(22498) } } } } } ================================================ FILE: app/src/main/java/knf/kuma/download/DownloadManagerCentral.kt ================================================ package knf.kuma.download import android.app.job.JobScheduler import android.content.Context import android.os.Build import knf.kuma.App import knf.kuma.pojos.DownloadObject object DownloadManagerCentral { private val isSchedulerEnabled = try { App.context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE } catch (_: Exception) { false } fun start(downloadObject: DownloadObject): Boolean { return if (isSchedulerEnabled) { DownloadManagerJob.start(downloadObject) } else { DownloadManager.start(downloadObject) } } fun cancel(eid: String) { if (isSchedulerEnabled) { DownloadManagerJob.cancel(eid) } else { DownloadManager.cancel(eid) } } fun cancelAll() { if (isSchedulerEnabled) { DownloadManagerJob.cancelAll() } else { DownloadManager.cancelAll() } } fun pause(downloadObject: DownloadObject) { if (isSchedulerEnabled) { DownloadManagerJob.pause(downloadObject) } else { DownloadManager.pause(downloadObject) } } fun pauseAll() { if (isSchedulerEnabled) { DownloadManagerJob.pauseAll() } else { DownloadManager.pauseAll() } } fun pause(did: Int) { if (isSchedulerEnabled) { DownloadManagerJob.pause(did) } else { DownloadManager.pause(did) } } fun resume(downloadObject: DownloadObject) { if (isSchedulerEnabled) { DownloadManagerJob.resume(downloadObject) } else { DownloadManager.resume(downloadObject) } } fun resume(did: Int) { if (isSchedulerEnabled) { DownloadManagerJob.resume(did) } else { DownloadManager.resume(did) } } fun setParallelDownloads(newValue: String?) { if (isSchedulerEnabled) { DownloadManagerJob.setParallelDownloads(newValue) } else { DownloadManager.setParallelDownloads(newValue) } } } ================================================ FILE: app/src/main/java/knf/kuma/download/DownloadManagerJob.kt ================================================ package knf.kuma.download import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.app.job.JobParameters import android.app.job.JobService import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.media.MediaScannerConnection import android.net.Uri import android.os.Build import android.util.Log import android.util.Pair import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.EnqueueAction import com.tonyodev.fetch2.Error import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.FetchConfiguration import com.tonyodev.fetch2.FetchListener import com.tonyodev.fetch2.Request import com.tonyodev.fetch2.Status import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.Func import com.tonyodev.fetch2okhttp.OkHttpDownloader import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.commons.AllSSLOkHttpClient import knf.kuma.commons.EAHelper import knf.kuma.commons.FileUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrash import knf.kuma.database.CacheDB import knf.kuma.pojos.DownloadObject import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jetbrains.anko.notificationManager import xdroid.toaster.Toaster import java.util.Locale class DownloadManagerJob : JobService() { private lateinit var currentParams: JobParameters private val stopReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == "stop.foreground") { unregisterReceiver(this) jobFinished(currentParams, false) } } } @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) override fun onStartJob(params: JobParameters): Boolean { currentParams = params setNotification(params, 23498, foregroundGroupNotification(), JOB_END_NOTIFICATION_POLICY_REMOVE) registerReceiver(stopReceiver, IntentFilter("stop.foreground"), RECEIVER_NOT_EXPORTED) return true } override fun onStopJob(params: JobParameters?): Boolean { try { unregisterReceiver(stopReceiver) } catch (e: Exception) { // } return true } companion object { const val CHANNEL_FOREGROUND = "service.LifeSaver" internal const val ACTION_PAUSE = 0 internal const val ACTION_RESUME = 1 internal const val ACTION_CANCEL = 2 private const val CHANNEL = "service.Downloads" private const val CHANNEL_ONGOING = "service.Downloads.Ongoing" @SuppressLint("StaticFieldLeak") private val context: Context = App.context private var fetch: Fetch? = null private val fetchConfiguration: FetchConfiguration.Builder by lazy { FetchConfiguration.Builder(context) .setDownloadConcurrentLimit(PrefsUtil.maxParallelDownloads) .enableLogging(BuildConfig.DEBUG) .enableRetryOnNetworkGain(true) .setAutoRetryMaxAttempts(3) .createDownloadFileOnEnqueue(false) .setHttpDownloader(OkHttpDownloader(AllSSLOkHttpClient.get())) } private val downloadDao = CacheDB.INSTANCE.downloadsDAO() private val notificationManager: NotificationManager by lazy { context.notificationManager } fun setParallelDownloads(newValue: String?) { if (newValue.isNullOrEmpty()) return fetch?.pauseAll() fetchConfiguration.setDownloadConcurrentLimit(Integer.parseInt(newValue)) fetch = Fetch.getInstance(fetchConfiguration.build()).apply { fetch?.getListenerSet()?.forEach { addListener(it, autoStart = true) } } fetch?.resumeAll() } init { fetch = Fetch.getInstance(fetchConfiguration.build()).addListener(object : FetchListener { override fun onAdded(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PENDING downloadDao.update(downloadObject) } } } override fun onQueued(download: Download, waitingOnNetwork: Boolean) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PENDING downloadDao.update(downloadObject) } } } override fun onWaitingNetwork(download: Download) { } override fun onCompleted(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { if (FileAccessHelper.isTempFile(download.file)) { if (FileAccessHelper.getTmpFile(downloadObject.file).length() < 5) { Log.e("Download", "Damaged tmp file, aborting") errorNotification(downloadObject) downloadDao.delete(downloadObject) fetch?.delete(download.id) stopIfNeeded() return@doAsync } Log.e("Download", "Moving temp") downloadObject.setEta(-2) downloadObject.progress = 0 downloadDao.update(downloadObject) FileUtil.moveFile( downloadObject.file, object : FileUtil.MoveCallback { override fun onProgress(pair: Pair) { if (!pair.second) { downloadObject.progress = pair.first updateNotification(downloadObject, false) downloadDao.update(downloadObject) } else if (pair.first == -1) { downloadDao.delete(downloadObject) errorNotification(downloadObject) } else { downloadObject.progress = 100 downloadObject.state = DownloadObject.COMPLETED downloadDao.update(downloadObject) notificationManager.cancel(downloadObject.eid.toInt()) completedNotification(downloadObject) } stopIfNeeded() } }) } else { downloadObject.state = DownloadObject.COMPLETED downloadDao.update(downloadObject) completedNotification(downloadObject) } } stopIfNeeded() } } override fun onError(download: Download, error: Error, throwable: Throwable?) { doAsync { Log.e("Download", "Error downloader") if (throwable != null) { throwable.printStackTrace() FirebaseCrashlytics.getInstance().recordException(throwable) } val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { errorNotification(downloadObject) downloadDao.delete(downloadObject) fetch?.delete(download.id) stopIfNeeded() } } } override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) { } override fun onStarted(download: Download, downloadBlocks: List, totalBlocks: Int) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.DOWNLOADING downloadDao.update(downloadObject) updateNotification(downloadObject, false) } context.UIDT(DownloadManagerJob::class.java) } } override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.DOWNLOADING downloadObject.setEta(etaInMilliSeconds) downloadObject.setSpeed(downloadedBytesPerSecond) downloadObject.progress = download.progress downloadObject.t_bytes = download.total downloadObject.d_bytes = download.downloaded downloadDao.update(downloadObject) updateNotification(downloadObject, false) } } } override fun onPaused(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PAUSED downloadObject.setEta(-1) downloadDao.update(downloadObject) updateNotification(downloadObject, true) } stopIfNeeded() } } override fun onResumed(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) { downloadObject.state = DownloadObject.PENDING downloadObject.time = System.currentTimeMillis() downloadDao.update(downloadObject) updateNotification(downloadObject, false) } } } override fun onCancelled(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) notificationManager.cancel(downloadObject.getDid()) stopIfNeeded() } } override fun onRemoved(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) notificationManager.cancel(downloadObject.getDid()) stopIfNeeded() } } override fun onDeleted(download: Download) { doAsync { val downloadObject = downloadDao.getByDid(download.id) if (downloadObject != null) notificationManager.cancel(downloadObject.getDid()) stopIfNeeded() } } }, autoStart = true) } fun start(downloadObject: DownloadObject): Boolean { try { val file = FileAccessHelper.getFileCreate(downloadObject.file) file?.let { val request = Request(downloadObject.link, file.absolutePath) if (downloadObject.headers != null) for (header in downloadObject.headers?.createHeaders() ?: listOf()) request.addHeader(header.first, header.second) request.enqueueAction = EnqueueAction.REPLACE_EXISTING downloadObject.setDid(request.id) downloadObject.canResume = true downloadDao.insert(downloadObject) fetch?.enqueue(request, { Log.e("Download", "Queued " + it.id) }, { it.throwable?.printStackTrace() GlobalScope.launch(Dispatchers.IO) { downloadDao.delete(downloadObject) } }) fetch?.resumeAll() } ?: return false return true } catch (e: Exception) { e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) Toaster.toast("Error al iniciar descarga: ${e.message}") return false } } fun cancel(eid: String) { doAsync { val downloadObject = downloadDao.getByEid(eid) if (downloadObject != null) { downloadDao.delete(downloadObject) notificationManager.cancel(downloadObject.eid?.toInt() ?: 0) if (downloadObject.did != null) fetch?.delete(downloadObject.getDid()) } } } fun cancelAll() { doAsync { noCrash { val downloads = downloadDao.allRaw val dids = mutableListOf() downloads.forEach { dids.add(it.getDid()) notificationManager.cancel(it.eid?.toInt() ?: 0) } fetch?.delete(dids) downloadDao.delete(downloads) stopIfNeeded() } } } fun pause(downloadObject: DownloadObject) { pause(downloadObject.getDid()) } fun pauseAll() { fetch?.getDownloadsWithStatus(Status.DOWNLOADING, Func { val list = mutableListOf() it.forEach { download -> list.add(download.id) } fetch?.pause(list) }) } fun pause(did: Int) { doAsync { fetch?.pause(did) } } fun resume(downloadObject: DownloadObject) { resume(downloadObject.getDid()) } internal fun resume(did: Int) { doAsync { fetch?.resume(did) } } private fun updateNotification(downloadObject: DownloadObject?, isPaused: Boolean) { if (downloadObject == null) return val notification = NotificationCompat.Builder(context, CHANNEL_ONGOING).apply { setSmallIcon(if (isPaused) R.drawable.ic_pause_not else if (downloadObject.eta.toLong() == -2L) R.drawable.ic_move else android.R.drawable.stat_sys_download) setContentTitle(downloadObject.name) setContentText(downloadObject.chapter) setOnlyAlertOnce(!isPaused || downloadObject.eta.toLong() == -2L) setProgress(100, downloadObject.progress, downloadObject.state == DownloadObject.PENDING) color = ContextCompat.getColor(App.context, EAHelper.getThemeColor()) setGroup("manager") setOngoing(!isPaused) setSound(null) setWhen(downloadObject.time) priority = NotificationCompat.PRIORITY_LOW if (downloadObject.eta.toLong() != -2L) { if (isPaused) addAction(R.drawable.ic_play_not, "Reanudar", getPending(downloadObject, ACTION_RESUME)) else addAction(R.drawable.ic_pause_not, "Pausar", getPending(downloadObject, ACTION_PAUSE)) addAction(R.drawable.ic_delete, "Cancelar", getPending(downloadObject, ACTION_CANCEL)) } if (!isPaused) setSubText(downloadObject.subtext) } notificationManager.notify(downloadObject.eid?.toInt() ?: 0, notification.build()) } private fun completedNotification(downloadObject: DownloadObject) { val notification = NotificationCompat.Builder(context, CHANNEL) .setColor(ContextCompat.getColor(context, android.R.color.holo_green_dark)) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setContentTitle(downloadObject.name) .setContentText(downloadObject.chapter) .setContentIntent(ServersFactory.getPlayIntent(context, downloadObject.name, downloadObject.file)) .setOngoing(false) .setAutoCancel(true) .setWhen(downloadObject.time) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() notificationManager.notify(downloadObject.eid?.toInt() ?: 0, notification) updateMedia(downloadObject) } private fun errorNotification(downloadObject: DownloadObject) { val notification = NotificationCompat.Builder(context, CHANNEL) .setColor(ContextCompat.getColor(context, android.R.color.holo_red_dark)) .setSmallIcon(android.R.drawable.stat_notify_error) .setContentTitle(downloadObject.name) .setContentText("Error al descargar " + downloadObject.chapter.lowercase(Locale.ENGLISH)) .setOngoing(false) .setWhen(downloadObject.time) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() notificationManager.notify(downloadObject.eid?.toInt() ?: 0, notification) } private fun foregroundGroupNotification(): Notification { return NotificationCompat.Builder(context, CHANNEL_FOREGROUND).apply { setSmallIcon(R.drawable.ic_service) setOngoing(true) priority = NotificationCompat.PRIORITY_MIN if (PrefsUtil.isGroupingEnabled) { setGroup("manager") setGroupSummary(true) } if (PrefsUtil.collapseDirectoryNotification) setSubText("Descargas en progreso") else setContentTitle("Descargas en progreso") }.build() } private fun updateMedia(downloadObject: DownloadObject) { try { val file = downloadObject.file context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(FileAccessHelper.getFile(file)))) MediaScannerConnection.scanFile(context, arrayOf(FileAccessHelper.getFile(file).absolutePath), arrayOf("video/mp4"), null) } catch (e: Exception) { e.printStackTrace() } } private fun getPending(downloadObject: DownloadObject, action: Int): PendingIntent { return try { val intent = Intent(context, DownloadReceiver::class.java) .putExtra("did", downloadObject.getDid()) .putExtra("eid", downloadObject.eid) .putExtra("action", action) PendingIntent.getBroadcast( context, downloadObject.key + action, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } catch (e: IllegalStateException) { PendingIntent.getBroadcast( context, 0, Intent(), PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } } private fun stopIfNeeded() { GlobalScope.launch(Dispatchers.IO) { if (downloadDao.countActive() == 0 && context.isServiceRunning(DownloadManagerJob::class.java)) { launch(Dispatchers.Main) { context.sendBroadcast(Intent("stop.foreground").setPackage(context.packageName)) } } } } } } ================================================ FILE: app/src/main/java/knf/kuma/download/DownloadReceiver.kt ================================================ package knf.kuma.download import android.content.BroadcastReceiver import android.content.Context import android.content.Intent class DownloadReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val did = intent.getIntExtra("did", 0) when (intent.getIntExtra("action", -1)) { DownloadManager.ACTION_PAUSE -> DownloadManagerCentral.pause(did) DownloadManager.ACTION_RESUME -> DownloadManagerCentral.resume(did) DownloadManager.ACTION_CANCEL -> DownloadManagerCentral.cancel( intent.getStringExtra("eid") ?: "") } } } ================================================ FILE: app/src/main/java/knf/kuma/download/DownloadService.kt ================================================ package knf.kuma.download import android.app.IntentService import android.app.Notification import android.app.NotificationManager import android.content.Intent import android.media.MediaScannerConnection import android.net.Uri import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrash import knf.kuma.commons.notNull import knf.kuma.database.CacheDB import knf.kuma.pojos.DownloadObject import knf.kuma.queue.QueueManager import knf.kuma.videoservers.ServersFactory import okhttp3.ConnectionSpec import okhttp3.OkHttpClient import okhttp3.Request import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.util.concurrent.TimeUnit class DownloadService : IntentService("Download service") { private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() private var manager: NotificationManager? = null private var current: DownloadObject? = null private var file: String? = null private val bufferSize = PrefsUtil.bufferSize() private val startNotification: Notification get() = NotificationCompat.Builder(this, CHANNEL_ONGOING).apply { setSmallIcon(android.R.drawable.stat_sys_download) setContentTitle(current?.name) setContentText(current?.chapter) setProgress(100, current?.progress ?: 0, true) if (PrefsUtil.isGroupingEnabled) { setGroup("manager") setGroupSummary(true) } setOngoing(true) setSound(null) setWhen(current?.time ?: 0) priority = NotificationCompat.PRIORITY_LOW }.build() override fun onCreate() { super.onCreate() foreground(DOWNLOADING_ID, startNotification) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { foreground(DOWNLOADING_ID, startNotification) return super.onStartCommand(intent, flags, startId) } override fun onHandleIntent(intent: Intent?) { foreground(DOWNLOADING_ID, startNotification) val currentEid = intent?.getStringExtra("eid") ?: return manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager current = downloadsDAO.getByEid(currentEid) if (current == null) return file = current?.file try { val request = Request.Builder() .url(intent.dataString ?: "") if (current?.headers != null) for (pair in current?.headers?.createHeaders() ?: mutableListOf()) { request.addHeader(pair.first, pair.second) } val response = OkHttpClient().newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .allEnabledTlsVersions() .allEnabledCipherSuites() .build())) .build().newCall(request.build()).execute() current?.t_bytes = response.body?.contentLength() ?: 0 val inputStream = BufferedInputStream(response.body?.byteStream()) val outputStream: BufferedOutputStream if (response.code == 200 || response.code == 206) { outputStream = BufferedOutputStream(FileAccessHelper.getOutputStream(current?.file), bufferSize * 1024) } else { Log.e("Download error", "Code: " + response.code) errorNotification() current?.let { downloadsDAO.delete(it) QueueManager.remove(it.eid) } response.close() cancelForeground() return } current?.state = DownloadObject.DOWNLOADING current?.let { downloadsDAO.update(it) } val data = ByteArray(bufferSize * 1024) var count: Int = inputStream.read(data, 0, bufferSize * 1024) while (count >= 0) { if (!downloadsDAO.existByEid(currentEid)) { FileAccessHelper.delete(file) current?.let { downloadsDAO.delete(it) } QueueManager.remove(current?.eid) cancelForeground() return } outputStream.write(data, 0, count) current?.let { it.d_bytes += count.toLong() val prog = (it.d_bytes * 100 / it.t_bytes).toInt() if (prog > it.progress) { it.progress = prog updateNotification() downloadsDAO.update(it) } count = inputStream.read(data, 0, bufferSize * 1024) } } outputStream.flush() outputStream.close() inputStream.close() response.close() completedNotification() } catch (e: Exception) { e.printStackTrace() FileAccessHelper.delete(file) current?.let { downloadsDAO.delete(it) QueueManager.remove(it.eid) } errorNotification() } } private fun updateNotification() { val notification = NotificationCompat.Builder(this, CHANNEL_ONGOING) .setSmallIcon(android.R.drawable.stat_sys_download) .setContentTitle(current?.name) .setContentText(current?.chapter) .setProgress(100, current?.progress ?: 0, false) .setGroup("manager") .setOngoing(true) .setSound(null) .setWhen(current?.time ?: System.currentTimeMillis()) .setPriority(NotificationCompat.PRIORITY_LOW) val pending = downloadsDAO.countPending() if (pending > 0) notification.setSubText(pending.toString() + " " + if (pending == 1) "pendiente" else "pendientes") manager?.notify(DOWNLOADING_ID, notification.build()) } private fun completedNotification() { current?.let { it.state = DownloadObject.COMPLETED downloadsDAO.update(it) val notification = NotificationCompat.Builder(this, CHANNEL) .setColor(ContextCompat.getColor(applicationContext, android.R.color.holo_green_dark)) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setContentTitle(current?.name) .setContentText(current?.chapter) .setContentIntent(ServersFactory.getPlayIntent(this, it.name, file ?: "")) .setOngoing(false) .setAutoCancel(true) .setWhen(it.time) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() manager?.notify(it.eid.toInt(), notification) } updateMedia() cancelForeground() } private fun updateMedia() { try { sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(FileAccessHelper.getFile(file)))) MediaScannerConnection.scanFile(applicationContext, arrayOf(FileAccessHelper.getFile(file).absolutePath), arrayOf("video/mp4"), null) } catch (e: Exception) { e.printStackTrace() } } private fun errorNotification() { val notification = NotificationCompat.Builder(this, CHANNEL) .setColor(ContextCompat.getColor(applicationContext, android.R.color.holo_red_dark)) .setSmallIcon(android.R.drawable.stat_notify_error) .setContentTitle(current?.name) .setContentText("Error al descargar " + current?.chapter?.lowercase()) .setOngoing(false) .setWhen(current?.time ?: 0) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() manager?.notify(current?.eid?.toInt() ?: 0, notification) cancelForeground() } private fun cancelForeground() { stopForeground(true) manager?.cancel(DOWNLOADING_ID) } override fun onTaskRemoved(rootIntent: Intent) { noCrash { cancelForeground() FileAccessHelper.delete(file) current?.let { if (manager.notNull()) errorNotification() downloadsDAO.delete(it) QueueManager.remove(it.eid) } } super.onTaskRemoved(rootIntent) } companion object { const val CHANNEL = "service.Downloads" const val CHANNEL_ONGOING = "service.Downloads.Ongoing" private const val DOWNLOADING_ID = 8879 } } ================================================ FILE: app/src/main/java/knf/kuma/download/FileAccessHelper.kt ================================================ package knf.kuma.download import android.Manifest import android.annotation.TargetApi import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Environment import android.provider.DocumentsContract import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import knf.kuma.App import knf.kuma.commons.FileUtil import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.findActivity import knf.kuma.commons.getPackage import knf.kuma.commons.isNull import knf.kuma.explorer.creator.Creator import knf.kuma.explorer.creator.DocumentFileCreator import knf.kuma.explorer.creator.SimpleFileCreator import knf.kuma.explorer.creator.SubFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster import java.io.File import java.io.FileFilter import java.io.FileInputStream import java.io.FileOutputStream import java.io.InputStream import java.io.OutputStream object FileAccessHelper { const val SD_REQUEST = 51247 var NOMEDIA_CREATING = false val downloadsDirectory: File get() { return try { if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads") } else { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads") } } catch (e: Exception) { e.printStackTrace() Environment.getDataDirectory() } } val downloadExplorerCreator: Creator get() { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { DocumentFileCreator(treeUri?.let { find(DocumentFile.fromTreeUri(App.context, it), "UKIKU/downloads", false) }) } else { SimpleFileCreator( if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads") } else { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads") } ) } } val internalRoot: File get() = Environment.getExternalStorageDirectory() val externalRoot: File? get() = FileUtil.getFullPathFromTreeUri(treeUri, App.context)?.let { File(it) } val treeUri: Uri? get() { return try { Uri.parse(PreferenceManager.getDefaultSharedPreferences(App.context).getString("tree_uri", null)) } catch (e: Exception) { null } } fun isStoragePermissionEnabled(): Boolean { return when { (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || PrefsUtil.downloadType == "1") -> treeUri != null && DocumentFile.fromTreeUri(App.context, treeUri!!)?.let { it.exists() && it.canWrite() } == true ContextCompat.checkSelfPermission(App.context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED -> true else -> false } } suspend fun isStoragePermissionEnabledAsync(): Boolean { return withContext(Dispatchers.IO) { when { (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || PrefsUtil.downloadType == "1") -> treeUri != null && DocumentFile.fromTreeUri(App.context, treeUri!!)?.let { it.exists() && it.canWrite() } == true ContextCompat.checkSelfPermission(App.context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED -> true else -> false } } } fun getFile(file_name: String?): File { return try { if (file_name.isNullOrEmpty()) throw IllegalStateException("Name can't be null!") if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) } else { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) } } catch (e: Exception) { e.printStackTrace() File(Environment.getDataDirectory(), "test.txt") } } fun findFile(file_name: String?): File { return try { if (file_name.isNullOrEmpty()) throw IllegalStateException("Name can't be null!") if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name)).listFiles { file -> file.name.contains(file_name) }!![0] } else File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name)).listFiles { file -> file.name.contains(file_name) }!![0] } catch (e: Exception) { e.printStackTrace() File(Environment.getDataDirectory(), "test.txt") } } fun fileFindExist(file_name: String?): Boolean { return try { if (file_name.isNullOrEmpty()) throw IllegalStateException("Name can't be null!") if (PrefsUtil.downloadType == "0") { !File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name)).listFiles { file -> file.name.contains(file_name) }.isNullOrEmpty() } else { !find(DocumentFile.fromTreeUri(App.context, treeUri!!), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name), false)?.listFiles()?.mapNotNull { it.name?.contains(file_name) }.isNullOrEmpty() } } catch (e: Exception) { e.printStackTrace() false } } fun getFileUri(file_name: String?): Uri? { if (file_name.isNullOrEmpty()) throw IllegalStateException("Name can't be null!") return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) Uri.fromFile( try { if (file_name.startsWith("$")) { findFile(file_name) } else { if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) } else { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) } } } catch (e: Exception) { e.printStackTrace() File(Environment.getDataDirectory(), "test.txt") } ) else getDataUri(file_name) } val rootFile: File get() { return try { if (PrefsUtil.downloadType == "0") { Environment.getExternalStorageDirectory() } else { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context)) } } catch (e: Exception) { e.printStackTrace() Environment.getExternalStorageDirectory() } } fun getTmpFile(file_name: String): File { return File(getDownloadsCacheDir(), PatternUtil.getNameFromFile(file_name) + file_name) } val toneFile: File get() { return File(App.context.getExternalFilesDir(null), "custom_tone") } fun setToneFile(enable: Boolean) { if (!enable) toneFile.delete() PreferenceManager.getDefaultSharedPreferences(App.context).edit().putBoolean("is_custom_tone", enable).apply() } fun getFileCreate(file_name: String): File? { return try { if (PrefsUtil.downloadType == "0") { val file = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) file.parentFile?.mkdirs() if (!file.exists()) file.createNewFile() file } else { createTmpIfNotExist() val file = File(getDownloadsCacheDir(), PatternUtil.getNameFromFile(file_name) + file_name) file.parentFile?.mkdirs() if (!file.exists()) file.createNewFile() file } } catch (e: Exception) { Log.e("File create", "Error") e.printStackTrace() null } } private fun getDownloadsCacheDir(): File { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) App.context.getExternalFilesDirs("downloads").last() else File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "Android/data/${getPackage()}/files/downloads") } internal fun isTempFile(file: String): Boolean { return try { val path = FileUtil.getFullPathFromTreeUri(treeUri, App.context) ?: return false file.contains(path) } catch (e: Exception) { false } } fun checkNoMedia(noMediaNeeded: Boolean) { NOMEDIA_CREATING = true doAsync { try { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { val file = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads") if (!file.exists()) file.mkdirs() val root = File(file, ".nomedia") if (noMediaNeeded && !root.exists()) root.createNewFile() else if (!noMediaNeeded && root.exists()) root.delete() val list = file.listFiles(FileFilter { it.isDirectory }) if (list != null && list.isNotEmpty()) for (current in list) { val inside = File(current, ".nomedia") if (noMediaNeeded && !inside.exists()) inside.createNewFile() else if (!noMediaNeeded && inside.exists()) inside.delete() } } treeUri?.let { val documentRoot = find(DocumentFile.fromTreeUri(App.context, it), "UKIKU/downloads") val nomediaRoot = documentRoot?.findFile(".nomedia") if (noMediaNeeded && (nomediaRoot == null || !nomediaRoot.exists())) documentRoot?.createFile("application/nomedia", ".nomedia") else if (!noMediaNeeded && nomediaRoot != null && nomediaRoot.exists()) nomediaRoot.delete() val documentList = documentRoot?.listFiles() if (!documentList.isNullOrEmpty()) for (dFile in documentList) { if (dFile.isDirectory) { val inside = dFile.findFile(".nomedia") if (noMediaNeeded && (inside == null || !inside.exists())) dFile.createFile("application/nomedia", ".nomedia") else if (!noMediaNeeded && inside != null && inside.exists()) inside.delete() } } } Toaster.toast("Archivos nomedia " + if (noMediaNeeded) "creados" else "eliminados") NOMEDIA_CREATING = false } catch (e: Exception) { e.printStackTrace() } } } fun getDownloadsDirectory(file_name: String): File { return try { if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/$file_name") } else { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads/$file_name") } } catch (e: Exception) { e.printStackTrace() Environment.getDataDirectory() } } fun getDownloadsDirectoryFiles(file_name: String): List { return try { when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> treeUri?.let { find(DocumentFile.fromTreeUri(App.context, it), "UKIKU/downloads/$file_name", false) }?.listFiles()?.map { SubFile(it.name ?: "", it.uri.toString()) }?.filter { it.name.endsWith(".mp4") } ?: emptyList() PrefsUtil.downloadType == "0" -> { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/$file_name").listFiles()?.map { SubFile(it.name, Uri.fromFile(it).toString()) }?.filter { it.name.endsWith(".mp4") } ?: emptyList() } else -> { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads/$file_name").listFiles()?.map { SubFile(it.name, Uri.fromFile(it).toString()) }?.filter { it.name.endsWith(".mp4") } ?: emptyList() } } } catch (e: Exception) { e.printStackTrace() emptyList() } } fun getDownloadsDirectoryFromFile(file_name: String): File { return try { if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/${PatternUtil.getNameFromFile(file_name)}") } else { File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads/${PatternUtil.getNameFromFile(file_name)}") } } catch (e: Exception) { e.printStackTrace() Environment.getDataDirectory() } } fun delete(file_name: String?, async: Boolean = true) { if (async) doAsync { delete(file_name) } else delete(file_name) } fun deletePath(file_name: String?, async: Boolean = true) { if (async) doAsync { deletePath(file_name) } else deletePath(file_name) } private fun delete(file_name: String?) { if (file_name.isNull()) return try { if (PrefsUtil.downloadType == "0") { val file = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) file.delete() val dir = file.parentFile if (dir?.listFiles() == null || dir.listFiles()?.isEmpty() == true) dir?.delete() } else { treeUri?.let { val documentFile = DocumentFile.fromTreeUri(App.context, it) if (documentFile != null && documentFile.exists()) { val file = find(documentFile, "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) file?.delete() val dir = file?.parentFile if (dir != null && dir.listFiles().isEmpty()) dir.delete() } } } } catch (e: Exception) { e.printStackTrace() } } private fun deletePath(file_name: String?) { if (file_name == null) return try { if (PrefsUtil.downloadType == "0") { val file = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name)).listFiles { file -> file.name.contains(file_name) }!![0] file.delete() val dir = file.parentFile if (dir?.listFiles() == null || dir.listFiles()?.isEmpty() == true) dir?.delete() } else { treeUri?.let { val documentFile = DocumentFile.fromTreeUri(App.context, it) if (documentFile != null && documentFile.exists()) { val file = find(documentFile, "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name))?.listFiles()?.let { var tFile: DocumentFile? = null it.forEach { if (it.name?.contains(file_name) == true) tFile = it } tFile } file?.delete() val dir = file?.parentFile if (dir != null && dir.listFiles().isEmpty()) dir.delete() } } } } catch (e: Exception) { e.printStackTrace() } } private fun createTmpIfNotExist() { if (!getDownloadsCacheDir().exists()) { treeUri?.let { getDownloadsCacheDir().mkdirs() /*val documentFile = DocumentFile.fromTreeUri(App.context, it) if (documentFile != null && documentFile.exists()) { val file = find(documentFile, "Android/data/${getPackage()}/files/downloads/tmp.file") file?.delete() }*/ } } } fun getOutputStream(file_name: String?): OutputStream? { if (file_name == null) return null try { return if (PrefsUtil.downloadType == "0" && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { var file = File( Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) ) if (!file.exists()) file.mkdirs() file = File(file, file_name) if (!file.exists()) file.createNewFile() FileOutputStream( File( Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name ) ) } else { treeUri?.let { App.context.contentResolver.openOutputStream( find( DocumentFile.fromTreeUri(App.context, it), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name )?.uri ?: Uri.EMPTY, "rw") } } } catch (e: Exception) { e.printStackTrace() return null } } fun getFileOutputStream(file_name: String): FileOutputStream? { try { return if (PrefsUtil.downloadType == "0") { var file = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name)) if (!file.exists()) file.mkdirs() file = File(file, file_name) if (!file.exists()) file.createNewFile() FileOutputStream(File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name)) } else { treeUri?.let { FileOutputStream(App.context.contentResolver.openFileDescriptor(find(DocumentFile.fromTreeUri(App.context, it), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name)?.uri ?: Uri.EMPTY, "rw")?.fileDescriptor) } } } catch (e: Exception) { e.printStackTrace() return null } } fun getInputStream(file_name: String): InputStream? { try { return if (PrefsUtil.downloadType == "0") { var file = File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name)) if (!file.exists()) file.mkdirs() file = File(file, file_name) if (!file.exists()) file.createNewFile() FileInputStream(File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name)) } else { treeUri?.let { App.context.contentResolver.openInputStream(find(DocumentFile.fromTreeUri(App.context, it), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name)?.uri ?: Uri.EMPTY) } } } catch (e: Exception) { e.printStackTrace() return null } } fun getTmpInputStream(file_name: String): InputStream? { return try { val file = File(getDownloadsCacheDir(), PatternUtil.getNameFromFile(file_name) + file_name) if (file.parentFile?.exists() == false) file.parentFile?.mkdirs() FileInputStream(file) } catch (e: Exception) { e.printStackTrace() null } } fun existFile(file_name: String): Boolean { return try { if (PrefsUtil.downloadType == "0") { File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name).exists() } else { treeUri?.let { val documentFile = DocumentFile.fromTreeUri(App.context, it) if (documentFile != null && documentFile.exists()) { find(documentFile, "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) } File(FileUtil.getFullPathFromTreeUri(treeUri, App.context), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name).exists() } ?: false } } catch (e: Exception) { e.printStackTrace() false } } fun canDownload(fragment: Fragment): Boolean { return if (PrefsUtil.downloadType == "0") { true } else { try { val uri = treeUri if (uri != null) { val documentFile = DocumentFile.fromTreeUri(App.context, uri) if (documentFile != null && documentFile.exists()) { true } else { openTreeChooser(fragment) false } } else { openTreeChooser(fragment) false } } catch (e: IllegalArgumentException) { openTreeChooser(fragment) false } } } fun canDownload(fragment: Fragment, value: String?): Boolean { return if (value == "0") { true } else { try { val uri = treeUri if (uri != null) { val documentFile = DocumentFile.fromTreeUri(App.context, uri) if (documentFile != null && documentFile.exists()) { true } else { openTreeChooser(fragment) false } } else { openTreeChooser(fragment) false } } catch (e: IllegalArgumentException) { openTreeChooser(fragment) false } } } fun getDataUri(file_name: String): Uri? { try { return if (PrefsUtil.downloadType == "0") { FileProvider.getUriForFile(App.context, "${getPackage()}.fileprovider", if (file_name.startsWith("$")) findFile(file_name) else File(Environment.getExternalStorageDirectory(), "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name)) } else { treeUri?.let { val documentFile = DocumentFile.fromTreeUri(App.context, it) if (documentFile != null && documentFile.exists()) { val root = find(documentFile, "UKIKU/downloads/" + PatternUtil.getNameFromFile(file_name) + file_name) return@let root?.uri } null } } } catch (e: Exception) { e.printStackTrace() return null } } @Throws(Exception::class) fun find(root: DocumentFile?, path: String, create: Boolean = true): DocumentFile? { var fRoot = root for (name in path.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { val file = fRoot?.findFile(name) fRoot = if (file == null || !file.exists()) { if (create) when { name.endsWith(".mp4") -> fRoot?.createFile("video/mp4", name) name.endsWith(".nomedia") -> fRoot?.createFile("application/nomedia", name) else -> fRoot?.createDirectory(name) } fRoot?.findFile(name) } else file } return fRoot } fun isUriValid(uri: Uri?): UriValidation { val uriValidation = UriValidation() uri ?: return uriValidation.also { it.errorMessage = "Uri es nulo" } if (isSDCardRoot(uri, uriValidation)) { if (isInternalStorage(uri)) PrefsUtil.storageType = "Memoria Interna" else PrefsUtil.storageType = "Memoria SD" App.context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) PreferenceManager.getDefaultSharedPreferences(App.context).edit().putString("tree_uri", uri.toString()).apply() uriValidation.isValid = true } return uriValidation } fun openTreeChooser(fragment: Fragment) { try { fragment.startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), SD_REQUEST) Log.e("FileAccess", "On open drocument tree") } catch (e: Exception) { Toaster.toast("Error al buscar SD") } } fun openTreeChooser(context: Context) { try { context.findActivity()?.startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), SD_REQUEST) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Toaster.toastLong("Por favor selecciona un directorio para las descargas") else Toaster.toastLong("Por favor selecciona la raiz del almacenamiento") } catch (e: Exception) { Toaster.toast("Error al buscar SD") } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private fun isSDCardRoot(uri: Uri, uriValidation: UriValidation): Boolean { return isRootUri(uri, uriValidation) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || isExternalStorageDocument(uri, uriValidation)) } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun isRootUri(uri: Uri, uriValidation: UriValidation): Boolean { return DocumentsContract.getTreeDocumentId(uri).endsWith(":") .also { if (!it) { Log.e("Storage", "$uri is not root") uriValidation.errorMessage = "No es la raiz!" } } || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun isInternalStorage(uri: Uri, uriValidation: UriValidation): Boolean { return isExternalStorageDocument(uri, uriValidation) && DocumentsContract.getTreeDocumentId(uri).contains("primary") .also { if (it) { Log.e("Storage", "$uri is internal storage") uriValidation.errorMessage = "Memoria interna" } } } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun isInternalStorage(uri: Uri): Boolean { return isExternalStorageDocument(uri) && DocumentsContract.getTreeDocumentId(uri).contains("primary") } private fun isExternalStorageDocument(uri: Uri, uriValidation: UriValidation): Boolean { return ("com.android.externalstorage.documents" == uri.authority).also { if (!it) { Log.e("Storage", "$uri is not external storage document") uriValidation.errorMessage = "No es almacenamiento externo" } } } private fun isExternalStorageDocument(uri: Uri): Boolean { return ("com.android.externalstorage.documents" == uri.authority) } } ================================================ FILE: app/src/main/java/knf/kuma/download/MultipleDownloadManager.kt ================================================ package knf.kuma.download import android.content.Context import android.os.Build import android.os.StatFs import android.text.format.Formatter import android.util.Log import android.view.View import androidx.fragment.app.Fragment import knf.kuma.commons.toast import knf.kuma.pojos.AnimeObject import knf.kuma.videoservers.FileActions object MultipleDownloadManager { private const val CHAPTER_SIZE = 160000000L private var index = 0 private var chaptersList: List = listOf() var isLoading = false var langSelected = -1 fun startDownload(fragment: Fragment, view: View, list: List, addQueue: Boolean) { if (list.isEmpty()) return if (!addQueue && !isSpaceAvailable(list.size)) { "Se requieren mínimo ${minSpaceString(fragment.requireContext(), list.size)} libres!".toast() return } clear(list) isLoading = true downloadNext(fragment, view, addQueue) } private fun downloadNext(fragment: Fragment, view: View, addQueue: Boolean) { if (index >= chaptersList.size || !fragment.isAdded || fragment.context == null) { isLoading = false langSelected = -1 return } val current = chaptersList[index] val callback: (FileActions.CallbackState, Any?) -> Unit = { state, _ -> when (state) { FileActions.CallbackState.USER_CANCELLED, FileActions.CallbackState.MISSING_PERMISSION, FileActions.CallbackState.LOW_STORAGE, FileActions.CallbackState.LIFECYCLE_EXPIRED -> { Log.e("MultiDownload", "Cancel processing") clear(emptyList()) } FileActions.CallbackState.OPERATION_RUNNING -> { Log.e("MultiDownload", "Running") } else -> { index++ downloadNext(fragment, view, addQueue) Log.e("MultiDownload", "on Next") } } } if (!addQueue) FileActions.download(fragment.requireContext(), fragment.viewLifecycleOwner, current, view, callback) else FileActions.queuedStream(fragment.requireContext(), fragment.viewLifecycleOwner, current, view, callback) } private fun clear(list: List) { index = 0 chaptersList = list isLoading = false langSelected = -1 } private fun minSpaceString(context: Context, size: Int): String { return Formatter.formatFileSize(context, size * CHAPTER_SIZE) } fun isSpaceAvailable(size: Int): Boolean { return try { getAvailable() > size * CHAPTER_SIZE } catch (e: Exception) { true } || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R } private fun getAvailable(): Long { val stat = StatFs(FileAccessHelper.rootFile.path) return stat.blockSizeLong * stat.availableBlocksLong } } ================================================ FILE: app/src/main/java/knf/kuma/download/UriValidation.kt ================================================ package knf.kuma.download class UriValidation { var errorMessage: String? = null var isValid = false override fun toString(): String { return errorMessage ?: "Error desconocido" } } ================================================ FILE: app/src/main/java/knf/kuma/download/downloadKt.kt ================================================ package knf.kuma.download import android.app.Notification import android.app.Service import android.app.job.JobInfo import android.app.job.JobScheduler import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo import android.net.NetworkCapabilities import android.net.NetworkRequest import android.os.Build import androidx.core.content.ContextCompat import knf.kuma.ads.AdsUtils import knf.kuma.commons.noCrash import org.jetbrains.anko.activityManager import java.util.Locale val isDeviceSamsung: Boolean get() = Build.MANUFACTURER.lowercase(Locale.getDefault()) == "samsung" fun Context.service(intent: Intent) { noCrash { if (isDeviceSamsung && AdsUtils.remoteConfigs.getBoolean("samsung_disable_foreground")) startService(intent) else ContextCompat.startForegroundService(this, intent) } } fun Context.UIDT(klass: Class<*>) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { val network = NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build() val info = JobInfo.Builder(23498, ComponentName(this, klass)) .setUserInitiated(true) .setRequiredNetwork(network) .build() (getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler).schedule(info) } } fun Service.foreground(id: Int, notification: Notification, isDataSync: Boolean = true) { noCrash { if (isDeviceSamsung && AdsUtils.remoteConfigs.getBoolean("samsung_disable_foreground")) return@noCrash if (Build.VERSION.SDK_INT >= 34) { startForeground(id, notification, if (isDataSync) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) } else { startForeground(id, notification) } } } fun Context.isServiceRunning(serviceClass: Class<*>): Boolean{ activityManager.getRunningServices(Int.MAX_VALUE).forEach { if (it.service.className == serviceClass.name) return true } return false } ================================================ FILE: app/src/main/java/knf/kuma/emision/AnimeSubObject.kt ================================================ package knf.kuma.emision import knf.kuma.commons.PrefsUtil import knf.kuma.search.SearchObject class AnimeSubObject : SearchObject() { var fileName = "" fun getFinalName(): String { return if (PrefsUtil.saveWithName) fileName else aid } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionActivity.kt ================================================ package knf.kuma.emision import android.app.Activity import android.content.Intent import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem import com.google.android.material.tabs.TabLayout import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityEmisionBinding import java.util.Calendar class EmissionActivity : GenericActivity(), TabLayout.OnTabSelectedListener { private var pagerAdapter: EmissionPagerAdapter? = null private val binding by lazy { ActivityEmisionBinding.inflate(layoutInflater) } private val currentDay: Int get() { var day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) day-- return when (day) { 0 -> 7 else -> day } } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) binding.toolbar.title = "Emisión" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.toolbar.setNavigationOnClickListener { finish() } binding.pager.offscreenPageLimit = 7 pagerAdapter = EmissionPagerAdapter(supportFragmentManager) binding.pager.adapter = pagerAdapter binding.tabs.setupWithViewPager(binding.pager) binding.tabs.addOnTabSelectedListener(this) binding.pager.setCurrentItem(currentDay - 1, true) EAHelper.clear2() showRandomInterstitial(this,PrefsUtil.fullAdsExtraProbability) } override fun onResume() { super.onResume() pagerAdapter?.updateChanges() } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_emision, menu) if (PrefsUtil.emissionShowHidden) menu.findItem(R.id.action_hideshow).setIcon(R.drawable.ic_hide_pref) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_hideshow -> { Log.e("Emission", "On menu click") val show = PrefsUtil.emissionShowHidden PrefsUtil.emissionShowHidden = !show pagerAdapter?.reloadPages() } } invalidateOptionsMenu() return super.onOptionsItemSelected(item) } override fun onTabSelected(tab: TabLayout.Tab) { EAHelper.enter2(getDayByPos(tab.position).toString()) } override fun onTabUnselected(tab: TabLayout.Tab) { } override fun onTabReselected(tab: TabLayout.Tab) { EAHelper.enter2(getDayByPos(tab.position).toString()) } private fun getDayByPos(position: Int): Int { var pos = position + 2 if (pos == 8) pos = 1 return pos } companion object { fun open(context: Activity) { context.startActivityForResult(Intent(context, EmissionActivity::class.java), 4987) } } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionActivityMaterial.kt ================================================ package knf.kuma.emision import android.app.Activity import android.content.Intent import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem import com.google.android.material.tabs.TabLayout import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityEmisionMaterialBinding import java.util.Calendar class EmissionActivityMaterial : GenericActivity(), TabLayout.OnTabSelectedListener { private var pagerAdapter: EmissionPagerAdapterMaterial? = null private val binding by lazy { ActivityEmisionMaterialBinding.inflate(layoutInflater) } private val currentDay: Int get() { var day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) day-- return when (day) { 0 -> 7 else -> day } } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) binding.toolbar.title = "Emisión" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.toolbar.setNavigationOnClickListener { finish() } binding.pager.offscreenPageLimit = 7 pagerAdapter = EmissionPagerAdapterMaterial(supportFragmentManager) binding.pager.adapter = pagerAdapter binding.tabs.setupWithViewPager(binding.pager) binding.tabs.addOnTabSelectedListener(this) binding.pager.setCurrentItem(currentDay - 1, true) EAHelper.clear2() showRandomInterstitial(this,PrefsUtil.fullAdsExtraProbability) } override fun onResume() { super.onResume() pagerAdapter?.updateChanges() } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_emision, menu) if (PrefsUtil.emissionShowHidden) menu.findItem(R.id.action_hideshow).setIcon(R.drawable.ic_hide_pref) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_hideshow -> { Log.e("Emission", "On menu click") val show = PrefsUtil.emissionShowHidden PrefsUtil.emissionShowHidden = !show pagerAdapter?.reloadPages() } } invalidateOptionsMenu() return super.onOptionsItemSelected(item) } override fun onTabSelected(tab: TabLayout.Tab) { EAHelper.enter2(getDayByPos(tab.position).toString()) } override fun onTabUnselected(tab: TabLayout.Tab) { } override fun onTabReselected(tab: TabLayout.Tab) { EAHelper.enter2(getDayByPos(tab.position).toString()) } private fun getDayByPos(position: Int): Int { var pos = position + 2 if (pos == 8) pos = 1 return pos } companion object { fun open(context: Activity) { context.startActivityForResult(Intent(context, EmissionActivityMaterial::class.java), 4987) } } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionAdapter.kt ================================================ package knf.kuma.emision import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.distinct import knf.kuma.commons.doOnUI import knf.kuma.commons.load import knf.kuma.commons.notSameContent import knf.kuma.custom.HiddenOverlay import knf.kuma.database.CacheDB import knf.kuma.search.SearchObjectFav import knf.kuma.widgets.emision.WEmisionProvider import org.jetbrains.anko.doAsync import org.jetbrains.anko.find class EmissionAdapter internal constructor(private val fragment: Fragment) : RecyclerView.Adapter() { val removeListener = fragment as RemoveListener var list: MutableList = ArrayList() private var blacklist: MutableSet = PrefsUtil.emissionBlacklist private var showHidden: Boolean = PrefsUtil.emissionShowHidden private val showHeart = PrefsUtil.emissionShowFavs override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmissionItem { return EmissionItem(LayoutInflater.from(parent.context).inflate(R.layout.item_emision, parent, false)) } override fun onBindViewHolder(holder: EmissionItem, position: Int) { val animeObject = list[position] holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.title.text = animeObject.name holder.hiddenOverlay.setHidden(blacklist.contains(animeObject.aid), false) holder.heart.visibility = when { showHeart && animeObject.isFav -> View.VISIBLE else -> View.GONE } //holder.observeFav(fragment, animeObject.aid, showHeart) holder.cardView.setOnClickListener { ActivityAnime.open(fragment, animeObject, holder.imageView, false, animate = true) } holder.cardView.setOnLongClickListener { val removed: Boolean = if (blacklist.contains(animeObject.aid)) { updateList(true, animeObject.aid) true } else { updateList(false, animeObject.aid) false } if (showHidden) { holder.hiddenOverlay.setHidden(!removed, true) } else if (!removed) { remove(holder.adapterPosition) } true } } override fun getItemCount(): Int { return list.size } fun update(newList: MutableList, animate: Boolean = true, callback: () -> Unit) { if (list notSameContent newList) if (PrefsUtil.useSmoothAnimations && newList.isNotEmpty()) doAsync { blacklist = PrefsUtil.emissionBlacklist showHidden = PrefsUtil.emissionShowHidden val result = if (animate) DiffUtil.calculateDiff(EmissionDiff(list, newList), true) else null list = newList fragment.doOnUI { try { if (animate) result?.dispatchUpdatesTo(this@EmissionAdapter) else notifyDataSetChanged() } catch (e: Exception) { e.printStackTrace() notifyDataSetChanged() } callback.invoke() } } else { blacklist = PrefsUtil.emissionBlacklist showHidden = PrefsUtil.emissionShowHidden list = newList notifyDataSetChanged() } } private fun updateList(remove: Boolean, aid: String) { this.blacklist = LinkedHashSet(PrefsUtil.emissionBlacklist) if (remove) blacklist.remove(aid) else blacklist.add(aid) PrefsUtil.emissionBlacklist = blacklist WEmisionProvider.update(fragment.context) } fun remove(position: Int) { if (position >= 0 && position <= list.size - 1) { list.removeAt(position) notifyItemRemoved(position) removeListener.onRemove(list.size <= 0) } } class EmissionItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView = itemView.find(R.id.card) val imageView: ImageView = itemView.find(R.id.img) val hiddenOverlay: HiddenOverlay = itemView.find(R.id.hidden) val heart: ImageView = itemView.find(R.id.heart) val title: TextView = itemView.find(R.id.title) private lateinit var liveData: LiveData private lateinit var observer: Observer fun observeFav(fragment: Fragment, aid: String, show: Boolean) { if (::liveData.isInitialized && ::observer.isInitialized) liveData.removeObserver(observer) if (!show) { heart.visibility = View.GONE return } liveData = CacheDB.INSTANCE.favsDAO().isFavLive(aid.toInt()).distinct observer = Observer { if (!PrefsUtil.emissionShowFavs) heart.visibility = View.GONE else heart.visibility = if (it) View.VISIBLE else View.GONE } liveData.observe(fragment, observer) } } } internal class EmissionDiff( private val oldList: MutableList, private val newList: MutableList) : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } override fun getOldListSize(): Int { return oldList.size } override fun getNewListSize(): Int { return newList.size } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].isFav == newList[newItemPosition].isFav } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionAdapterMaterial.kt ================================================ package knf.kuma.emision import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.distinct import knf.kuma.commons.doOnUI import knf.kuma.commons.load import knf.kuma.commons.notSameContent import knf.kuma.custom.HiddenOverlay import knf.kuma.database.CacheDB import knf.kuma.search.SearchObjectFav import knf.kuma.widgets.emision.WEmisionProvider import org.jetbrains.anko.doAsync import org.jetbrains.anko.find class EmissionAdapterMaterial internal constructor(private val fragment: Fragment) : RecyclerView.Adapter() { val removeListener = fragment as RemoveListener var list: MutableList = ArrayList() private var blacklist: MutableSet = PrefsUtil.emissionBlacklist private var showHidden: Boolean = PrefsUtil.emissionShowHidden private val showHeart = PrefsUtil.emissionShowFavs override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmissionItem { return EmissionItem(LayoutInflater.from(parent.context).inflate(R.layout.item_emision_material, parent, false)) } override fun onBindViewHolder(holder: EmissionItem, position: Int) { val animeObject = list[position] holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.title.text = animeObject.name holder.hiddenOverlay.setHidden(blacklist.contains(animeObject.aid), false) holder.heart.visibility = when { showHeart && animeObject.isFav -> View.VISIBLE else -> View.GONE } //holder.observeFav(fragment, animeObject.aid, showHeart) holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, animeObject, holder.imageView, false, animate = true) } holder.cardView.setOnLongClickListener { val removed: Boolean = if (blacklist.contains(animeObject.aid)) { updateList(true, animeObject.aid) true } else { updateList(false, animeObject.aid) false } if (showHidden) { holder.hiddenOverlay.setHidden(!removed, true) } else if (!removed) { remove(holder.adapterPosition) } true } } override fun getItemCount(): Int { return list.size } fun update(newList: MutableList, animate: Boolean = true, callback: () -> Unit) { if (list notSameContent newList) if (PrefsUtil.useSmoothAnimations && newList.isNotEmpty()) doAsync { blacklist = PrefsUtil.emissionBlacklist showHidden = PrefsUtil.emissionShowHidden val result = if (animate) DiffUtil.calculateDiff(EmissionDiff(list, newList), true) else null list = newList fragment.doOnUI { try { if (animate) result?.dispatchUpdatesTo(this@EmissionAdapterMaterial) else notifyDataSetChanged() } catch (e: Exception) { e.printStackTrace() notifyDataSetChanged() } callback.invoke() } } else { blacklist = PrefsUtil.emissionBlacklist showHidden = PrefsUtil.emissionShowHidden list = newList notifyDataSetChanged() } } private fun updateList(remove: Boolean, aid: String) { this.blacklist = LinkedHashSet(PrefsUtil.emissionBlacklist) if (remove) blacklist.remove(aid) else blacklist.add(aid) PrefsUtil.emissionBlacklist = blacklist WEmisionProvider.update(fragment.context) } fun remove(position: Int) { if (position >= 0 && position <= list.size - 1) { list.removeAt(position) notifyItemRemoved(position) removeListener.onRemove(list.size <= 0) } } class EmissionItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View = itemView.find(R.id.card) val imageView: ImageView = itemView.find(R.id.img) val hiddenOverlay: HiddenOverlay = itemView.find(R.id.hidden) val heart: View = itemView.find(R.id.heart) val title: TextView = itemView.find(R.id.title) private lateinit var liveData: LiveData private lateinit var observer: Observer fun observeFav(fragment: Fragment, aid: String, show: Boolean) { if (::liveData.isInitialized && ::observer.isInitialized) liveData.removeObserver(observer) if (!show) { heart.visibility = View.GONE return } liveData = CacheDB.INSTANCE.favsDAO().isFavLive(aid.toInt()).distinct observer = Observer { if (!PrefsUtil.emissionShowFavs) heart.visibility = View.GONE else heart.visibility = if (it) View.VISIBLE else View.GONE } liveData.observe(fragment, observer) } } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionFragment.kt ================================================ package knf.kuma.emision import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.PrefsUtil import knf.kuma.commons.distinct import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerEmisionBinding import knf.kuma.pojos.AnimeObject import knf.kuma.search.SearchObject import knf.kuma.search.forFav import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class EmissionFragment : Fragment(), RemoveListener { private var adapter: EmissionAdapter? = null private var isFirst = true private lateinit var binding: RecyclerEmisionBinding private lateinit var liveData: LiveData> private lateinit var observer: Observer> private val blacklist: Set get() = if (PrefsUtil.emissionShowHidden) LinkedHashSet() else PrefsUtil.emissionBlacklist override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return LayoutInflater.from(context).inflate(R.layout.recycler_emision, container, false).also { binding = RecyclerEmisionBinding.bind(it) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { delay(1000) binding.adContainer.implBanner(AdsType.EMISSION_BANNER, true) } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) adapter = EmissionAdapter(this) binding.recycler.verifyManager() binding.recycler.adapter = adapter if (context != null) observeList(Observer { animeObjects -> lifecycleScope.launch(Dispatchers.Main){ binding.progress.visibility = View.GONE adapter?.update(withContext(Dispatchers.IO) { animeObjects.map { it.forFav() }.toMutableList() }, false) { smoothScroll() } if (isFirst) { isFirst = false binding.recycler.scheduleLayoutAnimation() //checkStates(animeObjects) } binding.error.visibility = if (animeObjects.isEmpty()) View.VISIBLE else View.GONE } }) } private fun observeList(obs: Observer>) { if (::liveData.isInitialized && ::observer.isInitialized) liveData.removeObserver(observer) liveData = CacheDB.INSTANCE.animeDAO().getByDay(arguments?.getInt("day", 1) ?: 1, blacklist).distinct observer = obs liveData.observe(viewLifecycleOwner, observer) } override fun onRemove(showError: Boolean) { if (showError) lifecycleScope.launch(Dispatchers.Main) { binding.error.visibility = View.VISIBLE } } private fun smoothScroll() { //recycler.layoutManager?.smoothScrollToPosition(recycler,null,0) } fun updateChanges() { lifecycleScope.launch(Dispatchers.Main) { adapter?.notifyDataSetChanged() } } internal fun reloadList() { if (context != null) observeList { animeObjects -> lifecycleScope.launch(Dispatchers.Main) { binding.error.visibility = View.GONE if (animeObjects != null && animeObjects.isNotEmpty()) adapter?.update(withContext(Dispatchers.IO) { animeObjects.map { it.forFav() }.toMutableList() }) { smoothScroll() } else adapter?.update(ArrayList()) { smoothScroll() } if (animeObjects == null || animeObjects.isEmpty()) binding.error.visibility = View.VISIBLE } } } companion object { operator fun get(day: AnimeObject.Day): EmissionFragment { val bundle = Bundle() bundle.putInt("day", day.value) val fragment = EmissionFragment() fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionFragmentMaterial.kt ================================================ package knf.kuma.emision import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.PrefsUtil import knf.kuma.commons.distinct import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerEmisionBinding import knf.kuma.pojos.AnimeObject import knf.kuma.search.SearchObject import knf.kuma.search.forFav import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class EmissionFragmentMaterial : Fragment(), RemoveListener { private var adapter: EmissionAdapterMaterial? = null private var isFirst = true private lateinit var binding: RecyclerEmisionBinding private lateinit var liveData: LiveData> private lateinit var observer: Observer> private val blacklist: Set get() = if (PrefsUtil.emissionShowHidden) LinkedHashSet() else PrefsUtil.emissionBlacklist override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return LayoutInflater.from(context).inflate(R.layout.recycler_emision, container, false).also { binding = RecyclerEmisionBinding.bind(it) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { delay(1000) binding.adContainer.implBanner(AdsType.EMISSION_BANNER, true) } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) adapter = EmissionAdapterMaterial(this) binding.recycler.verifyManager() binding.recycler.adapter = adapter if (context != null) observeList { animeObjects -> lifecycleScope.launch(Dispatchers.Main) { binding.progress.visibility = View.GONE adapter?.update(withContext(Dispatchers.IO) { animeObjects.map { it.forFav() }.toMutableList() }, false) { smoothScroll() } if (isFirst) { isFirst = false binding.recycler.scheduleLayoutAnimation() //checkStates(animeObjects) } binding.error.visibility = if (animeObjects.isEmpty()) View.VISIBLE else View.GONE } } } private fun observeList(obs: Observer>) { if (::liveData.isInitialized && ::observer.isInitialized) liveData.removeObserver(observer) liveData = CacheDB.INSTANCE.animeDAO().getByDay(arguments?.getInt("day", 1) ?: 1, blacklist).distinct observer = obs liveData.observe(viewLifecycleOwner, observer) } override fun onRemove(showError: Boolean) { if (showError) lifecycleScope.launch(Dispatchers.Main) { binding.error.visibility = View.VISIBLE } } private fun smoothScroll() { //recycler.layoutManager?.smoothScrollToPosition(recycler,null,0) } fun updateChanges() { lifecycleScope.launch(Dispatchers.Main) { adapter?.notifyDataSetChanged() } } internal fun reloadList() { if (context != null) observeList { animeObjects -> lifecycleScope.launch(Dispatchers.Main) { binding.error.visibility = View.GONE if (animeObjects != null && animeObjects.isNotEmpty()) adapter?.update(withContext(Dispatchers.IO) { animeObjects.map { it.forFav() }.toMutableList() }) { smoothScroll() } else adapter?.update(ArrayList()) { smoothScroll() } if (animeObjects == null || animeObjects.isEmpty()) binding.error.visibility = View.VISIBLE } } } companion object { operator fun get(day: AnimeObject.Day): EmissionFragmentMaterial { val bundle = Bundle() bundle.putInt("day", day.value) val fragment = EmissionFragmentMaterial() fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionPagerAdapter.kt ================================================ package knf.kuma.emision import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import knf.kuma.pojos.AnimeObject class EmissionPagerAdapter internal constructor(fm: FragmentManager) : FragmentPagerAdapter(fm) { private val monday = EmissionFragment[AnimeObject.Day.MONDAY] private val tuesday = EmissionFragment[AnimeObject.Day.TUESDAY] private val wednesday = EmissionFragment[AnimeObject.Day.WEDNESDAY] private val thursday = EmissionFragment[AnimeObject.Day.THURSDAY] private val friday = EmissionFragment[AnimeObject.Day.FRIDAY] private val saturday = EmissionFragment[AnimeObject.Day.SATURDAY] private val sunday = EmissionFragment[AnimeObject.Day.SUNDAY] override fun getCount(): Int { return 7 } override fun getPageTitle(position: Int): CharSequence? { return when (position) { 0 -> "Lunes" 1 -> "Martes" 2 -> "Miércoles" 3 -> "Jueves" 4 -> "Viernes" 5 -> "Sábado" 6 -> "Domingo" else -> "Lunes" } } override fun getItem(position: Int): Fragment { return when (position) { 0 -> monday 1 -> tuesday 2 -> wednesday 3 -> thursday 4 -> friday 5 -> saturday 6 -> sunday else -> monday } } fun updateChanges() { monday.updateChanges() tuesday.updateChanges() wednesday.updateChanges() thursday.updateChanges() friday.updateChanges() saturday.updateChanges() sunday.updateChanges() } fun reloadPages() { monday.reloadList() tuesday.reloadList() wednesday.reloadList() thursday.reloadList() friday.reloadList() saturday.reloadList() sunday.reloadList() } } ================================================ FILE: app/src/main/java/knf/kuma/emision/EmissionPagerAdapterMaterial.kt ================================================ package knf.kuma.emision import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import knf.kuma.pojos.AnimeObject class EmissionPagerAdapterMaterial internal constructor(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { private val monday = EmissionFragmentMaterial[AnimeObject.Day.MONDAY] private val tuesday = EmissionFragmentMaterial[AnimeObject.Day.TUESDAY] private val wednesday = EmissionFragmentMaterial[AnimeObject.Day.WEDNESDAY] private val thursday = EmissionFragmentMaterial[AnimeObject.Day.THURSDAY] private val friday = EmissionFragmentMaterial[AnimeObject.Day.FRIDAY] private val saturday = EmissionFragmentMaterial[AnimeObject.Day.SATURDAY] private val sunday = EmissionFragmentMaterial[AnimeObject.Day.SUNDAY] override fun getCount(): Int { return 7 } override fun getPageTitle(position: Int): CharSequence? { return when (position) { 0 -> "Lunes" 1 -> "Martes" 2 -> "Miércoles" 3 -> "Jueves" 4 -> "Viernes" 5 -> "Sábado" 6 -> "Domingo" else -> "Lunes" } } override fun getItem(position: Int): Fragment { return when (position) { 0 -> monday 1 -> tuesday 2 -> wednesday 3 -> thursday 4 -> friday 5 -> saturday 6 -> sunday else -> monday } } fun updateChanges() { monday.updateChanges() tuesday.updateChanges() wednesday.updateChanges() thursday.updateChanges() friday.updateChanges() saturday.updateChanges() sunday.updateChanges() } fun reloadPages() { monday.reloadList() tuesday.reloadList() wednesday.reloadList() thursday.reloadList() friday.reloadList() saturday.reloadList() sunday.reloadList() } } ================================================ FILE: app/src/main/java/knf/kuma/emision/RemoveListener.kt ================================================ package knf.kuma.emision interface RemoveListener { fun onRemove(showError: Boolean) } ================================================ FILE: app/src/main/java/knf/kuma/explorer/DownloadingAdapter.kt ================================================ package knf.kuma.explorer import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ProgressBar import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.database.CacheDB import knf.kuma.download.DownloadManagerCentral import knf.kuma.pojos.DownloadObject import org.jetbrains.anko.find import java.util.Locale class DownloadingAdapter internal constructor(private val fragment: Fragment, private val downloadObjects: MutableList) : RecyclerView.Adapter() { private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): DownloadingItem { return DownloadingItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.item_downloading_extra, viewGroup, false)) } @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: DownloadingItem, position: Int) { val downloadObject = downloadObjects[position] holder.server.text = downloadObject.downloadServer holder.title.text = downloadObject.name holder.chapter.text = downloadObject.chapter holder.eta.text = downloadObject.subtext holder.progress.max = 100 holder.action.visibility = if (downloadObject.canResume) View.VISIBLE else View.INVISIBLE if (downloadObject.state == DownloadObject.PENDING) { holder.eta.visibility = View.GONE holder.progress.isIndeterminate = true holder.progress.progress = 0 } else { if (downloadObject.state == DownloadObject.PAUSED) holder.eta.visibility = View.GONE else holder.eta.visibility = View.VISIBLE holder.progress.isIndeterminate = false holder.progress.progress = downloadObject.progress } holder.action.setOnClickListener { if (downloadObject.state == DownloadObject.DOWNLOADING) { downloadObject.state = DownloadObject.PAUSED holder.action.text = "REANUDAR" DownloadManagerCentral.pause(downloadObject) } else if (downloadObject.state == DownloadObject.PAUSED) { downloadObject.state = DownloadObject.PENDING holder.action.text = "PAUSAR" DownloadManagerCentral.resume(downloadObject) } } holder.cancel.setOnClickListener { fragment.context?.let { MaterialDialog(it).safeShow { message(text = "¿Cancelar descarga del ${downloadObject.chapter.lowercase(Locale.getDefault())} de ${downloadObject.name}?") positiveButton(text = "CONFIRMAR") { try { downloadObjects.removeAt(holder.bindingAdapterPosition) notifyItemRemoved(holder.bindingAdapterPosition) DownloadManagerCentral.cancel(downloadObject.eid) } catch (_: Exception) { // } } negativeButton(text = "CANCELAR") } } } downloadsDAO.getLiveByKey(downloadObject.key).observe(fragment, Observer { downloadObject1 -> try { if (downloadObject1 == null || downloadObject1.state == DownloadObject.COMPLETED) { downloadObjects.removeAt(holder.bindingAdapterPosition) notifyItemRemoved(holder.bindingAdapterPosition) } else { downloadObject.state = downloadObject1.state if (downloadObject1.state == DownloadObject.PENDING) { holder.eta.visibility = View.GONE holder.progress.isIndeterminate = true holder.progress.progress = 0 } else { when (downloadObject.state) { DownloadObject.DOWNLOADING -> { holder.action.text = "PAUSAR" holder.eta.visibility = View.VISIBLE } DownloadObject.PAUSED -> { holder.action.text = "REANUDAR" holder.eta.visibility = View.GONE } } holder.progress.isIndeterminate = false if (downloadObject1.getEta() == -2L || PrefsUtil.downloaderType == 0) holder.progress.setProgress(downloadObject1.progress, true) else { holder.progress.progress = 0 holder.progress.secondaryProgress = downloadObject1.progress } holder.eta.text = downloadObject1.subtext } } } catch (_: Exception) { // } }) } override fun getItemCount(): Int { return downloadObjects.size } fun remove(eid: String) { ArrayList(downloadObjects).forEachIndexed { index, downloadObject -> if (downloadObject.eid == eid) { downloadObjects.removeAt(index) fragment.doOnUI { notifyItemRemoved(index) } return } } } class DownloadingItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val server: TextView = itemView.find(R.id.server) val title: TextView = itemView.find(R.id.title) val chapter: TextView = itemView.find(R.id.chapter) val eta: TextView = itemView.find(R.id.eta) val action: Button = itemView.find(R.id.action) val cancel: Button = itemView.find(R.id.cancel) val progress: ProgressBar = itemView.find(R.id.progress) } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/DownloadingAdapterMaterial.kt ================================================ package knf.kuma.explorer import android.annotation.SuppressLint import android.os.Build import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.onClickMenu import knf.kuma.commons.safeShow import knf.kuma.database.CacheDB import knf.kuma.download.DownloadManagerCentral import knf.kuma.pojos.DownloadObject import org.jetbrains.anko.find import java.util.Locale class DownloadingAdapterMaterial internal constructor(private val fragment: Fragment, private val downloadObjects: MutableList) : RecyclerView.Adapter() { private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): DownloadingItem { return DownloadingItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.item_downloading_extra_material, viewGroup, false)) } @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: DownloadingItem, position: Int) { val downloadObject = downloadObjects[position] holder.server.text = downloadObject.downloadServer holder.title.text = downloadObject.name holder.chapter.text = downloadObject.chapter holder.eta.text = downloadObject.subtext holder.progress.max = 100 if (downloadObject.state == DownloadObject.PENDING) { holder.eta.visibility = View.GONE holder.progress.isIndeterminate = true holder.progress.progress = 0 } else { if (downloadObject.state == DownloadObject.PAUSED) holder.eta.visibility = View.GONE else holder.eta.visibility = View.VISIBLE holder.progress.isIndeterminate = false holder.progress.progress = downloadObject.progress } holder.actionMenu.onClickMenu(R.menu.menu_download_options,hideItems = { downloadObject.getDisabledOptions() }){ item -> when(item.itemId){ R.id.pause -> { downloadObject.state = DownloadObject.PAUSED DownloadManagerCentral.pause(downloadObject) } R.id.resume -> { downloadObject.state = DownloadObject.PENDING DownloadManagerCentral.resume(downloadObject) } R.id.cancel -> { fragment.context?.let { MaterialDialog(it).safeShow { message(text = "¿Cancelar descarga del ${downloadObject.chapter.lowercase(Locale.getDefault())} de ${downloadObject.name}?") positiveButton(text = "CONFIRMAR") { try { downloadObjects.removeAt(holder.adapterPosition) notifyItemRemoved(holder.adapterPosition) DownloadManagerCentral.cancel(downloadObject.eid) } catch (e: Exception) { // } } negativeButton(text = "CANCELAR") } } } } } downloadsDAO.getLiveByKey(downloadObject.key).observe(fragment, Observer { downloadObject1 -> try { if (downloadObject1 == null || downloadObject1.state == DownloadObject.COMPLETED) { downloadObjects.removeAt(holder.adapterPosition) notifyItemRemoved(holder.adapterPosition) } else { downloadObject.state = downloadObject1.state if (downloadObject1.state == DownloadObject.PENDING) { holder.eta.visibility = View.GONE holder.progress.isIndeterminate = true holder.progress.progress = 0 } else { holder.progress.isIndeterminate = false if (downloadObject1.getEta() == -2L || PrefsUtil.downloaderType == 0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) holder.progress.setProgress(downloadObject1.progress, true) else holder.progress.progress = downloadObject1.progress else { holder.progress.progress = 0 holder.progress.secondaryProgress = downloadObject1.progress } holder.eta.visibility = View.VISIBLE holder.eta.text = downloadObject1.subtext } } } catch (e: Exception) { // } }) } override fun getItemCount(): Int { return downloadObjects.size } fun remove(eid: String) { ArrayList(downloadObjects).forEachIndexed { index, downloadObject -> if (downloadObject.eid == eid) { downloadObjects.removeAt(index) fragment.doOnUI { notifyItemRemoved(index) } return } } } private fun DownloadObject.getDisabledOptions(): List { val list = mutableListOf() if (canResume && (state == DownloadObject.DOWNLOADING || state == DownloadObject.PAUSED)){ if (state == DownloadObject.DOWNLOADING) list.add(R.id.resume) if (state == DownloadObject.PAUSED) list.add(R.id.pause) }else{ list.add(R.id.resume) list.add(R.id.pause) } return list } class DownloadingItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val server: TextView = itemView.find(R.id.server) val title: TextView = itemView.find(R.id.title) val chapter: TextView = itemView.find(R.id.chapter) val eta: TextView = itemView.find(R.id.eta) val actionMenu: View = itemView.find(R.id.actionMenu) val progress: ProgressBar = itemView.find(R.id.progress) } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerActivity.kt ================================================ package knf.kuma.explorer import androidx.activity.addCallback import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.commons.CastUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityExplorerBinding class ExplorerActivity : GenericActivity(), OnFileStateChange { private val binding by lazy { ActivityExplorerBinding.inflate(layoutInflater) } private var adapter: ExplorerPagerAdapter? = null private var isExplorerFiles = true override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) binding.toolbar.title = "Explorador" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } if (savedInstanceState == null) ExplorerCreator.onDestroy() binding.pager.offscreenPageLimit = 2 adapter = ExplorerPagerAdapter(this, supportFragmentManager) binding.pager.adapter = adapter binding.tabs.setupWithViewPager(binding.pager) onBackPressedDispatcher.addCallback(this) { val currentFragment = adapter?.getItem(binding.pager.currentItem) as? FragmentBase if (currentFragment?.onBackPressed() != true) { isEnabled = false onBackPressedDispatcher.onBackPressed() isEnabled = true } } showRandomInterstitial(this, PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_explorer_connected, menu) if (isExplorerFiles) menu.findItem(R.id.delete_all).isVisible = false CastUtil.registerActivity(this, menu, R.id.castMenu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.delete_all -> adapter?.onRemoveAllClicked() } return super.onOptionsItemSelected(item) } override fun onChange(isFile: Boolean) { isExplorerFiles = isFile invalidateOptionsMenu() } override fun onDestroy() { super.onDestroy() ThumbServer.stop() } companion object { @JvmStatic fun open(context: Context) { context.startActivity(Intent(context, ExplorerActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerActivityMaterial.kt ================================================ package knf.kuma.explorer import androidx.activity.addCallback import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.commons.CastUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityExplorerMaterialBinding class ExplorerActivityMaterial : GenericActivity(), OnFileStateChange { private val binding by lazy { ActivityExplorerMaterialBinding.inflate(layoutInflater) } private var adapter: ExplorerPagerAdapterMaterial? = null private var isExplorerFiles = true override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) binding.toolbar.title = "Explorador" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } if (savedInstanceState == null) ExplorerCreator.onDestroy() binding.pager.offscreenPageLimit = 2 adapter = ExplorerPagerAdapterMaterial(this, supportFragmentManager) binding.pager.adapter = adapter binding.tabs.setupWithViewPager(binding.pager) onBackPressedDispatcher.addCallback(this) { val currentFragment = adapter?.getItem(binding.pager.currentItem) as? FragmentBase if (currentFragment?.onBackPressed() != true) { isEnabled = false onBackPressedDispatcher.onBackPressed() isEnabled = true } } showRandomInterstitial(this, PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_explorer_connected, menu) if (isExplorerFiles) menu.findItem(R.id.delete_all).isVisible = false CastUtil.registerActivity(this, menu, R.id.castMenu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.delete_all -> adapter?.onRemoveAllClicked() } return super.onOptionsItemSelected(item) } override fun onChange(isFile: Boolean) { isExplorerFiles = isFile invalidateOptionsMenu() } override fun onDestroy() { super.onDestroy() ThumbServer.stop() } companion object { @JvmStatic fun open(context: Context) { context.startActivity(Intent(context, ExplorerActivityMaterial::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerChapsAdapter.kt ================================================ package knf.kuma.explorer import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import android.media.ThumbnailUtils import android.os.Build import android.provider.MediaStore.Video.Thumbnails.MINI_KIND import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.card.MaterialCardView import knf.kuma.App import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.cast.CastMedia import knf.kuma.commons.CastUtil import knf.kuma.commons.PicassoSingle import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.custom.SeenAnimeOverlay import knf.kuma.database.CacheDB import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import java.io.File import java.io.FileOutputStream import java.util.Locale class ExplorerChapsAdapter internal constructor(val fragment: Fragment, private val recyclerView: RecyclerView, val explorerObject: ExplorerObjectWrap, private val model: ExplorerFilesModel, private var clearInterface: FragmentChapters.ClearInterface?) : RecyclerView.Adapter() { private val context: Context? = fragment.context private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() private val chaptersDAO = CacheDB.INSTANCE.seenDAO() private val recordsDAO = CacheDB.INSTANCE.recordsDAO() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_chap } else { R.layout.item_chap_grid } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChapItem { return ChapItem(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: ChapItem, position: Int) { val chapObject = explorerObject.fileList[position] loadThumb(chapObject.obj, holder.imageView) val chapterNum = String.format(Locale.getDefault(), "Episodio %s", chapObject.obj.chapter) holder.seenOverlay.setSeen(chapObject.isSeen, false) holder.chapter.text = chapterNum holder.time.text = chapObject.obj.time holder.cardView.setOnClickListener { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.addChapter(SeenObject.fromDownloaded(chapObject.obj)) recordsDAO.add(RecordObject.fromDownloaded(chapObject.obj)) } chapObject.isSeen = true syncData { history() seen() } holder.seenOverlay.setSeen(true, true) if (CastUtil.get().connected()) { CastUtil.get().play(recyclerView, CastMedia.create(chapObject.obj)) } else { ServersFactory.startPlay(context, chapObject.obj.chapTitle, chapObject.obj.fileName) } } holder.cardView.setOnLongClickListener { if (!chapObject.isSeen) { fragment.lifecycleScope.launch(Dispatchers.IO){ chaptersDAO.addChapter(SeenObject.fromDownloaded(chapObject.obj)) } chapObject.isSeen = true holder.seenOverlay.setSeen(true, true) } else { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.deleteChapter(chapObject.obj.aid, chapterNum) } chapObject.isSeen = false holder.seenOverlay.setSeen(false, true) } syncData { seen() } true } holder.action.setOnClickListener { context?.let { MaterialDialog(context).safeShow { message(text = "¿Eliminar el episodio ${chapObject.obj.chapter} de ${chapObject.obj.title}?") positiveButton(text = "CONFIRMAR") { delete(chapObject.obj, holder.adapterPosition) } negativeButton(text = "CANCELAR") } } } } fun setInterface(clearInterface: FragmentChapters.ClearInterface) { this.clearInterface = clearInterface } private fun delete(obj: ExplorerObject.FileDownObj, position: Int) { if (position < 0) return doAsync { FileAccessHelper.delete(obj.fileName, true) downloadsDAO.deleteByEid(obj.eid) QueueManager.remove(obj.eid) explorerObject.fileList.removeAt(position) fragment.doOnUI { notifyItemRemoved(position) } if (explorerObject.fileList.size == 0) { model.remove(explorerObject.obj) clearInterface?.onClear() } else { model.removeOne(explorerObject.obj) } } } internal fun deleteAll() { doAsync { for ((i, obj) in explorerObject.fileList.withIndex()) { FileAccessHelper.delete(obj.obj.fileName, true) downloadsDAO.deleteByEid(obj.obj.eid) QueueManager.remove(obj.obj.eid) fragment.doOnUI { notifyItemRemoved(i) } } model.remove(explorerObject.obj) clearInterface?.onClear() } } private fun loadThumb(fileDownObj: ExplorerObject.FileDownObj, imageView: ImageView?) { val file = File(context?.cacheDir, explorerObject.obj.fileName + "_" + fileDownObj.chapter.lowercase(Locale.getDefault()) + ".png") if (file.exists()) { fileDownObj.thumb = file PicassoSingle.get().load(file).into(imageView) } else { doAsync { try { val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaMetadataRetriever().apply { setDataSource(App.context, fileDownObj.file.getFileUri()) }.frameAtTime else ThumbnailUtils.createVideoThumbnail(File(fileDownObj.file.getFileUri().path).absolutePath, MINI_KIND) if (bitmap == null) { throw IllegalStateException("Null bitmap") } else { file.createNewFile() bitmap.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(file)) fileDownObj.thumb = file fragment.doOnUI { PicassoSingle.get().load(file).into(imageView) } } } catch (e: Exception) { fragment.doOnUI { PicassoSingle.get().load(R.drawable.ic_no_thumb).fit().into(imageView) } } } } } override fun getItemCount(): Int { return try { explorerObject.fileList.size } catch (e: Exception) { 0 } } class ChapItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val seenOverlay: SeenAnimeOverlay by itemView.bind(R.id.seen) val chapter: TextView by itemView.bind(R.id.chapter) val time: TextView by itemView.bind(R.id.time) val action: ImageButton by itemView.bind(R.id.action) } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerChapsAdapterMaterial.kt ================================================ package knf.kuma.explorer import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import android.media.ThumbnailUtils import android.os.Build import android.provider.MediaStore.Video.Thumbnails.MINI_KIND import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.App import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.cast.CastMedia import knf.kuma.commons.CastUtil import knf.kuma.commons.PicassoSingle import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.custom.SeenAnimeOverlay import knf.kuma.database.CacheDB import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import java.io.File import java.io.FileOutputStream import java.util.Locale class ExplorerChapsAdapterMaterial internal constructor(val fragment: Fragment, private val recyclerView: RecyclerView, val explorerObject: ExplorerObjectWrap, private val model: ExplorerFilesModel, private var clearInterface: FragmentChaptersMaterial.ClearInterface?) : RecyclerView.Adapter() { private val context: Context? = fragment.context private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() private val chaptersDAO = CacheDB.INSTANCE.seenDAO() private val recordsDAO = CacheDB.INSTANCE.recordsDAO() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_chap_material } else { R.layout.item_chap_grid_material } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChapItem { return ChapItem(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: ChapItem, position: Int) { val chapObject = explorerObject.fileList[position] loadThumb(chapObject.obj, holder.imageView) val chapterNum = String.format(Locale.getDefault(), "Episodio %s", chapObject.obj.chapter) holder.seenOverlay.setSeen(chapObject.isSeen, false) holder.chapter.text = chapterNum holder.time.text = chapObject.obj.time holder.cardView.setOnClickListener { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.addChapter(SeenObject.fromDownloaded(chapObject.obj)) recordsDAO.add(RecordObject.fromDownloaded(chapObject.obj)) } chapObject.isSeen = true syncData { history() seen() } holder.seenOverlay.setSeen(true, true) if (CastUtil.get().connected()) { CastUtil.get().play(recyclerView, CastMedia.create(chapObject.obj)) } else { ServersFactory.startPlay(context, chapObject.obj.chapTitle, chapObject.obj.fileName) } } holder.cardView.setOnLongClickListener { if (!chapObject.isSeen) { fragment.lifecycleScope.launch(Dispatchers.IO){ chaptersDAO.addChapter(SeenObject.fromDownloaded(chapObject.obj)) } chapObject.isSeen = true holder.seenOverlay.setSeen(true, true) } else { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.deleteChapter(chapObject.obj.aid, chapterNum) } chapObject.isSeen = false holder.seenOverlay.setSeen(false, true) } syncData { seen() } true } holder.action.setOnClickListener { context?.let { MaterialDialog(context).safeShow { message(text = "¿Eliminar el episodio ${chapObject.obj.chapter} de ${chapObject.obj.title}?") positiveButton(text = "CONFIRMAR") { delete(chapObject.obj, holder.adapterPosition) } negativeButton(text = "CANCELAR") } } } } fun setInterface(clearInterface: FragmentChaptersMaterial.ClearInterface) { this.clearInterface = clearInterface } private fun delete(obj: ExplorerObject.FileDownObj, position: Int) { if (position < 0) return doAsync { FileAccessHelper.delete(obj.fileName, true) downloadsDAO.deleteByEid(obj.eid) QueueManager.remove(obj.eid) explorerObject.fileList.removeAt(position) fragment.doOnUI { notifyItemRemoved(position) } if (explorerObject.fileList.size == 0) { model.remove(explorerObject.obj) clearInterface?.onClear() } else { model.removeOne(explorerObject.obj) } } } internal fun deleteAll() { doAsync { for ((i, obj) in explorerObject.fileList.withIndex()) { FileAccessHelper.delete(obj.obj.fileName, true) downloadsDAO.deleteByEid(obj.obj.eid) QueueManager.remove(obj.obj.eid) fragment.doOnUI { notifyItemRemoved(i) } } model.remove(explorerObject.obj) clearInterface?.onClear() } } private fun loadThumb(fileDownObj: ExplorerObject.FileDownObj, imageView: ImageView?) { val file = File(context?.cacheDir, explorerObject.obj.fileName + "_" + fileDownObj.chapter.lowercase(Locale.getDefault()) + ".png") if (file.exists()) { fileDownObj.thumb = file PicassoSingle.get().load(file).into(imageView) } else { doAsync { try { val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaMetadataRetriever().apply { setDataSource(App.context, fileDownObj.file.getFileUri()) }.frameAtTime else ThumbnailUtils.createVideoThumbnail(File(fileDownObj.file.getFileUri().path).absolutePath, MINI_KIND) if (bitmap == null) { throw IllegalStateException("Null bitmap") } else { file.createNewFile() bitmap.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(file)) fileDownObj.thumb = file fragment.doOnUI { PicassoSingle.get().load(file).into(imageView) } } } catch (e: Exception) { fragment.doOnUI { PicassoSingle.get().load(R.drawable.ic_no_thumb).fit().into(imageView) } } } } } override fun getItemCount(): Int { return try { explorerObject.fileList.size } catch (e: Exception) { 0 } } class ChapItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val seenOverlay: SeenAnimeOverlay by itemView.bind(R.id.seen) val chapter: TextView by itemView.bind(R.id.chapter) val time: TextView by itemView.bind(R.id.time) val action: ImageButton by itemView.bind(R.id.action) } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerCreator.kt ================================================ package knf.kuma.explorer import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import knf.kuma.commons.doOnUIGlobal import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.ExplorerObject import org.jetbrains.anko.doAsync import java.util.Locale object ExplorerCreator { var IS_CREATED = false var IS_FILES = true var FILES_NAME: ExplorerObject? = null private val STATE_LISTENER = MutableLiveData() internal val stateListener: LiveData get() = STATE_LISTENER fun start(model: ExplorerFilesModel, listener: EmptyListener) { IS_CREATED = true doAsync { if (!FileAccessHelper.isStoragePermissionEnabled()) { //Toaster.toastLong("Permiso de almacenamiento no concedido") listener.onPermissionFailed() postState(null) IS_CREATED = false return@doAsync } postState("Iniciando busqueda") val creator = FileAccessHelper.downloadExplorerCreator if (creator.exist()) { postState("Buscando animes") val list = creator.createDirectoryList { progress, total -> postState(String.format(Locale.getDefault(), "Procesando animes %d/%d", progress, total)) } postState("Creando lista") model.setData(list) if (list.isEmpty()) { listener.onEmpty() } postState(null) } else { model.setData(emptyList()) listener.onEmpty() postState(null) } } } fun onDestroy() { IS_CREATED = false IS_FILES = true FILES_NAME = null } private fun postState(state: String?) { doOnUIGlobal { STATE_LISTENER.value = state } } interface EmptyListener { fun onEmpty() fun onPermissionFailed() } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerFilesAdapter.kt ================================================ package knf.kuma.explorer import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.commons.notSameContent import knf.kuma.pojos.ExplorerObject import java.util.Locale class ExplorerFilesAdapter internal constructor(private val fragment: Fragment, private var listener: FragmentFiles.SelectedListener?) : RecyclerView.Adapter() { private var list: MutableList = ArrayList() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_explorer } else { R.layout.item_explorer_grid } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileItem { return FileItem(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } fun setListener(listener: FragmentFiles.SelectedListener) { this.listener = listener } override fun onBindViewHolder(holder: FileItem, position: Int) { val explorerObject = list[position] holder.imageView.load(explorerObject.img) holder.title.text = explorerObject.name holder.chapter.text = String.format(Locale.getDefault(), if (explorerObject.count == 1) "%d archivo" else "%d archivos", explorerObject.count) holder.cardView.setOnClickListener { listener?.onSelected(explorerObject) } holder.cardView.setOnLongClickListener { ActivityAnime.open(fragment, explorerObject, holder.imageView) true } } override fun getItemCount(): Int { return list.size } fun update(list: MutableList) { if (this.list notSameContent list) { this.list = list notifyDataSetChanged() } } class FileItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val chapter: TextView by itemView.bind(R.id.chapter) } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerFilesAdapterMaterial.kt ================================================ package knf.kuma.explorer import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.pojos.ExplorerObject import java.util.Locale class ExplorerFilesAdapterMaterial internal constructor(private val fragment: Fragment, private var listener: FragmentFilesMaterial.SelectedListener?) : ListAdapter(ExplorerObjectDiff()) { private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_explorer_material } else { R.layout.item_explorer_grid_material } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileItem { return FileItem(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } fun setListener(listener: FragmentFilesMaterial.SelectedListener) { this.listener = listener } override fun onBindViewHolder(holder: FileItem, position: Int) { val explorerObject = getItem(position) holder.imageView.load(explorerObject.img) holder.title.text = explorerObject.name holder.chapter.text = String.format(Locale.getDefault(), if (explorerObject.count == 1) "%d archivo" else "%d archivos", explorerObject.count) holder.cardView.setOnClickListener { listener?.onSelected(explorerObject) } holder.cardView.setOnLongClickListener { ActivityAnimeMaterial.open(fragment, explorerObject, holder.imageView) true } } fun update(list: List) { submitList(list) } class FileItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val chapter: TextView by itemView.bind(R.id.chapter) } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerFilesModel.kt ================================================ package knf.kuma.explorer import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import knf.kuma.pojos.ExplorerObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ExplorerFilesModel : ViewModel() { val localFilesData = MutableLiveData>() private var localList = mutableListOf() fun setData(list: List) { viewModelScope.launch { localFilesData.value = list localList = list.toMutableList() } } fun remove(item: ExplorerObject) { viewModelScope.launch(Dispatchers.IO) { localList.filter { it.key != item.key }.let { withContext(Dispatchers.Main) { localFilesData.value = it } } } } fun removeOne(item: ExplorerObject) { viewModelScope.launch(Dispatchers.IO) { val found = localList.find { it.key == item.key } val index = localList.indexOf(found) if (index >= 0 && found != null) { val new = ExplorerObject(found).apply { count -= 1 } localList.removeAt(index) localList.add(index, new) withContext(Dispatchers.Main) { localFilesData.value = localList } } } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerObjectDiff.kt ================================================ package knf.kuma.explorer import androidx.recyclerview.widget.DiffUtil import knf.kuma.pojos.ExplorerObject class ExplorerObjectDiff: DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ExplorerObject, newItem: ExplorerObject): Boolean = oldItem.key == newItem.key override fun areContentsTheSame(oldItem: ExplorerObject, newItem: ExplorerObject): Boolean = oldItem.chapters.size == newItem.chapters.size && oldItem.count == newItem.count } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerObjectWrap.kt ================================================ package knf.kuma.explorer import knf.kuma.database.CacheDB import knf.kuma.pojos.ExplorerObject import java.util.Locale class ExplorerObjectWrap(val obj: ExplorerObject){ val fileList = obj.chapters.map { FileDownWrap(it) }.toMutableList() } class FileDownWrap(val obj: ExplorerObject.FileDownObj) { var isSeen = CacheDB.INSTANCE.seenDAO().chapterIsSeen(obj.aid, String.format(Locale.getDefault(), "Episodio %s", obj.chapter)) } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerPagerAdapter.kt ================================================ package knf.kuma.explorer import android.content.Context import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.viewpager.widget.PagerAdapter class ExplorerPagerAdapter(context: Context, private val fragmentManager: FragmentManager) : PagerAdapter() { private val fragments: Array = arrayOfNulls(2) private val stateChange: OnFileStateChange? = context as? OnFileStateChange override fun instantiateItem(container: ViewGroup, position: Int): Any { val fragment = getItem(position) try { fragment?.let { val trans = fragmentManager.beginTransaction() trans.add(container.id, fragment, "fragment:$position") trans.commit() } } catch (e: Exception) { e.printStackTrace() } return fragment ?: Any() } override fun destroyItem(container: ViewGroup, position: Int, any: Any) { val fragment = fragments[position] fragment?.let { val trans = fragmentManager.beginTransaction() trans.remove(fragment) trans.commit() } fragments[position] = null } override fun getCount(): Int { return fragments.size } override fun isViewFromObject(view: View, any: Any): Boolean { return (any as? Fragment)?.view === view } override fun getPageTitle(position: Int): CharSequence? { return when (position) { 0 -> "Archivos" 1 -> "Descargas" else -> "Archivos" } } fun getItem(position: Int): Fragment? { if (fragments[position] == null) { fragments[position] = createFragment(position) if (position == 0) (fragments[position] as? FragmentFilesRoot)?.setStateChange(stateChange) } return fragments[position] } private fun createFragment(position: Int): Fragment { return when (position) { 0 -> FragmentFilesRoot.get() 1 -> FragmentDownloads.get() else -> FragmentFilesRoot.get() } } internal fun onRemoveAllClicked() { try { (fragments[0] as? FragmentFilesRoot)?.onRemoveAll() } catch (e: Exception) { e.printStackTrace() } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ExplorerPagerAdapterMaterial.kt ================================================ package knf.kuma.explorer import android.content.Context import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.viewpager.widget.PagerAdapter class ExplorerPagerAdapterMaterial(context: Context, private val fragmentManager: FragmentManager) : PagerAdapter() { private val fragments: Array = arrayOfNulls(2) private val stateChange: OnFileStateChange? = context as? OnFileStateChange override fun instantiateItem(container: ViewGroup, position: Int): Any { val fragment = getItem(position) try { fragment?.let { val trans = fragmentManager.beginTransaction() trans.add(container.id, fragment, "fragment:$position") trans.commit() } } catch (e: Exception) { e.printStackTrace() } return fragment ?: Any() } override fun destroyItem(container: ViewGroup, position: Int, any: Any) { val fragment = fragments[position] fragment?.let { val trans = fragmentManager.beginTransaction() trans.remove(fragment) trans.commit() } fragments[position] = null } override fun getCount(): Int { return fragments.size } override fun isViewFromObject(view: View, any: Any): Boolean { return (any as? Fragment)?.view === view } override fun getPageTitle(position: Int): CharSequence? { return when (position) { 0 -> "Archivos" 1 -> "Descargas" else -> "Archivos" } } fun getItem(position: Int): Fragment? { if (fragments[position] == null) { fragments[position] = createFragment(position) if (position == 0) (fragments[position] as? FragmentFilesRootMaterial)?.setStateChange(stateChange) } return fragments[position] } private fun createFragment(position: Int): Fragment { return when (position) { 1 -> FragmentDownloadsMaterial.get() else -> FragmentFilesRootMaterial.get() } } internal fun onRemoveAllClicked() { try { (fragments[0] as? FragmentFilesRootMaterial)?.onRemoveAll() } catch (e: Exception) { e.printStackTrace() } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentBase.kt ================================================ package knf.kuma.explorer import androidx.fragment.app.Fragment abstract class FragmentBase : Fragment() { abstract fun onBackPressed(): Boolean } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentChapters.kt ================================================ package knf.kuma.explorer import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ProgressBar import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.floatingactionbutton.FloatingActionButton import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.backup.firestore.syncData import knf.kuma.commons.CastUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashSuspend import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.RecordObject import knf.kuma.queue.QueueManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick import xdroid.toaster.Toaster class FragmentChapters : Fragment() { private val model: ExplorerFilesModel by activityViewModels() lateinit var recyclerView: RecyclerView lateinit var progressBar: ProgressBar lateinit var fab: FloatingActionButton internal var adapter: ExplorerChapsAdapter? = null private var clearInterface: ClearInterface? = null private var isFirst = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_explorer_chaps } else { R.layout.recycler_explorer_chaps_grid } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) recyclerView.verifyManager(170) progressBar = view.find(R.id.progress) fab = view.find(R.id.fab) view.find(R.id.adContainer).implBanner(AdsType.EXPLORER_BANNER, true) return view } private fun playAll(list: List) { lifecycleScope.launch(Dispatchers.Main){ noCrashSuspend { withContext(Dispatchers.IO) { CacheDB.INSTANCE.recordsDAO().add(RecordObject.fromDownloaded(list.last())) syncData { history() } QueueManager.startQueueDownloaded(context, list) } adapter?.apply { explorerObject.fileList.forEach { it.isSeen = true } notifyDataSetChanged() } } } } @SuppressLint("RestrictedApi") fun setObject(explorerObject: ExplorerObject?) { noCrash { fab.internalSetVisibility(View.INVISIBLE, true) fab.hide() } clear() explorerObject?.let { it.getLiveData(context) .observe(this@FragmentChapters, Observer { fileDownObjs -> if (fileDownObjs.isEmpty()) { Toaster.toast("Directorio vacio") lifecycleScope.launch(Dispatchers.IO) { model.remove(explorerObject) launch(Dispatchers.Main) { clearInterface?.onClear() } } } else { explorerObject.chapters = fileDownObjs as MutableList lifecycleScope.launch(Dispatchers.Main) { progressBar.visibility = View.GONE adapter = ExplorerChapsAdapter(this@FragmentChapters, recyclerView, withContext(Dispatchers.IO) { ExplorerObjectWrap(explorerObject) }, model, clearInterface) recyclerView.adapter = adapter if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } if (!CastUtil.get().connected()) { fab.show() fab.onClick { playAll(fileDownObjs) } } } } }) } } internal fun deleteAll() { adapter?.deleteAll() } private fun clear() { isFirst = true adapter = null doOnUI { progressBar.visibility = View.VISIBLE recyclerView.adapter = null } } fun setInterface(clearInterface: ClearInterface) { this.clearInterface = clearInterface adapter?.setInterface(clearInterface) } interface ClearInterface { fun onClear() } companion object { const val TAG = "Chapters" operator fun get(clearInterface: ClearInterface): FragmentChapters { val fragmentChapters = FragmentChapters() fragmentChapters.setInterface(clearInterface) return fragmentChapters } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentChaptersMaterial.kt ================================================ package knf.kuma.explorer import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ProgressBar import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.floatingactionbutton.FloatingActionButton import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.backup.firestore.syncData import knf.kuma.commons.CastUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashSuspend import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.RecordObject import knf.kuma.queue.QueueManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick import xdroid.toaster.Toaster class FragmentChaptersMaterial : Fragment() { private val model: ExplorerFilesModel by activityViewModels() lateinit var recyclerView: RecyclerView lateinit var progressBar: ProgressBar lateinit var fab: FloatingActionButton internal var adapter: ExplorerChapsAdapterMaterial? = null private var clearInterface: ClearInterface? = null private var isFirst = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_explorer_chaps } else { R.layout.recycler_explorer_chaps_grid } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) recyclerView.verifyManager(170) progressBar = view.find(R.id.progress) fab = view.find(R.id.fab) view.find(R.id.adContainer).implBanner(AdsType.EXPLORER_BANNER, true) return view } private fun playAll(list: List) { lifecycleScope.launch(Dispatchers.Main){ noCrashSuspend { withContext(Dispatchers.IO) { CacheDB.INSTANCE.recordsDAO().add(RecordObject.fromDownloaded(list.last())) syncData { history() } QueueManager.startQueueDownloaded(context, list) } adapter?.apply { explorerObject.fileList.forEach { it.isSeen = true } notifyDataSetChanged() } } } } @SuppressLint("RestrictedApi") fun setObject(explorerObject: ExplorerObject?) { noCrash { fab.internalSetVisibility(View.INVISIBLE, true) fab.hide() } clear() explorerObject?.let { it.getLiveData(context) .observe(this@FragmentChaptersMaterial, Observer { fileDownObjs -> if (fileDownObjs.isEmpty()) { Toaster.toast("Directorio vacio") lifecycleScope.launch(Dispatchers.IO) { model.remove(explorerObject) launch(Dispatchers.Main) { clearInterface?.onClear() } } } else { explorerObject.chapters = fileDownObjs as MutableList lifecycleScope.launch(Dispatchers.Main) { progressBar.visibility = View.GONE adapter = ExplorerChapsAdapterMaterial(this@FragmentChaptersMaterial, recyclerView, withContext(Dispatchers.IO) { ExplorerObjectWrap(explorerObject) }, model, clearInterface) recyclerView.adapter = adapter if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } if (!CastUtil.get().connected()) { fab.show() fab.onClick { playAll(fileDownObjs) } } } } }) } } internal fun deleteAll() { adapter?.deleteAll() } private fun clear() { isFirst = true adapter = null doOnUI { progressBar.visibility = View.VISIBLE recyclerView.adapter = null } } fun setInterface(clearInterface: ClearInterface) { this.clearInterface = clearInterface adapter?.setInterface(clearInterface) } interface ClearInterface { fun onClear() } companion object { const val TAG = "Chapters" operator fun get(clearInterface: ClearInterface): FragmentChaptersMaterial { val fragmentChapters = FragmentChaptersMaterial() fragmentChapters.setInterface(clearInterface) return fragmentChapters } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentDownloads.kt ================================================ package knf.kuma.explorer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.google.android.material.snackbar.Snackbar import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.doOnUI import knf.kuma.commons.safeDismiss import knf.kuma.commons.safeShow import knf.kuma.commons.showSnackbar import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerDownloadingBinding import knf.kuma.download.DownloadManagerCentral import knf.kuma.pojos.DownloadObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class FragmentDownloads : FragmentBase() { private var isFirst = true private var adapter: DownloadingAdapter? = null private lateinit var binding: RecyclerDownloadingBinding override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = RecyclerDownloadingBinding.bind(view) binding.adContainer.implBanner(AdsType.EXPLORER_BANNER, true) CacheDB.INSTANCE.downloadsDAO().active.observe(viewLifecycleOwner) { downloadObjects -> binding.progress.visibility = View.GONE binding.error.visibility = if (downloadObjects.isEmpty()) View.VISIBLE else View.GONE binding.clear.visibility = if (downloadObjects.isEmpty()) View.GONE else View.VISIBLE if (isFirst || downloadObjects.isEmpty() || binding.recycler.adapter != null && downloadObjects.size > binding.recycler.adapter?.itemCount ?: 0) { isFirst = false binding.recycler.adapter = DownloadingAdapter(this@FragmentDownloads, downloadObjects as MutableList).also { adapter = it } } } binding.clear.onClick { activity?.let { MaterialDialog(it).safeShow { lifecycleOwner() message(text = "¿Desea limpiar todas las descargas en la lista?") positiveButton(text = "limpiar") { onRemoveAll() } negativeButton(text = "Cancelar") } } } } private fun onRemoveAll() { binding.clear.visibility = View.GONE val snackbar = binding.recycler.showSnackbar("Limpiando lista...", Snackbar.LENGTH_INDEFINITE) doAsync { DownloadManagerCentral.cancelAll() doOnUI { snackbar.safeDismiss() } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.recycler_downloading, container, false) } override fun onBackPressed(): Boolean { return false } companion object { fun get(): FragmentDownloads { return FragmentDownloads() } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentDownloadsMaterial.kt ================================================ package knf.kuma.explorer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.Observer import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.google.android.material.snackbar.Snackbar import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.doOnUI import knf.kuma.commons.safeDismiss import knf.kuma.commons.safeShow import knf.kuma.commons.showSnackbar import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerDownloadingBinding import knf.kuma.download.DownloadManagerCentral import knf.kuma.pojos.DownloadObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class FragmentDownloadsMaterial : FragmentBase() { private var isFirst = true private var adapter: DownloadingAdapterMaterial? = null private lateinit var binding: RecyclerDownloadingBinding override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = RecyclerDownloadingBinding.bind(view) binding.adContainer.implBanner(AdsType.EXPLORER_BANNER, true) CacheDB.INSTANCE.downloadsDAO().active.observe(viewLifecycleOwner, Observer { downloadObjects -> binding.progress.visibility = View.GONE binding.error.visibility = if (downloadObjects.isEmpty()) View.VISIBLE else View.GONE binding.clear.visibility = if (downloadObjects.isEmpty()) View.GONE else View.VISIBLE if (isFirst || downloadObjects.isEmpty() || binding.recycler.adapter != null && downloadObjects.size > binding.recycler.adapter?.itemCount ?: 0) { isFirst = false binding.recycler.adapter = DownloadingAdapterMaterial(this@FragmentDownloadsMaterial, downloadObjects as MutableList).also { adapter = it } } }) binding.clear.onClick { activity?.let { MaterialDialog(it).safeShow { lifecycleOwner() message(text = "¿Desea limpiar todas las descargas en la lista?") positiveButton(text = "limpiar") { onRemoveAll() } negativeButton(text = "Cancelar") } } } } private fun onRemoveAll() { binding.clear.visibility = View.GONE val snackbar = binding.recycler.showSnackbar("Limpiando lista...", Snackbar.LENGTH_INDEFINITE) doAsync { DownloadManagerCentral.cancelAll() doOnUI { snackbar.safeDismiss() } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.recycler_downloading, container, false) } override fun onBackPressed(): Boolean { return false } companion object { fun get(): FragmentDownloadsMaterial { return FragmentDownloadsMaterial() } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentFiles.kt ================================================ package knf.kuma.explorer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.verifyManager import knf.kuma.pojos.ExplorerObject import org.jetbrains.anko.find class FragmentFiles : Fragment() { private val model: ExplorerFilesModel by activityViewModels() lateinit var recyclerView: RecyclerView lateinit var error: View lateinit var progressBar: ProgressBar lateinit var state: TextView private var listener: SelectedListener? = null private var adapter: ExplorerFilesAdapter? = null private var isFist = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_explorer } else { R.layout.recycler_explorer_grid } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) model.localFilesData.observe(viewLifecycleOwner, { explorerObjects -> adapter?.update(explorerObjects.toMutableList()) if (explorerObjects.isNotEmpty()) { progressBar.visibility = View.GONE state.visibility = View.GONE if (isFist) { isFist = false recyclerView.scheduleLayoutAnimation() } } }) ExplorerCreator.stateListener.observe(viewLifecycleOwner, Observer { s -> state.text = s state.visibility = if (s == null) View.GONE else View.VISIBLE }) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) recyclerView.verifyManager() error = view.find(R.id.error) progressBar = view.find(R.id.progress) state = view.find(R.id.state) view.find(R.id.adContainer).implBanner(AdsType.EXPLORER_BANNER, true) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) adapter = ExplorerFilesAdapter(this, listener) recyclerView.adapter = adapter } fun onEmpty() { doOnUI { progressBar.visibility = View.GONE error.visibility = View.VISIBLE state.visibility = View.GONE } } fun setListener(listener: SelectedListener) { this.listener = listener adapter?.setListener(listener) } interface SelectedListener { fun onSelected(explorerObject: ExplorerObject) } companion object { const val TAG = "Files" operator fun get(listener: SelectedListener): FragmentFiles { val fragmentFiles = FragmentFiles() fragmentFiles.setListener(listener) return fragmentFiles } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentFilesMaterial.kt ================================================ package knf.kuma.explorer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.verifyManager import knf.kuma.pojos.ExplorerObject import org.jetbrains.anko.find class FragmentFilesMaterial : Fragment() { private val model: ExplorerFilesModel by activityViewModels() lateinit var recyclerView: RecyclerView lateinit var error: View lateinit var progressBar: ProgressBar lateinit var state: TextView private var listener: SelectedListener? = null private var adapter: ExplorerFilesAdapterMaterial? = null private var isFist = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_explorer } else { R.layout.recycler_explorer_grid } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) model.localFilesData.observe(viewLifecycleOwner, { explorerObjects -> adapter?.update(explorerObjects) if (explorerObjects.isNotEmpty()) { progressBar.visibility = View.GONE state.visibility = View.GONE if (isFist) { isFist = false recyclerView.scheduleLayoutAnimation() } } }) ExplorerCreator.stateListener.observe(viewLifecycleOwner, { s -> state.text = s state.visibility = if (s == null) View.GONE else View.VISIBLE }) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) recyclerView.verifyManager() error = view.find(R.id.error) progressBar = view.find(R.id.progress) state = view.find(R.id.state) view.find(R.id.adContainer).implBanner(AdsType.EXPLORER_BANNER, true) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) adapter = ExplorerFilesAdapterMaterial(this, listener) recyclerView.adapter = adapter } fun onEmpty() { doOnUI { progressBar.visibility = View.GONE error.visibility = View.VISIBLE state.visibility = View.GONE } } fun setListener(listener: SelectedListener) { this.listener = listener adapter?.setListener(listener) } interface SelectedListener { fun onSelected(explorerObject: ExplorerObject) } companion object { const val TAG = "Files" operator fun get(listener: SelectedListener): FragmentFilesMaterial { val fragmentFiles = FragmentFilesMaterial() fragmentFiles.setListener(listener) return fragmentFiles } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentFilesRoot.kt ================================================ package knf.kuma.explorer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.commons.safeShow import knf.kuma.pojos.ExplorerObject import xdroid.toaster.Toaster class FragmentFilesRoot : FragmentBase(), FragmentFiles.SelectedListener, FragmentChapters.ClearInterface, FragmentPermission.PermissionListener, ExplorerCreator.EmptyListener { private val model: ExplorerFilesModel by activityViewModels() private var files: FragmentFiles = FragmentFiles[this] private val chapters: FragmentChapters = FragmentChapters[this] private val permissions: FragmentPermission = FragmentPermission[this] private var isFiles = true private var name: String? = null private var stateChange: OnFileStateChange? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_explorer_files, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val transaction = childFragmentManager.beginTransaction() if (!files.isAdded) transaction.add(R.id.root, files, FragmentFiles.TAG) if (!chapters.isAdded) transaction.add(R.id.root, chapters, FragmentChapters.TAG) if (!permissions.isAdded) transaction.add(R.id.root, permissions, FragmentPermission.TAG) transaction.commit() super.onViewCreated(view, savedInstanceState) } private fun setFragment(isFiles: Boolean, explorerObject: ExplorerObject?) { stateChange?.onChange(isFiles) this.isFiles = isFiles this.name = explorerObject?.name ExplorerCreator.IS_FILES = isFiles ExplorerCreator.FILES_NAME = explorerObject val transaction = childFragmentManager.beginTransaction() transaction.hide(permissions) if (isFiles) { transaction.hide(chapters) transaction.show(files) } else { chapters.setObject(explorerObject) transaction.hide(files) transaction.show(chapters) } transaction.setCustomAnimations(R.anim.fadein, R.anim.fadeout) transaction.commit() } private fun showPermissionScreen() { val transaction = childFragmentManager.beginTransaction() transaction.hide(files) transaction.hide(chapters) transaction.show(permissions) transaction.commit() } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (savedInstanceState != null) { this.isFiles = savedInstanceState.getBoolean("isFiles", true) this.name = savedInstanceState.getString("name") } setFragment(ExplorerCreator.IS_FILES, ExplorerCreator.FILES_NAME) if (!ExplorerCreator.IS_CREATED) ExplorerCreator.start(model, this) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean("isFiles", isFiles) outState.putString("name", name) } internal fun setStateChange(stateChange: OnFileStateChange?) { this.stateChange = stateChange } internal fun onRemoveAll() { if (name != null) activity?.let { MaterialDialog(it).safeShow { message(text = "¿Eliminar todos los capitulos de $name?") positiveButton(text = "Eliminar") { chapters.deleteAll() } negativeButton(text = "Cancelar") } } else Toaster.toast("Error al borrar episodios") } override fun onSelected(explorerObject: ExplorerObject) { setFragment(false, explorerObject) } override fun onClear() { setFragment(true, null) } override fun onEmpty() { files.onEmpty() } override fun onPermissionFailed() { showPermissionScreen() } override fun onPermission() { setFragment(ExplorerCreator.IS_FILES, ExplorerCreator.FILES_NAME) if (!ExplorerCreator.IS_CREATED) ExplorerCreator.start(model, this) } override fun onBackPressed(): Boolean { return if (isFiles) { false } else { setFragment(true, null) true } } companion object { fun get(): FragmentFilesRoot { return FragmentFilesRoot() } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentFilesRootMaterial.kt ================================================ package knf.kuma.explorer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.commons.safeShow import knf.kuma.pojos.ExplorerObject import xdroid.toaster.Toaster class FragmentFilesRootMaterial : FragmentBase(), FragmentFilesMaterial.SelectedListener, FragmentChaptersMaterial.ClearInterface, ExplorerCreator.EmptyListener, FragmentPermission.PermissionListener { private val model: ExplorerFilesModel by activityViewModels() private var files: FragmentFilesMaterial = FragmentFilesMaterial[this] private val chapters: FragmentChaptersMaterial = FragmentChaptersMaterial[this] private val permissions: FragmentPermission = FragmentPermission[this] private var isFiles = true private var name: String? = null private var stateChange: OnFileStateChange? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_explorer_files, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val transaction = childFragmentManager.beginTransaction() if (!files.isAdded) transaction.add(R.id.root, files, FragmentFiles.TAG) if (!chapters.isAdded) transaction.add(R.id.root, chapters, FragmentChapters.TAG) if (!permissions.isAdded) transaction.add(R.id.root, permissions, FragmentPermission.TAG) transaction.commit() super.onViewCreated(view, savedInstanceState) } private fun setFragment(isFiles: Boolean, explorerObject: ExplorerObject?) { stateChange?.onChange(isFiles) this.isFiles = isFiles this.name = explorerObject?.name ExplorerCreator.IS_FILES = isFiles ExplorerCreator.FILES_NAME = explorerObject val transaction = childFragmentManager.beginTransaction() transaction.hide(permissions) if (isFiles) { transaction.hide(chapters) transaction.show(files) } else { chapters.setObject(explorerObject) transaction.hide(files) transaction.show(chapters) } transaction.setCustomAnimations(R.anim.fadein, R.anim.fadeout) transaction.commit() } private fun showPermissionScreen() { val transaction = childFragmentManager.beginTransaction() transaction.hide(files) transaction.hide(chapters) transaction.show(permissions) transaction.commit() } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (savedInstanceState != null) { this.isFiles = savedInstanceState.getBoolean("isFiles", true) this.name = savedInstanceState.getString("name") } setFragment(ExplorerCreator.IS_FILES, ExplorerCreator.FILES_NAME) if (!ExplorerCreator.IS_CREATED) ExplorerCreator.start(model, this) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean("isFiles", isFiles) outState.putString("name", name) } internal fun setStateChange(stateChange: OnFileStateChange?) { this.stateChange = stateChange } internal fun onRemoveAll() { if (name != null) activity?.let { MaterialDialog(it).safeShow { message(text = "¿Eliminar todos los capitulos de $name?") positiveButton(text = "Eliminar") { chapters.deleteAll() } negativeButton(text = "Cancelar") } } else Toaster.toast("Error al borrar episodios") } override fun onSelected(explorerObject: ExplorerObject) { setFragment(false, explorerObject) } override fun onClear() { setFragment(true, null) } override fun onEmpty() { files.onEmpty() } override fun onPermissionFailed() { showPermissionScreen() } override fun onPermission() { setFragment(ExplorerCreator.IS_FILES, ExplorerCreator.FILES_NAME) if (!ExplorerCreator.IS_CREATED) ExplorerCreator.start(model, this) } override fun onBackPressed(): Boolean { return if (isFiles) { false } else { setFragment(true, null) true } } companion object { fun get(): FragmentFilesRootMaterial { return FragmentFilesRootMaterial() } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/FragmentPermission.kt ================================================ package knf.kuma.explorer import android.Manifest import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButton import knf.kuma.R import knf.kuma.commons.isMIUI import knf.kuma.download.FileAccessHelper import kotlinx.coroutines.launch import org.jetbrains.anko.find import xdroid.toaster.Toaster class FragmentPermission : Fragment() { private lateinit var listener: PermissionListener private val permissionContract = registerForActivityResult(ActivityResultContracts.RequestPermission()) { if (it) { listener.onPermission() } else { Toast.makeText(requireContext(), "Permiso denegado", Toast.LENGTH_SHORT).show() } } private val treeChooser = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { val validation = FileAccessHelper.isUriValid(it) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") } else { listener.onPermission() } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_explorer_permission_pending, container, false) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.find(R.id.permission).apply { setOnClickListener { lifecycleScope.launch { if (!FileAccessHelper.isStoragePermissionEnabledAsync()) { when { Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> { permissionContract.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } else -> { try { treeChooser.launch(null) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Toaster.toastLong("Por favor selecciona un directorio para las descargas") else Toaster.toastLong("Por favor selecciona la raiz del almacenamiento") } catch (e: Exception) { e.printStackTrace() if (isMIUI) { permissionContract.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } } } } } } } } } fun setListener(listener: PermissionListener) { this.listener = listener } interface PermissionListener { fun onPermission() } companion object { const val TAG = "Permission" operator fun get(listener: PermissionListener): FragmentPermission { val fragmentFiles = FragmentPermission() fragmentFiles.setListener(listener) return fragmentFiles } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/OnFileStateChange.kt ================================================ package knf.kuma.explorer interface OnFileStateChange { fun onChange(isFile: Boolean) } ================================================ FILE: app/src/main/java/knf/kuma/explorer/ThumbServer.kt ================================================ package knf.kuma.explorer import fi.iki.elonen.NanoHTTPD import knf.kuma.commons.Network import java.io.File import java.io.FileInputStream object ThumbServer { private var SERVERINSTANCE: Server? = null fun loadFile(file: File): String? { return try { //stop() //SERVERINSTANCE = Server(file) setFile(file) "http://${Network.ipAddress}:4691" } catch (e: Exception) { e.printStackTrace() null } } private fun setFile(file: File) { if (SERVERINSTANCE == null) SERVERINSTANCE = Server(file) else SERVERINSTANCE?.loadedFile = file } fun stop() { if (SERVERINSTANCE?.isAlive == true) { SERVERINSTANCE?.stop() SERVERINSTANCE = null } } private class Server(var loadedFile: File) : NanoHTTPD(4691) { init { start(SOCKET_READ_TIMEOUT, false) } override fun serve(session: IHTTPSession?): Response { return newFixedLengthResponse(Response.Status.OK, "image/png", FileInputStream(loadedFile), loadedFile.length()) } } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/creator/Creator.kt ================================================ package knf.kuma.explorer.creator import knf.kuma.pojos.ExplorerObject interface Creator { fun exist(): Boolean fun createLinksList(): List fun createDirectoryList(progressCallback: (Int, Int) -> Unit): List } ================================================ FILE: app/src/main/java/knf/kuma/explorer/creator/DocumentFileCreator.kt ================================================ package knf.kuma.explorer.creator import androidx.documentfile.provider.DocumentFile import knf.kuma.database.CacheDB import knf.kuma.pojos.ExplorerObject class DocumentFileCreator(private val rootDF: DocumentFile?) : Creator { override fun exist(): Boolean = rootDF?.exists() ?: false override fun createLinksList(): List { rootDF ?: return emptyList() return rootDF.listFiles().filter { it.isDirectory }.mapNotNull { "https://www3.animeflv.net/anime/${it.name}" } } override fun createDirectoryList(progressCallback: (Int, Int) -> Unit): List { rootDF ?: return emptyList() val directories = rootDF.listFiles().filter { it.isDirectory } val list = mutableListOf() var progress = 0 CacheDB.INSTANCE.animeDAO().getAllByFile(directories.mapNotNull { it.name }.toMutableList()).forEach { try { progress++ progressCallback(progress, directories.size) list.add(ExplorerObject(it)) } catch (e: IllegalStateException) { e.printStackTrace() } } return list } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/creator/SimpleFileCreator.kt ================================================ package knf.kuma.explorer.creator import knf.kuma.database.CacheDB import knf.kuma.pojos.ExplorerObject import java.io.File import java.io.FileFilter class SimpleFileCreator(val base: File) : Creator { override fun exist(): Boolean = base.exists() override fun createLinksList(): List { return if (base.exists()) base.listFiles(FileFilter { it.isDirectory })?.map { "https://www3.animeflv.net/anime/${it.name}" } ?: emptyList() else emptyList() } override fun createDirectoryList(progressCallback: (Int, Int) -> Unit): List { return if (base.exists()) { val list = mutableListOf() val files = base.listFiles(FileFilter { it.isDirectory }) if (files != null) { var progress = 0 for (animeObject in CacheDB.INSTANCE.animeDAO().getAllByFile(files.map { it.name }.toMutableList())) try { progress++ progressCallback(progress, files.size) list.add(ExplorerObject(animeObject)) } catch (e: IllegalStateException) { e.printStackTrace() } } list } else emptyList() } } ================================================ FILE: app/src/main/java/knf/kuma/explorer/creator/SubFile.kt ================================================ package knf.kuma.explorer.creator import android.net.Uri data class SubFile(val name: String, private val uri: String) { fun getFileUri(): Uri = Uri.parse(uri) } ================================================ FILE: app/src/main/java/knf/kuma/faq/FaqActivity.kt ================================================ package knf.kuma.faq import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItems import knf.kuma.R import knf.kuma.commons.EAHelper import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.databinding.RecyclerFaqBinding import org.jetbrains.anko.toast class FaqActivity : GenericActivity() { private val binding by lazy { RecyclerFaqBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.title = "FAQ" binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } binding.recycler.layoutManager = VariantLinearLayoutManager(this) binding.recycler.adapter = FaqAdapter(createFAQList()) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_bug, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { MaterialDialog(this).safeShow { title(text = "Reportar problema") listItems(items = listOf("Telegram", "Facebook", "Email")) { _, index, _ -> when (index) { 0 -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/unbarredstream"))) 1 -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/ukikuapp/"))) 2 -> { val intent = Intent(Intent.ACTION_VIEW, Uri.parse("mailto:?subject=Problema con UKIKU&to=jordyamc@hotmail.com")) val chooser = Intent.createChooser(intent, "Enviar reporte") if (intent.resolveActivity(packageManager) != null) { startActivity(chooser) } else toast("No se encontraron clientes de email") } } } } return super.onOptionsItemSelected(item) } private fun createFAQList(): List = listOf( FaqItem("¿Dónde está el servidor natsuki?", "Los servidores vienen de Animeflv, la app no tiene el control de los que aparecen, tan solo extrae los que están disponibles"), FaqItem("¿Por qué no me funcionan los servidores?", "Los servidores vienen de Animeflv, son ellos los encargados de re subir los enlaces caídos, la app no puede hacer nada"), FaqItem("¿Para que funcionan las loli-coins?", "Sirven para comprar los pasos del easter egg y revelar logros ocultos"), FaqItem("¿Por qué me dice error 403?", "Solo presiona la barra roja que aparece"), FaqItem("¿Por qué no está este anime en la app?", "La app utiliza Animeflv para obtener la lista de animes, si no te aparece es porque, o no esta en Animeflv o no estas escribiendo el nombre correctamente"), FaqItem("¿Puedo pedir animes?", "No, la app utiliza Animeflv, no subimos los animes"), FaqItem("¿Por qué no me aparece el botón de añadir a favoritos?", "Es un error muuuuuuy raro, nunca se pudo encontrar una solución, en ese caso puedes añadir y quitar animes a favoritos haciendo click largo en la imagen del anime"), FaqItem("¿Se puede hacer Cast?", "Si, la app soporta transmisión por CAST, aparecerá un icono en la parte superior"), FaqItem("¿Por qué no puedo hacer Cast con capítulos Online?", "Si tu TV es Samsung entonces es imposible, la TV bloquea los intentos por usar CAST, en ese caso puedes descargar el capítulo y hacer CAST local"), FaqItem("¿Que significa Cloudflare activado?", "Es una protección de la página de Animeflv, la app debería poder pasar esa protección automáticamente"), FaqItem("¿Puedo adelantar el OP y ED?", "Si, en el reproductor interno hay un botón para saltar 1:30m (Lo que por lo regular dura un OP o ED)"), FaqItem("¿Se le puede hacer PiP (ventana flotante) a un capítulo?", "Si, pero solo es compatible con Android 8.1 o superior"), FaqItem("¿Por qué me va muy lenta las descargas?", "Esto podría ser por varios factores, tu internet, la velocidad de escritura (mientras más ocupado este el dispositivo más tardará), el servidor que estés usando para la descarga"), FaqItem("¿Hay forma de ver una serie de corrido (ver el siguiente cap sin regresar a los caps)?", "Si, puedes añadir los capítulos a la cola y verlos todos"), FaqItem("¿Por qué se me reinicia el capítulo cuando contesto mensajes (salir y entrar a la app)?", "Algunos dispositivos no muy potentes necesitan \"matar\" las aplicaciones en segundo plano para ahorrar memoria, esto sumado a que no todos los servidores soportan el adelantar videos"), FaqItem("¿Puedo añadir un sonido personalizado a las notificaciones?", "Si, hay una opción en configuraciones para ello"), FaqItem("¿Qué pasará si animeflv deja de existir?", "Se considerara cambiar de página o crear una app desde 0"), FaqItem("¿Puedo guardar mis animes en \"favoritos\" y \"siguiendo\"?", "Si, las dos secciones son independientes"), FaqItem("¿Cómo puedo ayudar a la app?", "Puedes activar los anuncios desde configuracion, ver anuncios de video, donar mediante Paypal, o haciendote Patreon"), FaqItem("¿Para qué sirven los logros?", "Para divertirte, se añadieron para que los usuarios tuvieran un objetivo aparte de ver anime"), FaqItem("¿Cómo puedo cambiar de color la app?", "Debes resolver el easter egg"), FaqItem("¿Como puedo reportar un error?", "Mediante la página de facebook, o mandando un mensaje al desarrollador vía Telegram o email"), FaqItem("¿Que es el modo family friendly?", "Este modo inhabilita los animes con genero ecchi"), FaqItem("¿Por qué al abrir la app me dice error de conexión (tiempo de conexión)?", "Animeflv podria estar lento, esto suele solucionarse después de unos minutos"), FaqItem("¿Cómo puedo contactar al desarrollador?", "Mediante la página de facebook, en Telegram como @UnbarredStream, o al email jordyamc@hotmail.com"), FaqItem("¿Ella en verdad me ama?", "NO") ) companion object { fun open(context: Context) = context.startActivity(Intent(context, FaqActivity::class.java)) } } ================================================ FILE: app/src/main/java/knf/kuma/faq/FaqActivityMaterial.kt ================================================ package knf.kuma.faq import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.core.net.toUri import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItems import knf.kuma.R import knf.kuma.commons.EAHelper import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.databinding.RecyclerFaqMaterialBinding import org.jetbrains.anko.toast class FaqActivityMaterial : GenericActivity() { private val binding by lazy { RecyclerFaqMaterialBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.title = "FAQ" binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } binding.recycler.layoutManager = VariantLinearLayoutManager(this) binding.recycler.adapter = FaqAdapter(createFAQList()) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_bug, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { MaterialDialog(this).safeShow { title(text = "Reportar problema") listItems(items = listOf("Telegram", "Facebook", "Email")) { _, index, _ -> when (index) { 0 -> startActivity(Intent(Intent.ACTION_VIEW, "https://t.me/unbarredstream".toUri())) 1 -> startActivity(Intent(Intent.ACTION_VIEW, "https://www.facebook.com/ukikuapp/".toUri())) 2 -> { val intent = Intent(Intent.ACTION_VIEW, "mailto:?subject=Problema con UKIKU&to=jordyamc@hotmail.com".toUri()) val chooser = Intent.createChooser(intent, "Enviar reporte") if (intent.resolveActivity(packageManager) != null) { startActivity(chooser) } else toast("No se encontraron clientes de email") } } } } return super.onOptionsItemSelected(item) } private fun createFAQList(): List = listOf( FaqItem("¿Dónde está el servidor natsuki?", "Los servidores vienen de Animeflv, la app no tiene el control de los que aparecen, tan solo extrae los que están disponibles"), FaqItem("¿Por qué no me funcionan los servidores?", "Los servidores vienen de Animeflv, son ellos los encargados de re subir los enlaces caídos, la app no puede hacer nada"), FaqItem("¿Para que funcionan las loli-coins?", "Sirven para comprar los pasos del easter egg y revelar logros ocultos"), FaqItem("¿Por qué me dice error 403?", "Solo presiona la barra roja que aparece"), FaqItem("¿Por qué no está este anime en la app?", "La app utiliza Animeflv para obtener la lista de animes, si no te aparece es porque, o no esta en Animeflv o no estas escribiendo el nombre correctamente"), FaqItem("¿Puedo pedir animes?", "No, la app utiliza Animeflv, no subimos los animes"), FaqItem("¿Por qué no me aparece el botón de añadir a favoritos?", "Es un error muuuuuuy raro, nunca se pudo encontrar una solución, en ese caso puedes añadir y quitar animes a favoritos haciendo click largo en la imagen del anime"), FaqItem("¿Se puede hacer Cast?", "Si, la app soporta transmisión por CAST, aparecerá un icono en la parte superior"), FaqItem("¿Por qué no puedo hacer Cast con capítulos Online?", "Si tu TV es Samsung entonces es imposible, la TV bloquea los intentos por usar CAST, en ese caso puedes descargar el capítulo y hacer CAST local"), FaqItem("¿Que significa Cloudflare activado?", "Es una protección de la página de Animeflv, la app debería poder pasar esa protección automáticamente"), FaqItem("¿Puedo adelantar el OP y ED?", "Si, en el reproductor interno hay un botón para saltar 1:30m (Lo que por lo regular dura un OP o ED)"), FaqItem("¿Se le puede hacer PiP (ventana flotante) a un capítulo?", "Si, pero solo es compatible con Android 8.1 o superior"), FaqItem("¿Por qué me va muy lenta las descargas?", "Esto podría ser por varios factores, tu internet, la velocidad de escritura (mientras más ocupado este el dispositivo más tardará), el servidor que estés usando para la descarga"), FaqItem("¿Hay forma de ver una serie de corrido (ver el siguiente cap sin regresar a los caps)?", "Si, puedes añadir los capítulos a la cola y verlos todos"), FaqItem("¿Por qué se me reinicia el capítulo cuando contesto mensajes (salir y entrar a la app)?", "Algunos dispositivos no muy potentes necesitan \"matar\" las aplicaciones en segundo plano para ahorrar memoria, esto sumado a que no todos los servidores soportan el adelantar videos"), FaqItem("¿Puedo añadir un sonido personalizado a las notificaciones?", "Si, hay una opción en configuraciones para ello"), FaqItem("¿Qué pasará si animeflv deja de existir?", "Se considerara cambiar de página o crear una app desde 0"), FaqItem("¿Puedo guardar mis animes en \"favoritos\" y \"siguiendo\"?", "Si, las dos secciones son independientes"), FaqItem("¿Cómo puedo ayudar a la app?", "Puedes activar los anuncios desde configuracion, ver anuncios de video, donar mediante Paypal, o haciendote Patreon"), FaqItem("¿Para qué sirven los logros?", "Para divertirte, se añadieron para que los usuarios tuvieran un objetivo aparte de ver anime"), FaqItem("¿Cómo puedo cambiar de color la app?", "Debes resolver el easter egg"), FaqItem("¿Como puedo reportar un error?", "Mediante la página de facebook, o mandando un mensaje al desarrollador vía Telegram o email"), FaqItem("¿Que es el modo family friendly?", "Este modo inhabilita los animes con genero ecchi"), FaqItem("¿Por qué al abrir la app me dice error de conexión (tiempo de conexión)?", "Animeflv podria estar lento, esto suele solucionarse después de unos minutos"), FaqItem("¿Cómo puedo contactar al desarrollador?", "Mediante la página de facebook, en Telegram como @UnbarredStream, o al email jordyamc@hotmail.com"), FaqItem("¿Ella en verdad me ama?", "NO") ) companion object { fun open(context: Context) = context.startActivity(Intent(context, FaqActivityMaterial::class.java)) } } ================================================ FILE: app/src/main/java/knf/kuma/faq/FaqAdapter.kt ================================================ package knf.kuma.faq import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.github.florent37.expansionpanel.ExpansionLayout import com.github.florent37.expansionpanel.viewgroup.ExpansionLayoutCollection import knf.kuma.R import knf.kuma.commons.inflate import org.jetbrains.anko.find class FaqAdapter(private val list: List) : RecyclerView.Adapter() { private val expansionCollection = ExpansionLayoutCollection().apply { openOnlyOne(true) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder = ItemHolder(parent.inflate(R.layout.item_faq)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: ItemHolder, position: Int) { val item = list[position] holder.apply { question.text = item.question answer.text = item.answer expansionLayout.let { it.collapse(false) expansionCollection.add(it) } } } class ItemHolder(val view: View) : RecyclerView.ViewHolder(view) { val question: TextView by lazy { view.find(R.id.question) } val answer: TextView by lazy { view.find(R.id.answer) } val expansionLayout: ExpansionLayout by lazy { view.find(R.id.expansionLayout) } } } ================================================ FILE: app/src/main/java/knf/kuma/faq/FaqItem.kt ================================================ package knf.kuma.faq data class FaqItem(val question: String, val answer: String) ================================================ FILE: app/src/main/java/knf/kuma/favorite/FavSectionHelper.kt ================================================ package knf.kuma.favorite import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import knf.kuma.commons.doOnUIGlobal import knf.kuma.database.CacheDB import knf.kuma.favorite.objects.FavSorter import knf.kuma.favorite.objects.InfoContainer import knf.kuma.pojos.FavSection import knf.kuma.pojos.FavoriteObject import org.jetbrains.anko.doAsync import java.util.Collections object FavSectionHelper { private val infoContainer = InfoContainer() var currentList: MutableList = ArrayList() private set private val liveData = MutableLiveData>() private val list: MutableList get() { val list = ArrayList() var currentSection: String? = null var section: MutableList = ArrayList() var noSection: MutableList = ArrayList() for (favoriteObject in CacheDB.INSTANCE.favsDAO().byCategory) { if (currentSection == null || currentSection != favoriteObject.category) { if (currentSection != null && currentSection != favoriteObject.category) { if (currentSection != FavoriteObject.CATEGORY_NONE) { list.add(FavSection(currentSection)) Collections.sort(section, FavSorter()) list.addAll(section) } else noSection = ArrayList(section) section = ArrayList() } currentSection = favoriteObject.category section.add(favoriteObject) } else if (currentSection == favoriteObject.category) section.add(favoriteObject) } if (currentSection != null) if (currentSection != FavoriteObject.CATEGORY_NONE) { list.add(FavSection(currentSection)) Collections.sort(section, FavSorter()) list.addAll(section) } else noSection = ArrayList(section) if (noSection.isNotEmpty()) { list.add(FavSection(FavoriteObject.CATEGORY_NONE)) Collections.sort(noSection, FavSorter()) list.addAll(noSection) } infoContainer.setLists(currentList, list) currentList = list return list } fun init(): LiveData> { reload() return getLiveData() } fun getInfoContainer(favoriteObject: FavoriteObject?): InfoContainer { infoContainer.reload(favoriteObject) return infoContainer } private fun getLiveData(): LiveData> { return liveData } private fun setLiveData(list: MutableList) { doOnUIGlobal { liveData.value = list } } fun reload() { doAsync { setLiveData(list) } } } ================================================ FILE: app/src/main/java/knf/kuma/favorite/FavoriteFragment.kt ================================================ package knf.kuma.favorite import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import androidx.annotation.LayoutRes import androidx.fragment.app.FragmentActivity import androidx.fragment.app.activityViewModels import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.WhichButton import com.afollestad.materialdialogs.actions.setActionButtonEnabled import com.afollestad.materialdialogs.input.getInputField import com.afollestad.materialdialogs.input.getInputLayout import com.afollestad.materialdialogs.input.input import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.pojos.FavSection import knf.kuma.pojos.FavoriteObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster class FavoriteFragment : BottomFragment(), FavsSectionAdapter.OnMoveListener { lateinit var recyclerView: FastScrollRecyclerView private lateinit var errorLayout: LinearLayout private var edited: FavoriteObject? = null private var manager: RecyclerView.LayoutManager? = null private var adapter: FavsSectionAdapter? = null private var isFirst = true private val model: FavoriteViewModel by activityViewModels() private lateinit var liveData: LiveData> private lateinit var observer: Observer> private var count = 0 private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_favs } else { R.layout.recycler_favs_grid } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) CacheDB.INSTANCE.favsDAO().all.observe(viewLifecycleOwner, Observer { FavSectionHelper.reload() }) activity?.let { observeList(it, Observer { favoriteObjects -> if (favoriteObjects == null || favoriteObjects.isEmpty()) { errorLayout.visibility = View.VISIBLE adapter?.updateList(ArrayList()) } else if (PrefsUtil.showFavSections()) { errorLayout.visibility = View.GONE val container = FavSectionHelper.getInfoContainer(edited) if (container.needReload) { adapter?.updateList(favoriteObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } } else adapter?.updatePosition(container) } else { errorLayout.visibility = View.GONE adapter?.updateList(favoriteObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } } edited = null }) } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) recyclerView.verifyManager() errorLayout = view.find(R.id.error) if (PrefsUtil.layType == "1" || !PrefsUtil.isNativeAdsEnabled) lifecycleScope.launch(Dispatchers.IO) { delay(1000) view.find(R.id.adContainer).implBanner(AdsType.FAVORITE_BANNER, true) } return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) manager = recyclerView.layoutManager adapter = FavsSectionAdapter(this, recyclerView, PrefsUtil.showFavSections()) if (PrefsUtil.layType == "1" && PrefsUtil.showFavSections()) { (manager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return try { if (FavSectionHelper.currentList[position].isSection) (manager as GridLayoutManager).spanCount else 1 } catch (e: Exception) { 1 } } } } recyclerView.adapter = adapter EAHelper.enter1("F") } private fun observeList(activity: FragmentActivity, obs: Observer>) { adapter?.updateList(mutableListOf()) isFirst = true if (::liveData.isInitialized && ::observer.isInitialized) liveData.removeObserver(observer) liveData = model.getData() observer = obs liveData.observe(viewLifecycleOwner, observer) } fun onChangeOrder() { activity?.let { observeList(it, Observer { favoriteObjects -> if (favoriteObjects == null || favoriteObjects.isEmpty()) { adapter?.updateList(ArrayList()) errorLayout.post { errorLayout.visibility = View.VISIBLE } } else { adapter?.updateList(favoriteObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } } }) } } fun showNewCategoryDialog(favoriteObject: FavoriteObject?) { edited = favoriteObject showNewCategoryDialog(favoriteObject == null, null) } private fun showNewCategoryDialog(isEmpty: Boolean, name: String?) { lifecycleScope.launch(Dispatchers.Main) { val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } context?.let { MaterialDialog(it).safeShow { title(text = "${if (name == null) "Nueva" else "Renombrar"} categoría") input(hint = "Nombre", prefill = name, waitForPositiveButton = false) { dialog, charSequence -> dialog.setActionButtonEnabled(WhichButton.POSITIVE, charSequence.isNotEmpty()) } getInputField().setBackgroundColor(Color.TRANSPARENT) getInputLayout().setBoxBackgroundColorResource(android.R.color.transparent) positiveButton(text = if (name == null) "Crear" else "Renombrar") { dialog -> val input = dialog.getInputField().text.toString() if (categories.contains(input)) { Toaster.toast("Esta categoría ya existe") showNewCategoryDialog(isEmpty, name) } else { if (isEmpty) showNewCategoryInit(false, input) else { launch(Dispatchers.IO) { edited?.let { favObj -> favObj.category = input CacheDB.INSTANCE.favsDAO().addFav(favObj) syncData { favs() } edited = null } } } } } } } } } fun showNewCategory(prefill: String? = null) { lifecycleScope.launch(Dispatchers.Main){ val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } context?.let { ctx -> MaterialDialog(ctx).safeShow { title(text = "Nueva categoría") input(hint = "Nombre", prefill = prefill, waitForPositiveButton = false) { dialog, charSequence -> dialog.setActionButtonEnabled(WhichButton.POSITIVE, charSequence.isNotEmpty()) } getInputField().setBackgroundColor(Color.TRANSPARENT) getInputLayout().setBoxBackgroundColorResource(android.R.color.transparent) positiveButton(text = "Crear") { dialog -> val input = dialog.getInputField().text.toString() if (categories.contains(input)) { Toaster.toast("Esta categoría ya existe") showNewCategory(input) } else { doAsync { edited?.let { it.category = input CacheDB.INSTANCE.favsDAO().addFav(it) syncData { favs() } } doOnUI { showAddToCategory(edited == null, input) } } } } } } } } private fun showCategoryRename(name: String) { lifecycleScope.launch(Dispatchers.Main){ val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } context?.let { MaterialDialog(it).safeShow { title(text = "Renombrar categoría") input(hint = "Nombre", prefill = name, waitForPositiveButton = false) { dialog, charSequence -> dialog.setActionButtonEnabled(WhichButton.POSITIVE, charSequence.isNotEmpty()) } getInputField().setBackgroundColor(Color.TRANSPARENT) getInputLayout().setBoxBackgroundColorResource(android.R.color.transparent) positiveButton(text = "Renombrar") { dialog -> val input = dialog.getInputField().text.toString() if (categories.contains(input)) { Toaster.toast("Esta categoría ya existe") showCategoryRename(name) } else { doAsync { val objects = CacheDB.INSTANCE.favsDAO().getAllInCategory(name) for (favoriteObject in objects) { favoriteObject.category = input } CacheDB.INSTANCE.favsDAO().addAll(objects) syncData { favs() } } } } } } } } private fun showAddToCategory(needAnimes: Boolean, name: String) { lifecycleScope.launch(Dispatchers.Main) { val fName = if (name == "Sin categoría") FavoriteObject.CATEGORY_NONE else name val favoriteObjects = withContext(Dispatchers.IO) { CacheDB.INSTANCE.favsDAO().getNotInCategory(fName) } if (favoriteObjects.isEmpty()) { if (needAnimes) Toaster.toast("Necesitas favoritos para crear una categoría") else Toaster.toast("No hay mas animes para agregar") } else { context?.let { MaterialDialog(it).safeShow { title(text = name) listItemsMultiChoice(items = FavoriteObject.getNames(favoriteObjects)) { _, indices, _ -> if (needAnimes && indices.isEmpty()) { Toaster.toast("La nueva categoría necesita animes!") showAddToCategory(needAnimes, name) } else { doAsync { edited = null val list = ArrayList() for (i in indices) { val favoriteObject = favoriteObjects[i] favoriteObject.category = fName list.add(favoriteObject) } CacheDB.INSTANCE.favsDAO().addAll(list) syncData { favs() } } } } positiveButton(text = "agregar") if (!needAnimes) negativeButton(text = "Cancelar") } } } } } private fun showDeleteCategory(name: String) { context?.let { MaterialDialog(it).safeShow { message(text = "¿Desea eliminar esta categoría?") positiveButton(text = "Eliminar") { doAsync { val objects = CacheDB.INSTANCE.favsDAO().getAllInCategory(name) for (favoriteObject in objects) { favoriteObject.category = FavoriteObject.CATEGORY_NONE } CacheDB.INSTANCE.favsDAO().addAll(objects) syncData { favs() } } } negativeButton(text = "Cancelar") } } } private fun showNewCategoryInit(isEdit: Boolean, name: String) { lifecycleScope.launch(Dispatchers.Main) { val fName = if (name == "Sin categoría") FavoriteObject.CATEGORY_NONE else name val favoriteObjects = withContext(Dispatchers.IO) { CacheDB.INSTANCE.favsDAO().getNotInCategory(fName) } if (favoriteObjects.isEmpty()) { Toaster.toast("Necesitas favoritos para crear una categoría") } else { val isNotDefault = isEdit && fName != FavoriteObject.CATEGORY_NONE context?.let { MaterialDialog(it).safeShow { title(text = name) listItemsMultiChoice(items = FavoriteObject.getNames(favoriteObjects)) { _, indices, _ -> edited = null val list = ArrayList() for (i in indices) { val favoriteObject = favoriteObjects[i] favoriteObject.category = fName list.add(favoriteObject) } doAsync { CacheDB.INSTANCE.favsDAO().addAll(list) syncData { favs() } } } positiveButton(text = "agregar") if (isNotDefault || !isEdit) negativeButton(text = when { isNotDefault -> "cancelar" !isEdit -> "atras" else -> "" }) { if (isNotDefault) MaterialDialog(context).safeShow { message(text = "¿Desea eliminar esta categoría?") positiveButton(text = "continuar") { edited = null doAsync { val objects = CacheDB.INSTANCE.favsDAO().getAllInCategory(fName) for (favoriteObject in objects) { favoriteObject.category = FavoriteObject.CATEGORY_NONE } CacheDB.INSTANCE.favsDAO().addAll(objects) syncData { favs() } } } } else if (!isEdit) showNewCategoryDialog(true, name) } if (!isEdit) setOnCancelListener { showNewCategoryDialog(true, name) } } } } } } override fun onEdit(category: String) { if (category == "Sin categoría") showAddToCategory(false, category) else context?.let { MaterialDialog(it).safeShow { title(text = category) listItems(items = listOf("Renombrar", "Agregar animes", "Eliminar sección")) { _, index, _ -> when (index) { 0 -> showCategoryRename(category) 1 -> showAddToCategory(false, category) 2 -> showDeleteCategory(category) } } } } //showNewCategoryInit(true, category) } override fun onSelect(favoriteObject: FavoriteObject) { if (favoriteObject !is FavSection) { lifecycleScope.launch(Dispatchers.Main) { val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } if (categories.size <= 1) { edited = favoriteObject showNewCategory(null) } else { context?.let { context -> MaterialDialog(context).safeShow { title(text = "Mover a...") listItemsSingleChoice(items = categories, initialSelection = categories.indexOf(favoriteObject.category)) { _, _, text -> doAsync { if (text != favoriteObject.category) { edited = favoriteObject.also { it.category = if (text == "Sin categoría") "_NONE_" else text.toString() CacheDB.INSTANCE.favsDAO().addFav(it) syncData { favs() } } } else Toaster.toast("Error al mover") } } positiveButton(text = "mover") negativeButton(text = "nuevo") { edited = favoriteObject showNewCategory(null) } } } } } } } override fun onReselect() { EAHelper.enter1("F") manager?.let { it.smoothScrollToPosition(recyclerView, null, 0) count++ if (count == 3) { lifecycleScope.launch(Dispatchers.IO){ if (adapter != null) Toaster.toast("Tienes " + CacheDB.INSTANCE.favsDAO().count + " animes en favoritos") count = 0 } } } } companion object { fun get(): FavoriteFragment { return FavoriteFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/favorite/FavoriteFragmentMaterial.kt ================================================ package knf.kuma.favorite import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import androidx.annotation.LayoutRes import androidx.fragment.app.FragmentActivity import androidx.fragment.app.activityViewModels import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.WhichButton import com.afollestad.materialdialogs.actions.setActionButtonEnabled import com.afollestad.materialdialogs.input.getInputField import com.afollestad.materialdialogs.input.getInputLayout import com.afollestad.materialdialogs.input.input import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.noCrash import knf.kuma.commons.safeShow import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.pojos.FavSection import knf.kuma.pojos.FavoriteObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster class FavoriteFragmentMaterial : BottomFragment(), FavsSectionAdapterMaterial.OnMoveListener { lateinit var recyclerView: FastScrollRecyclerView private lateinit var errorLayout: LinearLayout private var edited: FavoriteObject? = null private var manager: RecyclerView.LayoutManager? = null private var adapter: FavsSectionAdapterMaterial? = null private var isFirst = true private val model: FavoriteViewModel by activityViewModels() private lateinit var liveData: LiveData> private lateinit var observer: Observer> private var count = 0 private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_favs_matertial } else { R.layout.recycler_favs_grid_material } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) CacheDB.INSTANCE.favsDAO().all.observe(viewLifecycleOwner) { FavSectionHelper.reload() } activity?.let { observeList(it, Observer { favoriteObjects -> if (favoriteObjects == null || favoriteObjects.isEmpty()) { errorLayout.visibility = View.VISIBLE adapter?.updateList(ArrayList()) } else if (PrefsUtil.showFavSections()) { errorLayout.visibility = View.GONE val container = FavSectionHelper.getInfoContainer(edited) if (container.needReload) { adapter?.updateList(favoriteObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } } else adapter?.updatePosition(container) } else { errorLayout.visibility = View.GONE adapter?.updateList(favoriteObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } } edited = null }) } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layout, container, false) recyclerView = view.find(R.id.recycler) recyclerView.verifyManager() errorLayout = view.find(R.id.error) if (PrefsUtil.layType == "1" || !PrefsUtil.isNativeAdsEnabled) lifecycleScope.launch(Dispatchers.IO) { delay(1000) noCrash { view.find(R.id.adContainer).implBanner(AdsType.FAVORITE_BANNER, true) } } return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) manager = recyclerView.layoutManager adapter = FavsSectionAdapterMaterial(this, recyclerView, PrefsUtil.showFavSections()) if (PrefsUtil.layType == "1" && PrefsUtil.showFavSections()) { (manager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return try { if (FavSectionHelper.currentList[position].isSection) (manager as GridLayoutManager).spanCount else 1 } catch (e: Exception) { 1 } } } } recyclerView.adapter = adapter EAHelper.enter1("F") } private fun observeList(activity: FragmentActivity, obs: Observer>) { adapter?.updateList(mutableListOf()) isFirst = true if (::liveData.isInitialized && ::observer.isInitialized) liveData.removeObserver(observer) liveData = model.getData() observer = obs liveData.observe(viewLifecycleOwner, observer) } fun onChangeOrder() { activity?.let { observeList(it, Observer { favoriteObjects -> if (favoriteObjects == null || favoriteObjects.isEmpty()) { adapter?.updateList(ArrayList()) errorLayout.post { errorLayout.visibility = View.VISIBLE } } else { adapter?.updateList(favoriteObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } } }) } } fun showNewCategoryDialog(favoriteObject: FavoriteObject?) { edited = favoriteObject showNewCategoryDialog(favoriteObject == null, null) } private fun showNewCategoryDialog(isEmpty: Boolean, name: String?) { lifecycleScope.launch(Dispatchers.Main) { val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } context?.let { MaterialDialog(it).safeShow { title(text = "${if (name == null) "Nueva" else "Renombrar"} categoría") input(hint = "Nombre", prefill = name, waitForPositiveButton = false) { dialog, charSequence -> dialog.setActionButtonEnabled(WhichButton.POSITIVE, charSequence.isNotEmpty()) } getInputField().setBackgroundColor(Color.TRANSPARENT) getInputLayout().setBoxBackgroundColorResource(android.R.color.transparent) positiveButton(text = if (name == null) "Crear" else "Renombrar") { dialog -> val input = dialog.getInputField().text.toString() if (categories.contains(input)) { Toaster.toast("Esta categoría ya existe") showNewCategoryDialog(isEmpty, name) } else { if (isEmpty) showNewCategoryInit(false, input) else { launch(Dispatchers.IO) { edited?.let { favObj -> favObj.category = input CacheDB.INSTANCE.favsDAO().addFav(favObj) syncData { favs() } edited = null } } } } } } } } } fun showNewCategory(prefill: String? = null) { lifecycleScope.launch(Dispatchers.Main){ val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } context?.let { ctx -> MaterialDialog(ctx).safeShow { title(text = "Nueva categoría") input(hint = "Nombre", prefill = prefill, waitForPositiveButton = false) { dialog, charSequence -> dialog.setActionButtonEnabled(WhichButton.POSITIVE, charSequence.isNotEmpty()) } getInputField().setBackgroundColor(Color.TRANSPARENT) getInputLayout().setBoxBackgroundColorResource(android.R.color.transparent) positiveButton(text = "Crear") { dialog -> val input = dialog.getInputField().text.toString() if (categories.contains(input)) { Toaster.toast("Esta categoría ya existe") showNewCategory(input) } else { doAsync { edited?.let { it.category = input CacheDB.INSTANCE.favsDAO().addFav(it) syncData { favs() } } doOnUI { showAddToCategory(edited == null, input) } } } } } } } } private fun showCategoryRename(name: String) { lifecycleScope.launch(Dispatchers.Main){ val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } context?.let { MaterialDialog(it).safeShow { title(text = "Renombrar categoría") input(hint = "Nombre", prefill = name, waitForPositiveButton = false) { dialog, charSequence -> dialog.setActionButtonEnabled(WhichButton.POSITIVE, charSequence.isNotEmpty()) } getInputField().setBackgroundColor(Color.TRANSPARENT) getInputLayout().setBoxBackgroundColorResource(android.R.color.transparent) positiveButton(text = "Renombrar") { dialog -> val input = dialog.getInputField().text.toString() if (categories.contains(input)) { Toaster.toast("Esta categoría ya existe") showCategoryRename(name) } else { doAsync { val objects = CacheDB.INSTANCE.favsDAO().getAllInCategory(name) for (favoriteObject in objects) { favoriteObject.category = input } CacheDB.INSTANCE.favsDAO().addAll(objects) syncData { favs() } } } } } } } } private fun showAddToCategory(needAnimes: Boolean, name: String) { lifecycleScope.launch(Dispatchers.Main) { val fName = if (name == "Sin categoría") FavoriteObject.CATEGORY_NONE else name val favoriteObjects = withContext(Dispatchers.IO) { CacheDB.INSTANCE.favsDAO().getNotInCategory(fName) } if (favoriteObjects.isEmpty()) { if (needAnimes) Toaster.toast("Necesitas favoritos para crear una categoría") else Toaster.toast("No hay mas animes para agregar") } else { context?.let { MaterialDialog(it).safeShow { title(text = name) listItemsMultiChoice(items = FavoriteObject.getNames(favoriteObjects)) { _, indices, _ -> if (needAnimes && indices.isEmpty()) { Toaster.toast("La nueva categoría necesita animes!") showAddToCategory(needAnimes, name) } else { doAsync { edited = null val list = ArrayList() for (i in indices) { val favoriteObject = favoriteObjects[i] favoriteObject.category = fName list.add(favoriteObject) } CacheDB.INSTANCE.favsDAO().addAll(list) syncData { favs() } } } } positiveButton(text = "agregar") if (!needAnimes) negativeButton(text = "Cancelar") } } } } } private fun showDeleteCategory(name: String) { context?.let { MaterialDialog(it).safeShow { message(text = "¿Desea eliminar esta categoría?") positiveButton(text = "Eliminar") { doAsync { val objects = CacheDB.INSTANCE.favsDAO().getAllInCategory(name) for (favoriteObject in objects) { favoriteObject.category = FavoriteObject.CATEGORY_NONE } CacheDB.INSTANCE.favsDAO().addAll(objects) syncData { favs() } } } negativeButton(text = "Cancelar") } } } private fun showNewCategoryInit(isEdit: Boolean, name: String) { lifecycleScope.launch(Dispatchers.Main) { val fName = if (name == "Sin categoría") FavoriteObject.CATEGORY_NONE else name val favoriteObjects = withContext(Dispatchers.IO) { CacheDB.INSTANCE.favsDAO().getNotInCategory(fName) } if (favoriteObjects.isEmpty()) { Toaster.toast("Necesitas favoritos para crear una categoría") } else { val isNotDefault = isEdit && fName != FavoriteObject.CATEGORY_NONE context?.let { MaterialDialog(it).safeShow { title(text = name) listItemsMultiChoice(items = FavoriteObject.getNames(favoriteObjects)) { _, indices, _ -> edited = null val list = ArrayList() for (i in indices) { val favoriteObject = favoriteObjects[i] favoriteObject.category = fName list.add(favoriteObject) } doAsync { CacheDB.INSTANCE.favsDAO().addAll(list) syncData { favs() } } } positiveButton(text = "agregar") if (isNotDefault || !isEdit) negativeButton(text = when { isNotDefault -> "cancelar" !isEdit -> "atras" else -> "" }) { if (isNotDefault) MaterialDialog(context).safeShow { message(text = "¿Desea eliminar esta categoría?") positiveButton(text = "continuar") { edited = null doAsync { val objects = CacheDB.INSTANCE.favsDAO().getAllInCategory(fName) for (favoriteObject in objects) { favoriteObject.category = FavoriteObject.CATEGORY_NONE } CacheDB.INSTANCE.favsDAO().addAll(objects) syncData { favs() } } } } else if (!isEdit) showNewCategoryDialog(true, name) } if (!isEdit) setOnCancelListener { showNewCategoryDialog(true, name) } } } } } } override fun onEdit(category: String) { if (category == "Sin categoría") showAddToCategory(false, category) else context?.let { MaterialDialog(it).safeShow { title(text = category) listItems(items = listOf("Renombrar", "Agregar animes", "Eliminar sección")) { _, index, _ -> when (index) { 0 -> showCategoryRename(category) 1 -> showAddToCategory(false, category) 2 -> showDeleteCategory(category) } } } } //showNewCategoryInit(true, category) } override fun onSelect(favoriteObject: FavoriteObject) { if (favoriteObject !is FavSection) { lifecycleScope.launch(Dispatchers.Main) { val categories = withContext(Dispatchers.IO) { FavoriteObject.getCategories(CacheDB.INSTANCE.favsDAO().categories) } if (categories.size <= 1) { edited = favoriteObject showNewCategory(null) } else { context?.let { context -> MaterialDialog(context).safeShow { title(text = "Mover a...") listItemsSingleChoice(items = categories, initialSelection = categories.indexOf(favoriteObject.category)) { _, _, text -> doAsync { if (text != favoriteObject.category) { edited = favoriteObject.also { it.category = if (text == "Sin categoría") "_NONE_" else text.toString() CacheDB.INSTANCE.favsDAO().addFav(it) syncData { favs() } } } else Toaster.toast("Error al mover") } } positiveButton(text = "mover") negativeButton(text = "nuevo") { edited = favoriteObject showNewCategory(null) } } } } } } } override fun onReselect() { EAHelper.enter1("F") manager?.let { it.smoothScrollToPosition(recyclerView, null, 0) count++ if (count == 3) { lifecycleScope.launch(Dispatchers.IO){ if (adapter != null) Toaster.toast("Tienes " + CacheDB.INSTANCE.favsDAO().count + " animes en favoritos") count = 0 } } } } companion object { fun get(): FavoriteFragmentMaterial { return FavoriteFragmentMaterial() } } } ================================================ FILE: app/src/main/java/knf/kuma/favorite/FavoriteViewModel.kt ================================================ package knf.kuma.favorite import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import knf.kuma.commons.PrefsUtil import knf.kuma.database.CacheDB import knf.kuma.pojos.FavoriteObject class FavoriteViewModel : ViewModel() { fun getData(): LiveData> { return if (PrefsUtil.showFavSections()) FavSectionHelper.init() else when (PrefsUtil.favsOrder) { 1 -> CacheDB.INSTANCE.favsDAO().allID else -> CacheDB.INSTANCE.favsDAO().all } } } ================================================ FILE: app/src/main/java/knf/kuma/favorite/FavsSectionAdapter.kt ================================================ package knf.kuma.favorite import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.R import knf.kuma.ads.AdCallback import knf.kuma.ads.AdCardItemHolder import knf.kuma.ads.AdFavoriteObject import knf.kuma.ads.AdsUtilsMob import knf.kuma.ads.implAdsFavorite import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.favorite.objects.InfoContainer import knf.kuma.pojos.FavoriteObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class FavsSectionAdapter(private val fragment: Fragment, private val recyclerView: FastScrollRecyclerView, private val showSections: Boolean) : RecyclerView.Adapter(), FastScrollRecyclerView.SectionedAdapter { private val context: Context? private val listener: OnMoveListener private val orderType = PrefsUtil.favsOrder private var list: MutableList = ArrayList() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_fav } else { R.layout.item_fav_grid } init { this.listener = fragment as OnMoveListener this.context = fragment.context } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { TYPE_HEADER -> HeaderHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_fav_header, parent, false)) TYPE_ITEM -> ItemHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) TYPE_AD -> AdCardItemHolder(parent, AdCardItemHolder.TYPE_FAV).also { it.loadAd(fragment.lifecycleScope, object : AdCallback { override fun getID(): String = AdsUtilsMob.FAVORITE_BANNER }, 500) } else -> HeaderHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_fav_header, parent, false)) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val favoriteObject = list[position] if (holder is HeaderHolder) { holder.header.text = favoriteObject.name holder.action.setOnClickListener { listener.onEdit(favoriteObject.name ?: "") } } else if (holder is ItemHolder) { holder.imageView.load(PatternUtil.getCover(favoriteObject.aid ?: "")) holder.title.text = favoriteObject.name holder.type.text = favoriteObject.type holder.cardView.setOnClickListener { ActivityAnime.open(fragment, favoriteObject, holder.imageView) } if (showSections) holder.cardView.setOnLongClickListener { listener.onSelect(favoriteObject) true } } } override fun getItemCount(): Int { return list.size } override fun getItemViewType(position: Int): Int { return try { if (list[position] is AdFavoriteObject) FavsSectionAdapterMaterial.TYPE_AD else if (list[position].isSection) FavsSectionAdapterMaterial.TYPE_HEADER else FavsSectionAdapterMaterial.TYPE_ITEM } catch (_: Exception) { FavsSectionAdapterMaterial.TYPE_ITEM } } override fun getSectionName(position: Int): String { return try { if (showSections) "" else when (orderType) { 0 -> { val name = list[position].name if (name.isNotEmpty()) name.substring(0, 1).uppercase() else name } else -> list[position].aid } } catch (_: IllegalStateException) { "" } } fun updatePosition(container: InfoContainer) { val nlist = container.updated if (!nlist.isNullOrEmpty() && container.from != -1 && container.to != -1) { list = nlist recyclerView.post { notifyItemMoved(container.from, container.to) } } } fun updateList(list: MutableList) { fragment.lifecycleScope.launch(Dispatchers.IO) { this@FavsSectionAdapter.list = list if (PrefsUtil.layType == "0" && PrefsUtil.isNativeAdsEnabled) this@FavsSectionAdapter.list.implAdsFavorite() recyclerView.post { this@FavsSectionAdapter.notifyDataSetChanged() } } } internal interface OnMoveListener { fun onSelect(favoriteObject: FavoriteObject) fun onEdit(category: String) } internal class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView by itemView.bind(R.id.type) } internal class HeaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val header: TextView by itemView.bind(R.id.header) val action: ImageButton by itemView.bind(R.id.action) } companion object { internal const val TYPE_HEADER = 0 internal const val TYPE_ITEM = 1 internal const val TYPE_AD = 2 } } ================================================ FILE: app/src/main/java/knf/kuma/favorite/FavsSectionAdapterMaterial.kt ================================================ package knf.kuma.favorite import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.R import knf.kuma.ads.AdCallback import knf.kuma.ads.AdCardItemHolder import knf.kuma.ads.AdFavoriteObject import knf.kuma.ads.AdsUtilsMob import knf.kuma.ads.implAdsFavorite import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.favorite.objects.InfoContainer import knf.kuma.pojos.FavoriteObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class FavsSectionAdapterMaterial(private val fragment: Fragment, private val recyclerView: FastScrollRecyclerView, private val showSections: Boolean) : RecyclerView.Adapter(), FastScrollRecyclerView.SectionedAdapter { private val context: Context? private val listener: OnMoveListener private val orderType = PrefsUtil.favsOrder private var list: MutableList = ArrayList() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_fav_material } else { R.layout.item_fav_grid_material } init { this.listener = fragment as OnMoveListener this.context = fragment.context } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { TYPE_HEADER -> HeaderHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_fav_header, parent, false)) TYPE_ITEM -> ItemHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) TYPE_AD -> AdCardItemHolder(parent, AdCardItemHolder.TYPE_FAV).also { it.loadAd(fragment.lifecycleScope, object : AdCallback { override fun getID(): String = AdsUtilsMob.FAVORITE_BANNER }, 500) } else -> HeaderHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_fav_header, parent, false)) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (list.size == 0) return val favoriteObject = list[position] if (holder is HeaderHolder) { holder.header.text = favoriteObject.name holder.action.setOnClickListener { listener.onEdit(favoriteObject.name ?: "") } } else if (holder is ItemHolder) { holder.imageView.load(PatternUtil.getCover(favoriteObject.aid ?: "")) holder.title.text = favoriteObject.name holder.type.text = favoriteObject.type holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, favoriteObject, holder.imageView) } if (showSections) holder.cardView.setOnLongClickListener { listener.onSelect(favoriteObject) true } } } override fun getItemCount(): Int { return list.size } override fun getItemViewType(position: Int): Int { return try { if (list[position] is AdFavoriteObject) TYPE_AD else if (list[position].isSection) TYPE_HEADER else TYPE_ITEM } catch (_: Exception) { TYPE_ITEM } } override fun getSectionName(position: Int): String { return try { if (showSections) "" else when (orderType) { 0 -> { val name = list[position].name if (name.isNotEmpty()) name.substring(0, 1).uppercase() else name } else -> list[position].aid } } catch (_: IllegalStateException) { "" } } fun updatePosition(container: InfoContainer) { val nlist = container.updated if (!nlist.isNullOrEmpty() && container.from != -1 && container.to != -1) { list = nlist recyclerView.post { notifyItemMoved(container.from, container.to) } } } fun updateList(list: MutableList) { fragment.lifecycleScope.launch(Dispatchers.IO) { this@FavsSectionAdapterMaterial.list = list if (PrefsUtil.layType == "0" && PrefsUtil.isNativeAdsEnabled) this@FavsSectionAdapterMaterial.list.implAdsFavorite() recyclerView.post { this@FavsSectionAdapterMaterial.notifyDataSetChanged() } } } internal interface OnMoveListener { fun onSelect(favoriteObject: FavoriteObject) fun onEdit(category: String) } internal class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView by itemView.bind(R.id.type) } internal class HeaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val header: TextView by itemView.bind(R.id.header) val action: ImageButton by itemView.bind(R.id.action) } companion object { internal const val TYPE_HEADER = 0 internal const val TYPE_ITEM = 1 internal const val TYPE_AD = 2 } } ================================================ FILE: app/src/main/java/knf/kuma/favorite/objects/FavSorter.kt ================================================ package knf.kuma.favorite.objects import knf.kuma.commons.PrefsUtil import knf.kuma.pojos.FavoriteObject class FavSorter : Comparator { override fun compare(o1: FavoriteObject, o2: FavoriteObject): Int { return when (PrefsUtil.favsOrder) { 0 -> o1.name?.compareTo(o2.name ?: "") ?: 0 1 -> o1.aid?.compareTo(o2.aid ?: "") ?: 0 else -> o1.name?.compareTo(o2.name ?: "") ?: 0 } } } ================================================ FILE: app/src/main/java/knf/kuma/favorite/objects/InfoContainer.kt ================================================ package knf.kuma.favorite.objects import knf.kuma.pojos.FavSection import knf.kuma.pojos.FavoriteObject class InfoContainer { var updated: MutableList? = arrayListOf() var needReload = false var from: Int = 0 var to: Int = 0 private var current: MutableList? = null fun setLists(current: MutableList, updated: MutableList) { this.current = ArrayList(current) this.updated = ArrayList(updated) } fun reload(favoriteObject: FavoriteObject?) { when { favoriteObject == null -> needReload = true favoriteObject is FavSection -> needReload = true updated?.contains(favoriteObject) == false -> needReload = true current?.size != updated?.size -> needReload = true else -> { needReload = false from = current?.indexOf(favoriteObject) ?: -1 to = updated?.indexOf(favoriteObject) ?: -1 } } } } ================================================ FILE: app/src/main/java/knf/kuma/home/DirAdapter.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.directory.DirObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class DirAdapter(val fragment: HomeFragment) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@DirAdapter.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.name holder.type?.text = "\u2605${item.rate_stars ?: "?.?"}" holder.root.onClick { ActivityAnime.open(fragment, item, holder.img, true, true) } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/DirAdapterMaterial.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.directory.DirObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class DirAdapterMaterial(val fragment: HomeFragmentMaterial) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@DirAdapterMaterial.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card_material)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.name holder.type?.text = "\u2605${item.rate_stars ?: "?.?"}" holder.root.onClick { ActivityAnimeMaterial.open(fragment, item) } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/HomeFragment.kt ================================================ package knf.kuma.home import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.google.firebase.Firebase import com.google.firebase.crashlytics.crashlytics import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.safeContext import knf.kuma.database.CacheDB import knf.kuma.databinding.FragmentHomeBinding import knf.kuma.pojos.QueueObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.SeeingObject import knf.kuma.queue.QueueActivity import knf.kuma.recents.RecentsActivity import knf.kuma.recents.RecentsViewModel import knf.kuma.recommended.RecommendActivity import knf.kuma.recommended.RecommendHelper import knf.kuma.seeing.SeeingActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync class HomeFragment : BottomFragment() { private val viewModel: RecentsViewModel by viewModels() private var lastNew: String = "0" private lateinit var binding: FragmentHomeBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { EAHelper.enter1("R") return inflater.inflate(R.layout.fragment_home, container, false).also { binding = FragmentHomeBinding.bind(it) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.listNew.apply { setAdapter(RecentsAdapter(this@HomeFragment, isLarge = false, showSeen = false)) setViewAllOnClick { PrefsUtil.recentLastHiddenNew = lastNew.toInt() binding.listNew.hide() } } binding.listFavUpdated.apply { setAdapter(RecentsAdapter(this@HomeFragment, true)) setViewAllClass(RecentsActivity::class.java) } binding.listBestEmission.setAdapter(DirAdapter(this)) binding.listPending.apply { setAdapter(QueueAdapter(this@HomeFragment)) setViewAllClass(QueueActivity::class.java) } binding.listWaiting.apply { setAdapter(WaitingAdapter(this@HomeFragment)) setViewAllClass(SeeingActivity::class.java) } binding.listRecommended.apply { setAdapter(RecommendedAdapter(activity)) setViewAllClass(RecommendActivity::class.java) } binding.listRecommendedStaff.setAdapter(SearchAdapter(this)) lifecycleScope.launch(Dispatchers.IO) { delay(1000) binding.adContainer.implBanner(AdsType.RECENT_BANNER, true) delay(500) binding.adContainer2.implBanner(AdsType.RECENT_BANNER2, true) } lifecycleScope.launch { viewModel.dbFlow.collect { list -> if (list.isNotEmpty()) { doAsync { try { binding.listNew.updateList(filterNew(list.filter { it.isNew })) val favFiltered = list.filter { CacheDB.INSTANCE.favsDAO().isFav(it.aid.toInt()) } if (favFiltered.isEmpty()) { binding.listFavUpdated.apply { setSubheader("Ultimos actualizados") setError("Recientes no actualizados") updateList(list) } } else { binding.listFavUpdated.apply { setSubheader("Favoritos actualizados") updateList(favFiltered) } } } catch (e: Exception) { Firebase.crashlytics.recordException(e) lifecycleScope.launch(Dispatchers.Main) { Toast.makeText(safeContext, "Error al mostrar recientes: ${e.message}", Toast.LENGTH_LONG).show() } } } } } } lifecycleScope.launch { CacheDB.INSTANCE.favsDAO().countFlow.drop(1).collect { doAsync { val cached = CacheDB.INSTANCE.recentsDAO().all val filtered = cached.filter { CacheDB.INSTANCE.favsDAO().isFav(it.aid.toInt()) } binding.listFavUpdated.apply { if (filtered.isEmpty()) { setSubheader("Ultimos actualizados") setError("Recientes no actualizados") updateList(cached) } else { setSubheader("Favoritos actualizados") updateList(filtered) } } } } } CacheDB.INSTANCE.favsDAO().countLive.observe(viewLifecycleOwner, Observer { RecommendHelper.createRecommended { binding.listRecommended.updateList(it) } }) CacheDB.INSTANCE.animeDAO().emissionVotesLimited.observe(viewLifecycleOwner, Observer { binding.listBestEmission.updateList(it) }) CacheDB.INSTANCE.queueDAO().all.observe(viewLifecycleOwner, Observer { doAsync { binding.listPending.updateList(QueueObject.takeOne(it)) } }) CacheDB.INSTANCE.seeingDAO().getAllWState(SeeingObject.STATE_CONSIDERING, SeeingObject.STATE_PAUSED).observe(viewLifecycleOwner, Observer { binding.listWaiting.updateList(it) }) StaffRecommendations.createList { binding.listRecommendedStaff.updateList(it) } viewModel.reload() } private fun filterNew(list: List): List { if (list.isNotEmpty()) { lastNew = list[0].aid if (list[0].aid.toInt() == PrefsUtil.recentLastHiddenNew) return emptyList() } return list } override fun onReselect() { EAHelper.enter1("R") } } ================================================ FILE: app/src/main/java/knf/kuma/home/HomeFragmentMaterial.kt ================================================ package knf.kuma.home import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.google.firebase.Firebase import com.google.firebase.crashlytics.crashlytics import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrashSuspend import knf.kuma.commons.safeContext import knf.kuma.database.CacheDB import knf.kuma.databinding.FragmentHomeMaterialBinding import knf.kuma.pojos.QueueObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.SeeingObject import knf.kuma.queue.QueueActivityMaterial import knf.kuma.recents.RecentsModelActivity import knf.kuma.recents.RecentsViewModel import knf.kuma.recommended.RecommendActivityMaterial import knf.kuma.recommended.RecommendHelper import knf.kuma.seeing.SeeingActivityMaterial import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync class HomeFragmentMaterial : BottomFragment() { private val viewModel: RecentsViewModel by viewModels() private var lastNew: String = "0" private lateinit var binding: FragmentHomeMaterialBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { EAHelper.enter1("R") return inflater.inflate(R.layout.fragment_home_material, container, false).also { binding = FragmentHomeMaterialBinding.bind(it) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.listNew.apply { setAdapter(RecentsAdapterMaterial(this@HomeFragmentMaterial, isLarge = false, showSeen = false)) setViewAllOnClick { PrefsUtil.recentLastHiddenNew = lastNew.toInt() binding.listNew.hide() } } binding.listFavUpdated.apply { setAdapter(RecentsAdapterMaterial(this@HomeFragmentMaterial, true)) setViewAllClass(RecentsModelActivity::class.java) } binding.listBestEmission.setAdapter(DirAdapterMaterial(this)) binding.listPending.apply { setAdapter(QueueAdapterMaterial(this@HomeFragmentMaterial)) setViewAllClass(QueueActivityMaterial::class.java) } binding.listWaiting.apply { setAdapter(WaitingAdapterMaterial(this@HomeFragmentMaterial)) setViewAllClass(SeeingActivityMaterial::class.java) } binding.listRecommended.apply { setAdapter(RecommendedAdapterMaterial(activity)) setViewAllClass(RecommendActivityMaterial::class.java) } binding.listRecommendedStaff.setAdapter(SearchAdapterMaterial(this)) lifecycleScope.launch(Dispatchers.IO) { noCrashSuspend { delay(1000) binding.adContainer.implBanner(AdsType.RECENT_BANNER, true) delay(500) binding.adContainer2.implBanner(AdsType.RECENT_BANNER2, true) } } lifecycleScope.launch { viewModel.dbFlow.collect { list -> if (list.isNotEmpty()) { doAsync { try { binding.listNew.updateList(filterNew(list.filter { it.isNew })) val favFiltered = list.filter { CacheDB.INSTANCE.favsDAO().isFav(it.aid.toInt()) } if (favFiltered.isEmpty()) { binding.listFavUpdated.apply { setSubheader("Ultimos actualizados") setError("Recientes no actualizados") updateList(list) } } else { binding.listFavUpdated.apply { setSubheader("Favoritos actualizados") updateList(favFiltered) } } } catch (e: Exception) { Firebase.crashlytics.recordException(e) lifecycleScope.launch(Dispatchers.Main) { Toast.makeText(safeContext, "Error al mostrar recientes: ${e.message}", Toast.LENGTH_LONG).show() } } } } } } lifecycleScope.launch { CacheDB.INSTANCE.favsDAO().countFlow.drop(1).collect { doAsync { val cached = CacheDB.INSTANCE.recentsDAO().all val filtered = cached.filter { CacheDB.INSTANCE.favsDAO().isFav(it.aid.toInt()) } binding.listFavUpdated.apply { if (filtered.isEmpty()) { setSubheader("Ultimos actualizados") setError("Recientes no actualizados") updateList(cached) } else { setSubheader("Favoritos actualizados") updateList(filtered) } } } } } CacheDB.INSTANCE.favsDAO().countLive.observe(viewLifecycleOwner, Observer { RecommendHelper.createRecommended { binding.listRecommended.updateList(it) } }) CacheDB.INSTANCE.animeDAO().emissionVotesLimited.observe(viewLifecycleOwner, Observer { binding.listBestEmission.updateList(it) }) CacheDB.INSTANCE.queueDAO().all.observe(viewLifecycleOwner, Observer { doAsync { binding.listPending.updateList(QueueObject.takeOne(it)) } }) CacheDB.INSTANCE.seeingDAO().getAllWState(SeeingObject.STATE_CONSIDERING, SeeingObject.STATE_PAUSED).observe(viewLifecycleOwner, Observer { binding.listWaiting.updateList(it) }) StaffRecommendations.createList { binding.listRecommendedStaff.updateList(it) } viewModel.reload() } private fun filterNew(list: List): List { if (list.isNotEmpty()) { lastNew = list[0].aid if (list[0].aid.toInt() == PrefsUtil.recentLastHiddenNew) return emptyList() } return list } override fun onReselect() { EAHelper.enter1("R") } } ================================================ FILE: app/src/main/java/knf/kuma/home/QueueAdapter.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.database.CacheDB import knf.kuma.pojos.QueueObject import knf.kuma.queue.QueueActivity import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick import java.util.Locale class QueueAdapter(val fragment: HomeFragment) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@QueueAdapter.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] noCrash { holder.img.load(PatternUtil.getCover(item.chapter.aid)) holder.title.text = item.chapter.name holder.type?.text = String.format(Locale.getDefault(), if (item.count == 1) "%d episodio" else "%d episodios", item.count) } holder.root.onClick { try { QueueActivity.open(fragment.context, item.chapter.aid) } catch (e: Exception) { doAsync { CacheDB.INSTANCE.queueDAO().allRaw.forEach { try { it.chapter.aid } catch (e: Exception) { CacheDB.INSTANCE.queueDAO().remove(it) } } syncData { queue() } } } } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/QueueAdapterMaterial.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.database.CacheDB import knf.kuma.pojos.QueueObject import knf.kuma.queue.QueueActivityMaterial import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick import java.util.Locale class QueueAdapterMaterial(val fragment: HomeFragmentMaterial) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@QueueAdapterMaterial.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card_material)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] noCrash { holder.img.load(PatternUtil.getCover(item.chapter.aid)) holder.title.text = item.chapter.name holder.type?.text = String.format(Locale.getDefault(), if (item.count == 1) "%d episodio" else "%d episodios", item.count) } holder.root.onClick { try { QueueActivityMaterial.open(fragment.context, item.chapter.aid) } catch (e: Exception) { doAsync { CacheDB.INSTANCE.queueDAO().allRaw.forEach { try { it.chapter.aid } catch (e: Exception) { CacheDB.INSTANCE.queueDAO().remove(it) } } syncData { queue() } } } } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/RecentsAdapter.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.backup.firestore.syncData import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.custom.SeenAnimeOverlay import knf.kuma.database.CacheDB import knf.kuma.pojos.RecentObject import knf.kuma.pojos.SeenObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick import org.jetbrains.anko.sdk27.coroutines.onLongClick class RecentsAdapter(val fragment: HomeFragment, private val isLarge: Boolean = true, private val showSeen: Boolean = true) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@RecentsAdapter.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(if (isLarge) R.layout.item_fav_grid_card else R.layout.item_fav_grid_card_simple)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { if (list.isEmpty()) return val item = list[position] holder.img.load(item.img) holder.title.text = item.name holder.type?.text = item.chapter holder.root.onClick { if (item.animeObject != null) { ActivityAnime.open(fragment, item.animeObject, holder.img) } else { fragment.lifecycleScope.launch(Dispatchers.Main) { val animeObject = withContext(Dispatchers.IO) { CacheDB.INSTANCE.animeDAO().getByAid(item.aid) } if (animeObject != null) { ActivityAnime.open(fragment, animeObject, holder.img) } else { ActivityAnime.open(fragment, item, holder.img) } } } } if (showSeen) { holder.seenOverlay.setSeen(item.isSeen, false) holder.root.onLongClick(returnValue = true) { if (item.isSeen) { doAsync { CacheDB.INSTANCE.seenDAO().deleteChapter(item.aid, item.chapter) } item.isSeen = false holder.seenOverlay.setSeen(seen = false, animate = true) } else { doAsync { CacheDB.INSTANCE.seenDAO().addChapter(SeenObject.fromRecent(item)) } item.isSeen = true holder.seenOverlay.setSeen(seen = true, animate = true) } syncData { seen() } } } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val seenOverlay: SeenAnimeOverlay by itemView.bind(R.id.seenOverlay) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/RecentsAdapterMaterial.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.custom.SeenAnimeOverlay import knf.kuma.database.CacheDB import knf.kuma.pojos.RecentObject import knf.kuma.pojos.SeenObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick import org.jetbrains.anko.sdk27.coroutines.onLongClick class RecentsAdapterMaterial(val fragment: HomeFragmentMaterial, private val isLarge: Boolean = true, private val showSeen: Boolean = true) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@RecentsAdapterMaterial.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(if (isLarge) R.layout.item_fav_grid_card_material else R.layout.item_fav_grid_card_simple_material)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { if (list.isEmpty()) return val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.name holder.type?.text = item.chapter holder.root.onClick { if (item.animeObject != null) { ActivityAnimeMaterial.open(fragment, item.animeObject, holder.img) } else { fragment.lifecycleScope.launch(Dispatchers.Main) { val animeObject = withContext(Dispatchers.IO) { CacheDB.INSTANCE.animeDAO().getByAid(item.aid) } if (animeObject != null) { ActivityAnimeMaterial.open(fragment, animeObject, holder.img) } else { ActivityAnimeMaterial.open(fragment, item, holder.img) } } } } if (showSeen) { holder.seenOverlay.setSeen(item.isSeen, false) holder.root.onLongClick(returnValue = true) { if (item.isSeen) { doAsync { CacheDB.INSTANCE.seenDAO().deleteChapter(item.aid, item.chapter) } item.isSeen = false holder.seenOverlay.setSeen(seen = false, animate = true) } else { doAsync { CacheDB.INSTANCE.seenDAO().addChapter(SeenObject.fromRecent(item)) } item.isSeen = true holder.seenOverlay.setSeen(seen = true, animate = true) } syncData { seen() } } } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val seenOverlay: SeenAnimeOverlay by itemView.bind(R.id.seenOverlay) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/RecommendedAdapter.kt ================================================ package knf.kuma.home import android.app.Activity import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.recommended.AnimeShortObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class RecommendedAdapter(val activity: Activity?) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@RecommendedAdapter.list = list.transform() doOnUIGlobal { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.name holder.type?.text = item.type holder.root.onClick { ActivityAnime.open(activity, item, holder.img, true, true) } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/RecommendedAdapterMaterial.kt ================================================ package knf.kuma.home import android.app.Activity import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.recommended.AnimeShortObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class RecommendedAdapterMaterial(val activity: Activity?) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@RecommendedAdapterMaterial.list = list.transform() doOnUIGlobal { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card_material)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.name holder.type?.text = item.type holder.root.onClick { ActivityAnimeMaterial.open(activity, item, holder.img, true, true) } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/SearchAdapter.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.search.SearchAdvObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class SearchAdapter(val fragment: HomeFragment) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@SearchAdapter.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.name holder.type?.text = item.type holder.root.onClick { ActivityAnime.open(fragment, item, holder.img, true, true) } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/SearchAdapterMaterial.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.commons.transform import knf.kuma.search.SearchAdvObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class SearchAdapterMaterial(val fragment: HomeFragmentMaterial) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@SearchAdapterMaterial.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card_material)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.name holder.type?.text = item.type holder.root.onClick { ActivityAnimeMaterial.open(fragment, item, holder.img, true, true) } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView? by itemView.optionalBind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/home/StaffRecommendations.kt ================================================ package knf.kuma.home import knf.kuma.database.CacheDB import knf.kuma.directory.DirObject import knf.kuma.search.SearchAdvObject import org.jetbrains.anko.doAsync object StaffRecommendations { private val recommendations: List = listOf( "2928", "2597", "1279", "1615", "363", "1706", "2950", "1182", "2479", "2478", "29", "30", "854", "2508", "2702", "1976", "2526", "5", "1290", "2852", "937", "3043", "2091", "1165", "3105", "2104", "108", "117", "996", "92", "1739", "1740", "181", "2671", "727", "1632", "2687", "3106", "1487", "1488", "1019", "460", "1493", "1494", "918", "1294", "2485", "2833", "2340", "3033", "1008", "869", "1286", "1791", "1044", "801", "901", "1048", "2279", "292", "28", "284", "2305", "1899", "2834", "349", "1627", "312", "277", "1497", "711", "150", "2748", "2901", "2639", "2860", "638", "773", "2959", "263", "1618", "1897", "2290", "125", "1324", "2533", "2636", "2789", "2395", "1028", "904", "721", "1753", "2794", "1813", "3104", "1912", "853", "661", "840", "2983", "310", "151", "863", "2382", "2599", "992", "126", "609", "127", "787", "880", "128", "704", "1272", "31", "1347", "129", "1554", "1555", "419", "1082", "2094", "1320", "1634", "1629", "8", "878", "1635", "2454", "1213", "2229", "1638", "1443", "2150", "1486", "465", "640", "966", "967", "1095", "1474", "2659", "2126", "978", "13", "2602", "2848", "1325", "364", "556", "1064", "1132", "557", "1322", "1930", "189", "1209", "1427", "1001", "1681", "1617", "353", "953", "951", "952", "80", "860", "3001", "1222", "897", "1195", "1092", "1179", "1091", "133", "417", "2954", "134", "1429", "42", "1196", "589", "366", "1958", "862", "2660", "2802", "2874", "2984", "1909", "2846", "1135", "1132", "9", "1139", "60", "593", "2661", "2652", "1237", "1915", "1297", "2287", "2767", "2111", "2840", "1316", "6", "2100", "1576", "866", "849", "91", "719", "1031", "1870", "2947", "2998", "778", "2762", "1900", "93", "2245", "2247", "174", "2491", "2657", "338", "986", "2295", "2233", "1428", "26", "27", "1318", "2903", "49", "1620", "2536", "2895", "3065", "393", "40", "39", "10", "2588", "1127", "118", "119", "299", "267", "268", "1780", "3113", "104", "639", "872", "1628", "1608", "1218", "34", "2496", "1793", "2457", "2876", "1002", "2089", "226", "1208", "690", "2740", "2543", "2446", "2586", "3137", "3011", "135", "136", "2110" ) fun randomIds(count: Int): List = recommendations.shuffled().take(count) fun createList(onCreate: (list: List) -> Unit) { doAsync { onCreate(CacheDB.INSTANCE.animeDAO().animesWithIDRandom(recommendations)) } } fun createDirList(onCreate: (list: List) -> Unit) { doAsync { onCreate(CacheDB.INSTANCE.animeDAO().animesDirWithIDRandom(recommendations)) } } } ================================================ FILE: app/src/main/java/knf/kuma/home/UpdateableAdapter.kt ================================================ package knf.kuma.home import androidx.recyclerview.widget.RecyclerView abstract class UpdateableAdapter : RecyclerView.Adapter() { abstract fun updateList(list: List) } ================================================ FILE: app/src/main/java/knf/kuma/home/WaitingAdapter.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.transform import knf.kuma.pojos.SeeingObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class WaitingAdapter(val fragment: HomeFragment) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@WaitingAdapter.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card_simple)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { noCrash { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.title holder.root.onClick { ActivityAnime.open(fragment.activity, item) } } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) } } ================================================ FILE: app/src/main/java/knf/kuma/home/WaitingAdapterMaterial.kt ================================================ package knf.kuma.home import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.transform import knf.kuma.pojos.SeeingObject import org.jetbrains.anko.doAsync import org.jetbrains.anko.sdk27.coroutines.onClick class WaitingAdapterMaterial(val fragment: HomeFragmentMaterial) : UpdateableAdapter() { private var list: List = emptyList() override fun updateList(list: List) { doAsync { this@WaitingAdapterMaterial.list = list.transform() fragment.doOnUI { notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder = RecentViewHolder(parent.inflate(R.layout.item_fav_grid_card_simple_material)) override fun getItemCount(): Int = list.size override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { noCrash { val item = list[position] holder.img.load(PatternUtil.getCover(item.aid)) holder.title.text = item.title holder.root.onClick { ActivityAnimeMaterial.open(fragment.activity, item) } } } class RecentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) } } ================================================ FILE: app/src/main/java/knf/kuma/jobscheduler/BackUpWork.kt ================================================ package knf.kuma.jobscheduler import android.content.Context import androidx.work.BackoffPolicy import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters import knf.kuma.App import knf.kuma.backup.Backups import knf.kuma.commons.PrefsUtil import knf.kuma.commons.safeContext import knf.kuma.pojos.AutoBackupObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.concurrent.TimeUnit class BackUpWork(val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) { override fun doWork(): Result { val service = Backups.createService() return if (service?.isLoggedIn == true) { val backupObject = runBlocking(Dispatchers.IO) { service.search(Backups.keyAutoBackup) } if (backupObject != null) { if (backupObject == AutoBackupObject(context)) Backups.backupAll() else WorkManager.getInstance(context).cancelAllWorkByTag(TAG) } Result.success() } else Result.failure() } companion object { internal const val TAG = "backupObj-job" fun checkInit() { GlobalScope.launch(Dispatchers.IO) { val service = Backups.createService() if (service?.isLoggedIn == true) { val obj = service.search(Backups.keyAutoBackup) as? AutoBackupObject val localObj = AutoBackupObject(App.context) if (obj == localObj) { val days = obj.value if (days.isNullOrBlank()) service.backup(localObj, Backups.keyAutoBackup) else if (days != PrefsUtil.autoBackupTime) { PrefsUtil.autoBackupTime = days reSchedule(days.toInt()) } } } } } fun reSchedule(days: Int) { WorkManager.getInstance(safeContext).cancelAllWorkByTag(TAG) if (days > 0) { PeriodicWorkRequestBuilder(days.toLong(), TimeUnit.DAYS, 1, TimeUnit.HOURS).apply { setConstraints(networkConnectedConstraints()) setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) addTag(TAG) }.build().enqueueUnique(TAG, ExistingPeriodicWorkPolicy.REPLACE) } } } } ================================================ FILE: app/src/main/java/knf/kuma/jobscheduler/DirUpdateWork.kt ================================================ package knf.kuma.jobscheduler import android.content.Context import androidx.preference.PreferenceManager import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.safeContext import knf.kuma.directory.DirectoryService import knf.kuma.directory.DirectoryUpdateService import xdroid.toaster.Toaster import java.util.concurrent.TimeUnit class DirUpdateWork(val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) { override fun doWork(): Result { if (PrefsUtil.isDirectoryFinished && !DirectoryUpdateService.isRunning && !DirectoryService.isRunning) DirectoryUpdateService.run(context) return Result.success() } companion object { const val TAG = "dir-update-work-unique" fun schedule(context: Context) { WorkManager.getInstance(context).cancelAllWorkByTag("dir-update-job") WorkManager.getInstance(context).cancelAllWorkByTag("dir-update-work") val preferences = PreferenceManager.getDefaultSharedPreferences(context) val time = (preferences.getString("dir_update_time", "7") ?: "7").toLong() if (PrefsUtil.isDirectoryFinished && time > 0) PeriodicWorkRequestBuilder(time, TimeUnit.DAYS, 1, TimeUnit.HOURS).apply { setConstraints(networkConnectedConstraints()) addTag(TAG) }.build().enqueueUnique(TAG, ExistingPeriodicWorkPolicy.KEEP) } fun reSchedule(value: Int) { if (value > 0) PeriodicWorkRequestBuilder(value.toLong(), TimeUnit.DAYS, 1, TimeUnit.HOURS).apply { setConstraints(networkConnectedConstraints()) addTag(TAG) }.build().enqueueUnique(TAG, ExistingPeriodicWorkPolicy.REPLACE) else WorkManager.getInstance(safeContext).cancelAllWorkByTag(TAG) } fun runNow() { if (Network.isConnected) { OneTimeWorkRequestBuilder().apply { addTag(TAG) setConstraints(networkConnectedConstraints()) }.build().enqueue() } else { Toaster.toast("Se necesita internet") } } } } ================================================ FILE: app/src/main/java/knf/kuma/jobscheduler/RecentsWork.kt ================================================ package knf.kuma.jobscheduler import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.preference.PreferenceManager import androidx.tvprovider.media.tv.PreviewChannelHelper import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ForegroundInfo import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters import com.bumptech.glide.Glide import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.R import knf.kuma.commons.DesignUtils import knf.kuma.commons.Network import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.create import knf.kuma.commons.isFullMode import knf.kuma.commons.jsoupCookies import knf.kuma.database.CacheDB import knf.kuma.download.DownloadDialogActivity import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.NotificationObj import knf.kuma.pojos.RecentObject import knf.kuma.pojos.Recents import knf.kuma.recents.RecentsNotReceiver import knf.kuma.search.SearchAdvObject import knf.kuma.tv.ChannelUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.anko.notificationManager import pl.droidsonroids.jspoon.Jspoon import java.util.concurrent.TimeUnit import kotlin.random.Random class RecentsWork(val context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) { private val RECENTS_GROUP = "recents-group" private val recentsDAO = CacheDB.INSTANCE.recentsDAO() private val favsDAO = CacheDB.INSTANCE.favsDAO() private val seeingDAO = CacheDB.INSTANCE.seeingDAO() private val animeDAO = CacheDB.INSTANCE.animeDAO() private val notificationDAO = CacheDB.INSTANCE.notificationDAO() private val manager: NotificationManager by lazy { context.notificationManager } private val summaryBroadcast: Intent get() = Intent(context, RecentsNotReceiver::class.java).putExtra("mode", 1) override suspend fun doWork(): Result { if (!Network.isConnected) return Result.success().also { Log.e("Recents", "No Network") } //setForeground(createForegroundInfo()) try { val recents = withContext(Dispatchers.IO) { Jspoon.create().adapter(Recents::class.java) .fromHtml(jsoupCookies("https://www3.animeflv.net/").get().outerHtml()) } val objects = RecentObject.create(recents.list ?: listOf()) for ((i, recentObject) in objects.withIndex()) recentObject.key = i notifyChannel(objects) val local = recentsDAO.all if (local.isEmpty() && !BuildConfig.DEBUG) return Result.success() if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("notify_favs", false)) { notifyFavChaps(local, objects) } else { notifyAllChaps(local, objects) } recentsDAO.setCache(objects) return Result.success() } catch (e: Exception) { e.printStackTrace() return Result.failure() } } private fun createForegroundInfo(): ForegroundInfo = ForegroundInfo( Random.nextInt(1000, 9999), NotificationCompat.Builder(context, CHANNEL_RECENTS) .setSmallIcon(R.drawable.ic_recents_group) .setColor(ContextCompat.getColor(context, R.color.colorAccent)) .setContentText("Buscando nuevos episodios") .setProgress(100, 0, true) .build() ) private fun notifyTest() { manager.notify( Random.nextInt(), NotificationCompat.Builder(context, CHANNEL_RECENTS) .setSmallIcon(R.drawable.ic_recents_group) .setColor(ContextCompat.getColor(context, R.color.colorAccent)) .setContentText("Test notification, ${System.currentTimeMillis()}") .build() ) } @Throws(Exception::class) private fun notifyAllChaps( local: MutableList, objects: MutableList ) { for (recentObject in objects) { if (!local.contains(recentObject)) { notifyRecent(recentObject) } } } @Throws(Exception::class) private fun notifyFavChaps( local: MutableList, objects: MutableList ) { for (recentObject in objects) { if (!local.contains(recentObject) && (favsDAO.isFav(Integer.parseInt(recentObject.aid)) || seeingDAO.isSeeing( recentObject.aid )) ) { notifyRecent(recentObject) } } } private fun notifyChannel(objects: List) { if (!context.resources.getBoolean(R.bool.isTv) || !PrefsUtil.tvRecentsChannelCreated) return val lastNotified = objects.indexOf(objects.find { it.eid == PrefsUtil.tvRecentsChannelLastEid }) if (lastNotified != 0) { with(PreviewChannelHelper(context)) { PrefsUtil.tvRecentsChannelIds?.forEach { deletePreviewProgram(it.toLong()) } } val newIds = mutableSetOf() objects.forEach { newIds.add(ChannelUtils.addProgram(context, it).toString()) } PrefsUtil.tvRecentsChannelIds = newIds PrefsUtil.tvRecentsChannelLastEid = objects.first().eid } } @Throws(Exception::class) private fun notifyRecent(recentObject: RecentObject) { val animeObject = getAnime(recentObject) val obj = NotificationObj( "${recentObject.aid}${recentObject.chapter}".hashCode(), NotificationObj.RECENT ) val notification = NotificationCompat.Builder(context, CHANNEL_RECENTS).create { setSmallIcon(R.drawable.ic_new_recent) color = ContextCompat.getColor(context, R.color.colorAccent) setContentTitle(recentObject.name) setContentText(recentObject.chapter) priority = NotificationCompat.PRIORITY_MAX val tone = FileAccessHelper.toneFile if (tone.exists()) setSound( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val uri: Uri = FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", tone ) context.grantUriPermission( "com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) uri } else Uri.fromFile(tone) ) setLargeIcon(getBitmap(recentObject)) setAutoCancel(true) setOnlyAlertOnce(true) setContentIntent( PendingIntent.getActivity( context, System.currentTimeMillis().toInt(), getAnimeIntent(animeObject, obj), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) setDeleteIntent( PendingIntent.getBroadcast( context, System.currentTimeMillis().toInt(), obj.getBroadcast(context), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) if (isFullMode && !PrefsUtil.isFamilyFriendly) addAction( android.R.drawable.stat_sys_download_done, "Acciones", PendingIntent.getActivity( context, System.currentTimeMillis().toInt(), getChapIntent(recentObject, obj), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) setGroup(RECENTS_GROUP) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) }.build() notificationDAO.add(obj) manager.notify(obj.key, notification) notifySummary() } private fun getBitmap(recentObject: RecentObject): Bitmap? { return try { if (PrefsUtil.showRecentImage) Glide.with(context).asBitmap().load(PatternUtil.getCover(recentObject.aid)).submit().get() else null } catch (e: Exception) { null } } @Throws(Exception::class) private fun getAnime(recentObject: RecentObject): SearchAdvObject { var animeObject: SearchAdvObject? = animeDAO.getByAid(recentObject.aid) if (animeObject == null) { val tmp = AnimeObject(recentObject.anime, Jspoon.create().adapter(AnimeObject.WebInfo::class.java).fromHtml(jsoupCookies(recentObject.anime).get().outerHtml())) animeObject = SearchAdvObject().apply { key = tmp.key name = tmp.name link = tmp.link aid = tmp.aid type = tmp.type img = tmp.img } animeDAO.insert(tmp) } return animeObject } private fun getAnimeIntent(animeObject: SearchAdvObject, notificationObj: NotificationObj): Intent { return Intent(context, DesignUtils.infoClass) .setData(Uri.parse(animeObject.link)) .putExtras(notificationObj.getBroadcast(context)) .putExtra("title", animeObject.name) .putExtra("aid", animeObject.aid) .putExtra("img", animeObject.img) .putExtra("notification", true) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } private fun getChapIntent(recentObject: RecentObject, notificationObj: NotificationObj): Intent { return Intent(context, DownloadDialogActivity::class.java) .setData(Uri.parse(recentObject.url)) .putExtras(notificationObj.getBroadcast(context)) .putExtra("notification", true) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } private fun notifySummary() { val notification = NotificationCompat.Builder(context, CHANNEL_RECENTS) .setSmallIcon(R.drawable.ic_recents_group) .setColor(ContextCompat.getColor(context, R.color.colorAccent)) .setContentTitle("Nuevos capitulos") .setContentText("Hay nuevos capitulos recientes!!") .setGroupSummary(true) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setGroup(RECENTS_GROUP) .setAutoCancel(true) .setContentIntent( PendingIntent.getActivity( context, 0, Intent(context, DesignUtils.mainClass), PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) .setDeleteIntent( PendingIntent.getBroadcast( context, System.currentTimeMillis().toInt(), summaryBroadcast, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) .build() if (PrefsUtil.isGroupingEnabled) manager.notify(KEY_SUMMARY, notification) } companion object { const val CHANNEL_RECENTS = "channel.RECENTS" const val KEY_SUMMARY = 55971 internal const val TAG = "recents-job" fun schedule(context: Context) { val preferences = PreferenceManager.getDefaultSharedPreferences(context) val time = (preferences.getString("recents_time", "1") ?: "1").toInt() * 15 PeriodicWorkRequestBuilder( time.coerceAtLeast(15).toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES ).apply { setInitialDelay(15L, TimeUnit.MINUTES) //setConstraints(networkConnectedConstraints()) //setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) addTag(TAG) }.build().enqueueUnique(TAG, ExistingPeriodicWorkPolicy.KEEP) /*WorkManager.getInstance(context).getWorkInfosByTagLiveData(TAG).let { ld -> lateinit var observer: Observer> doOnUI { ld.observeForever(Observer> { ld.removeObserver(observer) if (it.isEmpty()) doAsync { val preferences = PreferenceManager.getDefaultSharedPreferences(context) val time = (preferences.getString("recents_time", "1") ?: "1").toInt() * 15 if (time > 0) PeriodicWorkRequestBuilder(time.coerceAtLeast(15).toLong(), TimeUnit.MINUTES).apply { setInitialDelay(15L, TimeUnit.MINUTES) //setConstraints(networkConnectedConstraints()) //setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) addTag(TAG) }.build().enqueueUnique(TAG, ExistingPeriodicWorkPolicy.REPLACE) } }.also { observer = it }) } }*/ } fun reSchedule(time: Int) { WorkManager.getInstance(App.context).cancelAllWorkByTag(TAG) if (time > 0) PeriodicWorkRequestBuilder( time.coerceAtLeast(15).toLong(), TimeUnit.MINUTES ).apply { //setConstraints(networkConnectedConstraints()) //setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) addTag(TAG) }.build().enqueueUnique(TAG, ExistingPeriodicWorkPolicy.UPDATE) } fun run() = OneTimeWorkRequestBuilder().build().enqueue() } } ================================================ FILE: app/src/main/java/knf/kuma/jobscheduler/UpdateWork.kt ================================================ package knf.kuma.jobscheduler import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.Worker import androidx.work.WorkerParameters import knf.kuma.R import knf.kuma.commons.Network import knf.kuma.commons.isFullMode import knf.kuma.updater.UpdateActivity import org.jetbrains.anko.doAsync import org.jetbrains.anko.notificationManager import org.jsoup.Jsoup import java.util.concurrent.TimeUnit class UpdateWork(val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) { override fun doWork(): Result { if (Network.isConnected && isFullMode) try { val document = Jsoup.connect("https://raw.githubusercontent.com/jordyamc/UKIKU/master/version.num") .get() val nCode = Integer.parseInt(document.select("body").first().ownText().trim { it <= ' ' }) val sCode = PreferenceManager.getDefaultSharedPreferences(context) .getInt("last_notified_update", 0) if (nCode <= sCode) return Result.success() val oCode = context.packageManager.getPackageInfo(context.packageName, 0).versionCode if (nCode > oCode) { showNotification() PreferenceManager.getDefaultSharedPreferences(context).edit() .putInt("last_notified_update", nCode).apply() } } catch (e: Exception) { e.printStackTrace() } return Result.success() } private fun showNotification() { try { val notification = NotificationCompat.Builder(context, CHANNEL).apply { setSmallIcon(R.drawable.ic_not_update) setContentTitle("UKIKU") setContentText("Nueva versión disponible") setContentIntent( PendingIntent.getActivity( context, 5598, Intent(context, UpdateActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) color = ContextCompat.getColor(context, R.color.colorAccent) }.build() context.notificationManager.notify(954857, notification) } catch (e: Exception) { e.printStackTrace() } } companion object { const val TAG = "update-job" const val CHANNEL = "app-updater" fun schedule() { doAsync { PeriodicWorkRequestBuilder(6, TimeUnit.HOURS, 1, TimeUnit.HOURS).apply { setConstraints(networkConnectedConstraints()) addTag(TAG) }.build().enqueueUnique(TAG, ExistingPeriodicWorkPolicy.KEEP) } } } } ================================================ FILE: app/src/main/java/knf/kuma/jobscheduler/WorkExt.kt ================================================ package knf.kuma.jobscheduler import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager import androidx.work.WorkRequest import knf.kuma.App fun networkConnectedConstraints(): Constraints = Constraints.Builder().apply { setRequiredNetworkType(NetworkType.CONNECTED) }.build() fun WorkRequest.enqueue() = WorkManager.getInstance(App.context).enqueue(this) fun PeriodicWorkRequest.enqueueUnique(tag: String, type: ExistingPeriodicWorkPolicy) = WorkManager.getInstance(App.context).enqueueUniquePeriodicWork(tag, type, this) ================================================ FILE: app/src/main/java/knf/kuma/news/MaterialNewsActivity.kt ================================================ package knf.kuma.news import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.afollestad.materialdialogs.LayoutMode import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.bottomsheets.BottomSheet import com.afollestad.materialdialogs.bottomsheets.setPeekHeight import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.google.android.material.snackbar.Snackbar import ir.mahdiparastesh.chlm.SpacingItemDecoration import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.ads.showRandomInterstitial import knf.kuma.commons.EAHelper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.asPx import knf.kuma.commons.setSurfaceBars import knf.kuma.commons.showSnackbar import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityNewsMaterialBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MaterialNewsActivity : GenericActivity(), SwipeRefreshLayout.OnRefreshListener { val model: NewsViewModel by viewModels() private val binding by lazy { ActivityNewsMaterialBinding.inflate(layoutInflater) } val adapter: MaterialNewsAdapter by lazy { MaterialNewsAdapter(this) } var snack: Snackbar? = null override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) binding.toolbar.title = "Recientes" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } binding.refresh.setColorSchemeResources(EAHelper.getThemeColor(), EAHelper.getThemeColorLight(), R.color.colorPrimary) binding.refresh.setOnRefreshListener(this) binding.refresh.isRefreshing = true binding.recycler.adapter = adapter binding.recycler.addItemDecoration(SpacingItemDecoration(0, 20.asPx)) loadList() lifecycleScope.launch(Dispatchers.IO) { delay(500) binding.adContainer.implBanner(AdsType.NEWS_BANNER, true) } showRandomInterstitial(this, PrefsUtil.fullAdsExtraProbability) } private fun loadList() { snack?.dismiss() if (Network.isConnected) lifecycleScope.launch { NewsRepository.getNews(getCategory()) { isEmpty, cause -> if (isEmpty) { Log.e("News", "Error loading: $cause") binding.error.visibility = View.VISIBLE snack = binding.recycler.showSnackbar("Error al cargar noticias: $cause", Snackbar.LENGTH_INDEFINITE, "reintentar") { loadList() } } else { binding.error.visibility = View.GONE binding.recycler.scheduleLayoutAnimation() } runOnUiThread { binding.refresh.isRefreshing = false } }.collect { adapter.submitData(it) } } else { snack = binding.recycler.showSnackbar("Sin internet", Snackbar.LENGTH_INDEFINITE) } } private fun getCategory(): String { return when (model.selectedFilter) { 1 -> "noticias/anime" 2 -> "noticias/cultura-otaku" 3 -> "noticias/japon" 4 -> "noticias/live-action" 5 -> "noticias/manga" 6 -> "noticias/mercancia-de-anime" 7 -> "noticias/novelas-ligeras" 8 -> "noticias/videojuegos" 9 -> "noticias/resenas" else -> "noticias/" } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_news_filters, menu) return super.onCreateOptionsMenu(menu) } @SuppressLint("CheckResult") override fun onOptionsItemSelected(item: MenuItem): Boolean { MaterialDialog(this, BottomSheet(LayoutMode.WRAP_CONTENT)).show { lifecycleOwner(this@MaterialNewsActivity) title(text = "Filtros") setPeekHeight(99999999) listItemsSingleChoice( items = model.filtersList, initialSelection = model.selectedFilter, waitForPositiveButton = false ) { _, index, name -> binding.refresh.isRefreshing = true supportActionBar?.title = name model.selectedFilter = index loadList() } } return super.onOptionsItemSelected(item) } override fun onRefresh() { binding.refresh.isRefreshing = true loadList() } companion object { fun open(context: Context) { context.startActivity(Intent(context, MaterialNewsActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/news/MaterialNewsAdapter.kt ================================================ package knf.kuma.news import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.imageview.ShapeableImageView import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.commons.inflate import knf.kuma.commons.load import knf.kuma.commons.noCrash import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick class MaterialNewsAdapter(val activity: AppCompatActivity) : PagingDataAdapter(NewsItem.DIFF) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder = NewsViewHolder(parent.inflate(R.layout.item_news_material)) override fun onBindViewHolder(holder: NewsViewHolder, position: Int) { val item = getItem(position) if (item == null) holder.apply { root.setOnClickListener(null) image.setImageDrawable(null) progress.visibility = View.VISIBLE type.text = null title.text = null date.text = null } else holder.apply { root.onClick { AchievementManager.onNewsOpened() openNews(activity, item) } noCrash { image.load(item.image) } progress.visibility = View.GONE type.text = item.type title.text = item.title date.text = item.date } } class NewsViewHolder(view: View) : RecyclerView.ViewHolder(view) { val root: View = itemView.find(R.id.root) val image: ShapeableImageView = itemView.find(R.id.image) val progress: ProgressBar = itemView.find(R.id.progress) val type: TextView = itemView.find(R.id.type) val title: TextView = itemView.find(R.id.title) val date: TextView = itemView.find(R.id.date) } } ================================================ FILE: app/src/main/java/knf/kuma/news/NewsActivity.kt ================================================ package knf.kuma.news import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import ir.mahdiparastesh.chlm.SpacingItemDecoration import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.asPx import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityNewsBinding class NewsActivity : GenericActivity(), SwipeRefreshLayout.OnRefreshListener { private val binding by lazy { ActivityNewsBinding.inflate(layoutInflater) } val adapter: NewsAdapter by lazy { NewsAdapter(this) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) binding.toolbar.title = "Noticias" setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } binding.refresh.setColorSchemeResources(EAHelper.getThemeColor(), EAHelper.getThemeColorLight(), R.color.colorPrimary) binding.refresh.setOnRefreshListener(this) binding.refresh.isRefreshing = true binding.recycler.adapter = adapter binding.recycler.addItemDecoration(SpacingItemDecoration(0, 10.asPx)) NewsCreator.createNews().observe(this) { if (it == null || it.isEmpty()) binding.error.visibility = View.VISIBLE else { binding.error.visibility = View.GONE adapter.update(it) binding.recycler.scheduleLayoutAnimation() } binding.refresh.isRefreshing = false } if (!PrefsUtil.isNativeAdsEnabled) binding.adContainer.implBanner(AdsType.NEWS_BANNER) } override fun onRefresh() { binding.refresh.isRefreshing = true NewsCreator.reload() } override fun onDestroy() { super.onDestroy() NewsCreator.destroy() } companion object { fun open(context: Context) { context.startActivity(Intent(context, NewsActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/news/NewsAdapter.kt ================================================ package knf.kuma.news import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdCallback import knf.kuma.ads.AdCardItemHolder import knf.kuma.ads.AdsUtilsMob import knf.kuma.ads.implAdsNews import knf.kuma.commons.PrefsUtil import knf.kuma.commons.isSameContent import knf.kuma.commons.noCrashLet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick class NewsAdapter(val activity: AppCompatActivity) : RecyclerView.Adapter() { var list: List = listOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { if (viewType == 1) return AdCardItemHolder(parent, AdCardItemHolder.TYPE_NEWS).also { it.loadAd(activity.lifecycleScope, object : AdCallback { override fun getID(): String = AdsUtilsMob.NEWS_BANNER }, 500) } return NewsHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_news, parent, false)) } override fun getItemCount(): Int { return list.size } override fun getItemViewType(position: Int): Int { return noCrashLet { if (list[position] is AdNewsObject) 1 else 0 } ?: 1 } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val newsObject = list[position] if (holder is NewsHolder) { holder.metadata.text = newsObject.metaData() holder.title.text = newsObject.title holder.description.text = newsObject.description holder.card.onClick { AchievementManager.onNewsOpened() NewsCreator.openNews(activity, newsObject) } } } fun update(list: MutableList) { activity.lifecycleScope.launch(Dispatchers.IO) { if (PrefsUtil.isNativeAdsEnabled) list.implAdsNews() if (this@NewsAdapter.list isSameContent list) return@launch this@NewsAdapter.list = list launch(Dispatchers.Main) { notifyDataSetChanged() } } } class NewsHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val card: MaterialCardView = itemView.find(R.id.card) val metadata: TextView = itemView.find(R.id.metadata) val title: TextView = itemView.find(R.id.title) val description: TextView = itemView.find(R.id.description) } } ================================================ FILE: app/src/main/java/knf/kuma/news/NewsCreator.kt ================================================ package knf.kuma.news import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import android.os.Build import android.text.Html import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import knf.kuma.ads.AdCallback import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.toast import org.jetbrains.anko.doAsync import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.jsoup.parser.Parser import org.jsoup.select.Elements import java.text.SimpleDateFormat import java.util.Locale import java.util.regex.Pattern object NewsCreator { private val liveData = MutableLiveData?>() fun createNews(): LiveData?> { reload() return liveData } fun reload() { doAsync { try { val document = Jsoup.connect("https://somoskudasai.com/feed/").parser(Parser.xmlParser()).get() val items = document.select("item") val news = mutableListOf() items.forEach { val title = it.select("title").first().noCDText() val link = it.select("link").first().noCDText() val author = it.select("dc|creator").first().noCDText() val date = it.select("pubDate").first().noCDText() val categories = it.select("category").getAllStringNoCD() val description = it.select("description").first().noCDText() val content = it.select("content|encoded").first().noCDHtml() news.add(NewsObject( title, link, date, author, categories, description, content )) } doOnUIGlobal { liveData.value = news } } catch (e: Exception) { e.printStackTrace() doOnUIGlobal { liveData.value = null } } } } fun destroy() { doOnUIGlobal { liveData.value = null } } fun openNews(activity: AppCompatActivity, newsObject: NewsObject) { try { //CustomTabsIntent.Builder().build().launchUrl(activity, Uri.parse(newsObject.link)) NewsDialog.show(activity, newsObject.link) } catch (e: ActivityNotFoundException) { try { activity.startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(newsObject.link))) } catch (anfe: ActivityNotFoundException) { "No se encontró ningun navegador para abrir noticia".toast() } } catch (ex: Exception) { "Error al abrir noticia".toast() } } private fun Element.noCDText(): String { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) Html.fromHtml(text().cdRemoved(), Html.FROM_HTML_MODE_LEGACY).toString() else Html.fromHtml(text().cdRemoved()).toString() } private fun Element.noCDHtml(): String { return text().cdRemoved() } private fun Elements.getAllStringNoCD(): List { val list = mutableListOf() this.forEach { list.add(it.noCDText()) } return list } private fun String.cdRemoved(): String { if (!this.trim().startsWith("" else "La entrada " else "") val matcher = pattern.matcher(this) matcher.find() return matcher.group(1).trim() } } open class NewsObject( val title: String, val link: String, val date: String, val author: String, val categories: List, val description: String, val content: String ) { fun metaData(): String { val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss Z", Locale.ENGLISH) val formated = dateFormat.parse(date) //return "$author — ${TimeAgo.using(formated.time)}" val simpleDate = SimpleDateFormat("EEE, dd MMM hh:mmaa", Locale.getDefault()) return "$author — ${simpleDate.format(formated)}" } override fun equals(other: Any?): Boolean { return other is NewsObject && other.link == link } override fun hashCode(): Int { return link.hashCode() } } class AdNewsObject(private val adId: String) : NewsObject("", "", "", "", emptyList(), "", ""), AdCallback { override fun getID(): String = adId } ================================================ FILE: app/src/main/java/knf/kuma/news/NewsDialog.kt ================================================ package knf.kuma.news import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.OnLifecycleEvent import com.google.android.material.bottomsheet.BottomSheetDialogFragment import knf.kuma.R import knf.kuma.commons.doOnUI import org.jetbrains.anko.find class NewsDialog : BottomSheetDialogFragment(), LifecycleObserver { private var link: String = "about:blank" @SuppressLint("SetJavaScriptEnabled") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.lay_news, container, false) doOnUI { rootView.find(R.id.webview).apply { setInitialScale(1) settings.useWideViewPort = true settings.loadWithOverviewMode = true settings.javaScriptEnabled = true webViewClient = WebViewClient() loadUrl(link) } } return rootView } fun setUpOwner(owner: LifecycleOwner) { owner.lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPaused() { safeDismiss() } fun safeShow(manager: FragmentManager, tag: String) { try { show(manager, tag) } catch (e: Exception) { // } } private fun safeDismiss() { try { dismiss() } catch (e: Exception) { // } } companion object { fun show(activity: AppCompatActivity, link: String) { NewsDialog().apply { this.link = link setUpOwner(activity) }.safeShow(activity.supportFragmentManager, "BottomSheetDialog") } fun show(fragment: Fragment, link: String) { NewsDialog().apply { this.link = link setUpOwner(fragment) }.safeShow(fragment.childFragmentManager, "BottomSheetDialog") } } } ================================================ FILE: app/src/main/java/knf/kuma/news/NewsObjects.kt ================================================ package knf.kuma.news import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ClipboardManager import android.content.Intent import android.net.Uri import androidx.annotation.Keep import androidx.appcompat.app.AppCompatActivity import androidx.core.content.getSystemService import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.PagingSource import androidx.paging.PagingState import androidx.recyclerview.widget.DiffUtil import knf.kuma.commons.NoSSLOkHttpClient import knf.kuma.commons.safeContext import knf.kuma.commons.toast import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import org.jsoup.nodes.Element import pl.droidsonroids.jspoon.ElementConverter import pl.droidsonroids.jspoon.annotation.Selector import pl.droidsonroids.retrofit2.JspoonConverterFactory import retrofit2.Call import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.http.GET import retrofit2.http.Path @Keep class NewsItem { @Selector("header h3", defValue = "") lateinit var title: String @Selector("header p", defValue = "") lateinit var type: String @Selector(":root", converter = DateConverter::class) lateinit var date: String @Selector(":root", converter = ImageConverter::class) lateinit var image: String @Selector("header h3 a", attr = "href", defValue = "") lateinit var link: String class ImageConverter : ElementConverter { override fun convert(node: Element, selector: Selector): String { val image = node.select("figure img,figure amp-img").ifEmpty { return "" }.first() return when { image.hasAttr("data-src") -> image.attr("data-src") image.hasAttr("src") -> image.attr("src") else -> "" } } } class DateConverter : ElementConverter { override fun convert(node: Element, selector: Selector): String { val date = node.select("footer > span:last-of-type").first() return date.text().substringBeforeLast(" por ") } } companion object { val DIFF = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: NewsItem, newItem: NewsItem): Boolean = oldItem.link == newItem.link override fun areContentsTheSame(oldItem: NewsItem, newItem: NewsItem): Boolean = oldItem.title == newItem.title && oldItem.date == newItem.date } } } class NewsPage { @Selector("article:has(figure):has(h3):not(.logo):not(.grid)") var newsList: List = emptyList() } class NewsDataSource(private val newsFactory: NewsFactory, val category: String, val onInit: (isEmpty: Boolean, cause: String?) -> Unit) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? = state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1) } override suspend fun load(params: LoadParams): LoadResult { val page = params.key?:1 try { val response = withContext(Dispatchers.IO) { when (category) { "noticias/" -> newsFactory.getLatestPage().execute() else -> newsFactory.getNewsPage(category, page).execute() } } if (response.isSuccessful){ response.body()?.let { if (page == 1) onInit(false, null) return LoadResult.Page(it.newsList, null, if (it.newsList.size < 12 || category == "noticias/") null else page + 1) } ?: run{ if (page == 1) onInit(true, "Empty body") } } val errorString = withContext(Dispatchers.IO) { response.errorBody()?.string() } if (page == 1) onInit(true, "$errorString") return LoadResult.Error(IllegalStateException()) }catch (e:Exception){ if (page == 1) { onInit(true, "${e.message}") safeContext.getSystemService()?.apply { setPrimaryClip(ClipData.newPlainText("News", e.stackTraceToString())) } } return LoadResult.Error(e) } } } interface NewsFactory { @GET("{category}/{page}/") fun getNewsPage(@Path("category") category: String, @Path("page") page: Int): Call @GET("noticias/") fun getLatestPage(): Call } object NewsRepository { fun getNews(category: String, onInit: (isEmpty: Boolean, cause: String?) -> Unit): Flow> { return Pager( config = PagingConfig(12), pagingSourceFactory = { NewsDataSource(getFactory(), category, onInit) } ).flow } private fun getFactory(): NewsFactory { val retrofit = Retrofit.Builder() .baseUrl("https://somoskudasai.com/") .client(NoSSLOkHttpClient.get()) .addConverterFactory(JspoonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() return retrofit.create(NewsFactory::class.java) } } fun openNews(activity: AppCompatActivity, newsItem: NewsItem) { try { NewsDialog.show(activity, newsItem.link) //CommentariesDialog.show(activity, newsItem.link) //CustomTabsIntent.Builder().build().launchUrl(activity, newsItem.link.toUri()) } catch (e: ActivityNotFoundException) { try { activity.startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(newsItem.link))) } catch (anfe: ActivityNotFoundException) { "No se encontró ningun navegador para abrir noticia".toast() } } catch (ex: Exception) { "Error al abrir noticia".toast() } } ================================================ FILE: app/src/main/java/knf/kuma/news/NewsViewModel.kt ================================================ package knf.kuma.news import androidx.lifecycle.ViewModel class NewsViewModel : ViewModel(){ var selectedFilter = 0 val filtersList: List = mutableListOf().apply{ add("Recientes") add("Anime") add("Cultura Otaku") add("Japón") add("Live Action") add("Manga") add("Mercancía / Figuras") add("Novelas Ligeras") add("VideoJuegos") add("Reseñas") } } ================================================ FILE: app/src/main/java/knf/kuma/player/AudioFocusWrapper.kt ================================================ package knf.kuma.player import android.annotation.TargetApi import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.media.AudioAttributesCompat import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.SimpleExoPlayer /** * Wrapper around a [SimpleExoPlayer] simplifies playback by automatically handling * audio focus using [AudioFocusRequest] on Oreo+ devices, and an * [AudioManager.OnAudioFocusChangeListener] on previous versions. */ class AudioFocusWrapper( private val audioAttributes: AudioAttributesCompat, private val audioManager: AudioManager, private val player: ExoPlayer ) : ExoPlayer by player { private var shouldPlayWhenReady = false private val audioFocusListener = AudioManager.OnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_GAIN -> { if (shouldPlayWhenReady || player.playWhenReady) { player.playWhenReady = true player.volume = MEDIA_VOLUME_DEFAULT } shouldPlayWhenReady = false } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { if (player.playWhenReady) { player.volume = MEDIA_VOLUME_DUCK } } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { shouldPlayWhenReady = player.playWhenReady player.playWhenReady = false } AudioManager.AUDIOFOCUS_LOSS -> { abandonAudioFocus() } } } @get:RequiresApi(Build.VERSION_CODES.O) private val audioFocusRequest by lazy { buildFocusRequest() } override fun setPlayWhenReady(playWhenReady: Boolean) { if (playWhenReady) requestAudioFocus() else abandonAudioFocus() } private fun requestAudioFocus() { val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { requestAudioFocusOreo() } else { @Suppress("deprecation") audioManager.requestAudioFocus(audioFocusListener, audioAttributes.legacyStreamType, AudioManager.AUDIOFOCUS_GAIN) } // Call the listener whenever focus is granted - even the first time! if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { shouldPlayWhenReady = true audioFocusListener.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN) } else { Log.e("Player", "Playback not started: Audio focus request denied") } } private fun abandonAudioFocus() { player.playWhenReady = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { abandonAudioFocusOreo() } else { @Suppress("deprecation") audioManager.abandonAudioFocus(audioFocusListener) } } @RequiresApi(Build.VERSION_CODES.O) private fun requestAudioFocusOreo(): Int = audioManager.requestAudioFocus(audioFocusRequest) @RequiresApi(Build.VERSION_CODES.O) private fun abandonAudioFocusOreo() = audioManager.abandonAudioFocusRequest(audioFocusRequest) @TargetApi(Build.VERSION_CODES.O) private fun buildFocusRequest(): AudioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(audioAttributes.unwrap() as AudioAttributes) .setOnAudioFocusChangeListener(audioFocusListener) .build() } private const val MEDIA_VOLUME_DEFAULT = 1.0f private const val MEDIA_VOLUME_DUCK = 0.2f ================================================ FILE: app/src/main/java/knf/kuma/player/BVListener.kt ================================================ package knf.kuma.player import android.content.Context import android.graphics.Point import android.media.AudioManager import android.view.MotionEvent import android.view.View import androidx.appcompat.app.AppCompatActivity class BVListener(val activity: AppCompatActivity) : View.OnTouchListener { private val audioManager: AudioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager private val screenSize: Point private val sWidth: Int private val sHeight: Int private var intLeft: Boolean = false private var intRight: Boolean = false private var downX: Float = 0.toFloat() private var downY: Float = 0.toFloat() private var diffX: Long = 0 private var diffY: Long = 0 private val window = activity.window private val params = activity.window.attributes private var currentBrightness = params.screenBrightness init { val display = activity.windowManager.defaultDisplay screenSize = Point() display.getSize(screenSize) sWidth = screenSize.x sHeight = screenSize.y } override fun onTouch(v: View?, event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { //touch is start downX = event.x downY = event.y if (event.x < sWidth / 2) { //here check touch is screen left or right side intLeft = true intRight = false } else if (event.x > sWidth / 2) { //here check touch is screen left or right side intLeft = false intRight = true } v?.performClick() } MotionEvent.ACTION_UP, MotionEvent.ACTION_MOVE -> { if (event.action == MotionEvent.ACTION_UP) v?.performClick() //finger move to screen //val x2 = event.x val y2 = event.y diffX = Math.ceil((event.x - downX).toDouble()).toLong() diffY = Math.ceil((event.y - downY).toDouble()).toLong() if (Math.abs(diffY) > Math.abs(diffX)) { if (intLeft) { //if left its for brightness if (downY < y2) { //down swipe brightness decrease currentBrightness -= 0.1f if (currentBrightness < 0) currentBrightness = 0f params.screenBrightness = currentBrightness window.attributes = params } else if (downY > y2) { //up swipe brightness increase currentBrightness += 0.1f if (currentBrightness > 1) currentBrightness = 1f params.screenBrightness = currentBrightness window.attributes = params } } else if (intRight) { //if right its for audio if (downY < y2) { //down swipe volume decrease audioManager.adjustVolume(AudioManager.ADJUST_LOWER, AudioManager.FLAG_PLAY_SOUND) } else if (downY > y2) { //up swipe volume increase audioManager.adjustVolume(AudioManager.ADJUST_RAISE, AudioManager.FLAG_PLAY_SOUND) } } } } } return true } } ================================================ FILE: app/src/main/java/knf/kuma/player/CustomExoPlayer.kt ================================================ package knf.kuma.player import android.annotation.TargetApi import android.app.PictureInPictureParams import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle import android.view.View import android.webkit.MimeTypeMap import android.widget.TextView import androidx.annotation.RequiresApi import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.google.android.exoplayer2.C import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Timeline import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.R import knf.kuma.commons.BypassUtil import knf.kuma.commons.EAHelper import knf.kuma.commons.SSLSkipper import knf.kuma.commons.doOnUI import knf.kuma.commons.noCrash import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.databinding.ExoPlayerBinding import knf.kuma.pojos.QueueObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster class CustomExoPlayer : GenericActivity(), Player.Listener { private var exoPlayer: ExoPlayer? = null private var playerState: PlayerState = PlayerState() private var isEnding = false private var playList: List = ArrayList() private val binding by lazy { ExoPlayerBinding.inflate(layoutInflater) } private val resizeMode: Int get() { return when (PreferenceManager.getDefaultSharedPreferences(this).getString("player_resize", "0")) { "0" -> AspectRatioFrameLayout.RESIZE_MODE_FIT "1" -> AspectRatioFrameLayout.RESIZE_MODE_FILL "2" -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH "3" -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT else -> AspectRatioFrameLayout.RESIZE_MODE_FIT } } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE setContentView(binding.root) window.decorView.setBackgroundColor(Color.BLACK) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) find(R.id.pip).visibility = View.VISIBLE find(R.id.pip).setOnClickListener { onPip() } find(R.id.skip).setOnClickListener { onSkip() } hideUI() binding.player.resizeMode = resizeMode binding.player.requestFocus() lifecycleScope.launch(Dispatchers.Main) { playerState = withContext(Dispatchers.IO) { CacheDB.INSTANCE.playerStateDAO().find(intent.getStringExtra("title") ?: "???") ?: PlayerState() } if (savedInstanceState != null) { playerState.position = savedInstanceState.getLong("position", C.TIME_UNSET) playerState.window = savedInstanceState.getInt("listPosition", 0) } checkPlaylist(intent) initPlayer(intent) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong("position", playerState.position) if (playerState.window != 0) outState.putInt("listPosition", playerState.window) } private fun hideUI() { window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) } private fun initPlayer(intent: Intent) { if (exoPlayer == null) { SSLSkipper.skip() lifecycleScope.launch(Dispatchers.Main) { find(R.id.video_title).text = intent.getStringExtra("title") exoPlayer = ExoPlayer.Builder(this@CustomExoPlayer).build() binding.player.player = exoPlayer exoPlayer?.addListener(this@CustomExoPlayer) addMedia(exoPlayer, intent) exoPlayer?.prepare() val canResume = playerState.window >= 0 && playerState.position != C.TIME_UNSET if (canResume) exoPlayer?.seekTo(playerState.window, playerState.position) exoPlayer?.playWhenReady = true /*disposable = Observable.interval(1, TimeUnit.SECONDS).map { exoPlayer?.currentPosition?:0 } .subscribeOn(AndroidSchedulers.from(exoPlayer?.applicationLooper, false)) .subscribe { if (it > 0) lastPosition = it }*/ } } } private suspend fun addMedia(player: ExoPlayer?, intent: Intent) { player?: return if (intent.getBooleanExtra("isPlayList", false)) { val sourceList = ArrayList() playList = withContext(Dispatchers.IO) { CacheDB.INSTANCE.queueDAO() .getAllByAid(intent.getStringExtra("playlist") ?: "empty") } noCrash { find(R.id.video_title).text = playList[0].title() } DefaultHttpDataSource.Factory() for (queueObject in playList) { sourceList.add( ProgressiveMediaSource.Factory( DefaultHttpDataSource.Factory().apply { setUserAgent(BypassUtil.userAgent) } ).createMediaSource(MediaItem.fromUri(queueObject.createUri())) ) } player.addMediaSources(sourceList) } else { if (intent.getBooleanExtra("isFile", false) || !intent.hasExtra("headers")) { player.addMediaItem(MediaItem.fromUri(intent.data ?: Uri.parse(""))) } else { val httpFactory = DefaultHttpDataSource.Factory().apply { setAllowCrossProtocolRedirects(true) intent.getStringArrayExtra("headers") ?.let { headerArray -> val slices = headerArray.toList().chunked(2) val headers = mutableMapOf() slices.forEach { headers[it[0]] = it[1] } setDefaultRequestProperties(headers) if (headers.contains("User-Agent")) { setUserAgent(headers["User-Agent"]) } else { setUserAgent(BypassUtil.userAgent) } }?: setUserAgent(BypassUtil.userAgent) } val factory = when(MimeTypeMap.getFileExtensionFromUrl(intent.data?.toString())) { "m3u8" -> HlsMediaSource.Factory(httpFactory) else -> ProgressiveMediaSource.Factory(httpFactory) } player.addMediaSource( factory.createMediaSource( MediaItem.fromUri( intent.data ?: Uri.parse("") ) ) ) } } } private fun releasePlayer() { exoPlayer?.stop() exoPlayer?.release() exoPlayer = null } @TargetApi(Build.VERSION_CODES.N) internal fun onPip() { try { if (!isInPictureInPictureMode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { playerState.position = exoPlayer?.currentPosition ?: 0 val params = PictureInPictureParams.Builder() //.setAspectRatio(Rational(player.width, player.height)) .build() enterPictureInPictureMode(params) } } } catch (e: Exception) { e.printStackTrace() } } private fun onSkip() { exoPlayer?.seekTo( exoPlayer?.currentWindowIndex ?: 0, (exoPlayer?.currentPosition ?: 0) + 85000 ) } @RequiresApi(Build.VERSION_CODES.O) override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration ) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) if (!isInPictureInPictureMode) { runOnUiThread { find(R.id.lay_top).visibility = View.VISIBLE find(R.id.lay_bottom).visibility = View.VISIBLE binding.player.useController = true } /*getApplication().startActivity(new Intent(this, getClass()) .putExtra("isReorder", true) .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));*/ } else { runOnUiThread { find(R.id.lay_top).visibility = View.GONE find(R.id.lay_bottom).visibility = View.GONE find(R.id.progress).visibility = View.GONE binding.player.useController = false } } } private fun checkPlaylist(intent: Intent) { if (!intent.getBooleanExtra("isPlayList", false)) { find(com.google.android.exoplayer2.R.id.exo_next).visibility = View.GONE find(com.google.android.exoplayer2.R.id.exo_prev).visibility = View.GONE } else { find(com.google.android.exoplayer2.R.id.exo_next).visibility = View.VISIBLE find(com.google.android.exoplayer2.R.id.exo_prev).visibility = View.VISIBLE } } override fun onUserLeaveHint() { super.onUserLeaveHint() exoPlayer?.playWhenReady = false } override fun onNewIntent(intent: Intent) { setIntent(intent) releasePlayer() playerState.window = C.INDEX_UNSET playerState.position = 0 checkPlaylist(intent) initPlayer(intent) super.onNewIntent(intent) } override fun onResume() { doOnUI { hideUI() } exoPlayer?.playWhenReady = true super.onResume() } override fun onPause() { super.onPause() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode) return val state = playerState.apply { title = find(R.id.video_title).text.toString() position = if (!isEnding) { exoPlayer?.currentPosition ?: 0 } else 0 } doAsync { CacheDB.INSTANCE.playerStateDAO().set(state) } if (!isFinishing) exoPlayer?.pause() else exoPlayer?.stop() } override fun onDestroy() { releasePlayer() super.onDestroy() } override fun onTimelineChanged(timeline: Timeline, reason: Int) { } override fun onLoadingChanged(isLoading: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode) return find(R.id.progress).post { find(R.id.progress).visibility = if (isLoading) View.VISIBLE else View.GONE } } override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { if (playbackState == Player.STATE_READY) doOnUI { hideUI() } if (playbackState == Player.STATE_ENDED) { isEnding = true finish() } } override fun onRepeatModeChanged(repeatMode: Int) { } override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { } override fun onPlayerError(error: PlaybackException) { Toaster.toast( "Error al reproducir: " + error.message?.replace("%", "%%"), emptyArray() ) /*MaterialDialog(this).show { message(text = error.stackTraceToString().also { (getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.setPrimaryClip(ClipData.newPlainText("stack", it)) }) positiveButton(text = "OK") }*/ FirebaseCrashlytics.getInstance().recordException(error) finish() } /*override fun onPositionDiscontinuity(reason: Int) { try { val latestPosition = exoPlayer?.currentWindowIndex ?: 0 if (latestPosition != listPosition) { val state = PlayerState().apply { title = playList[listPosition].title() if (reason == 0) { position = 0 } else if (reason in 1..2) { position = lastPosition } } doAsync { CacheDB.INSTANCE.playerStateDAO().set(state) } listPosition = latestPosition video_title.text = playList[listPosition].title() } } catch (e: Exception) { e.printStackTrace() } }*/ override fun onPositionDiscontinuity( oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int ) { try { val latestPosition = newPosition.windowIndex if (latestPosition != playerState.window) { val state = playerState.apply { title = playList[playerState.window].title() if (reason == 0) { position = 0 } else if (reason in 1..2) { position = oldPosition.positionMs } } doAsync { CacheDB.INSTANCE.playerStateDAO().set(state) } playerState.window = latestPosition find(R.id.video_title).text = playList[playerState.window].title() } } catch (e: Exception) { e.printStackTrace() } } override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { } } ================================================ FILE: app/src/main/java/knf/kuma/player/MediaCatalog.kt ================================================ package knf.kuma.player import android.content.Intent import android.support.v4.media.MediaDescriptionCompat import androidx.core.os.bundleOf import knf.kuma.pojos.QueueObject /** * Manages a set of media metadata that is used to create a playlist for [VideoActivity]. */ open class MediaCatalog(private val list: MutableList, private val intent: Intent, playList: List) : MutableList by list { companion object : MediaCatalog(mutableListOf(), Intent(), emptyList()) init { if (intent.getBooleanExtra("isPlayList", false)) { var count = 1 playList.forEach { list.add( with(MediaDescriptionCompat.Builder()) { setTitle(it.title()) setMediaId(count.toString()) setMediaUri(it.createUri()) build() }) count++ } } else list.add( with(MediaDescriptionCompat.Builder()) { setTitle(intent.getStringExtra("title")) setMediaId("1") setMediaUri(intent.data) setExtras(bundleOf("headers" to intent.getStringArrayExtra("headers"))) build() }) } } ================================================ FILE: app/src/main/java/knf/kuma/player/Player.kt ================================================ package knf.kuma.player import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.media.AudioManager import android.net.Uri import android.support.v4.media.MediaDescriptionCompat import android.webkit.MimeTypeMap import androidx.media.AudioAttributesCompat import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey import com.afollestad.materialdialogs.MaterialDialog import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.ui.PlayerView import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import com.google.firebase.crashlytics.FirebaseCrashlytics import io.reactivex.rxjava3.disposables.Disposable import knf.kuma.commons.BypassUtil import knf.kuma.commons.noCrashLetNullable import knf.kuma.database.CacheDB import knf.kuma.pojos.QueueObject import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster @Entity data class PlayerState( @PrimaryKey var title: String = "", var position: Long = 0, @Ignore var window: Int = 0, @Ignore var whenReady: Boolean = true, @Ignore var isFinishing: Boolean = false) class PlayerHolder( private val context: Context, private val playerState: PlayerState, private val playerView: PlayerView, private val intent: Intent, private val playList: List ) { val audioFocusPlayer: ExoPlayer val playerCallback: PlayerCallback private var listPosition = 0 private val mediaCatalog: MediaCatalog private var disposable: Disposable? = null private var lastPosition = 0L // Create the exoPlayer instance. init { val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val audioAttributes = AudioAttributesCompat.Builder() .setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE) .setUsage(AudioAttributesCompat.USAGE_MEDIA) .build() playerCallback = context as PlayerCallback mediaCatalog = MediaCatalog(mutableListOf(), intent, playList) playerCallback.onChangeTitle((mediaCatalog[listPosition].title ?: "").toString()) mediaCatalog[listPosition].title?.let { playerState.title = it.toString() } if (mediaCatalog.size == 1) playerCallback.onChangeTitle(mediaCatalog[0].title.toString()) audioFocusPlayer = AudioFocusWrapper( audioAttributes, audioManager, ExoPlayer.Builder(context).build() .also { player -> playerView.player = player } ) /*disposable = Observable.interval(1, TimeUnit.SECONDS).map { audioFocusPlayer.currentPosition } .subscribeOn(AndroidSchedulers.from(audioFocusPlayer.applicationLooper, false)) .subscribe { if (it > 0) lastPosition = it }*/ } private fun buildMediaSource(): List { return mediaCatalog.mapNotNull { noCrashLetNullable { createExtractorMediaSource(it) } } } private fun createExtractorMediaSource(descriptor: MediaDescriptionCompat): MediaData { val item = MediaItem.fromUri(descriptor.mediaUri?: Uri.EMPTY) if (intent.getBooleanExtra("isFile", false)) return MediaData(item) val httpFactory = DefaultHttpDataSource.Factory().apply { descriptor.extras?.getStringArray("headers")?.let { headerArray -> val headers = headerArray.toList().chunked(2).associate { Pair(it[0], it[1]) } setDefaultRequestProperties(headers) if (headers.contains("User-Agent")) { setUserAgent(headers["User-Agent"]) } else { setUserAgent(BypassUtil.userAgent) } } ?: setUserAgent(BypassUtil.userAgent) } val factory = when(MimeTypeMap.getFileExtensionFromUrl(descriptor.mediaUri?.toString())) { "m3u8" -> HlsMediaSource.Factory(httpFactory) else -> ProgressiveMediaSource.Factory(httpFactory) } return MediaData(factory.createMediaSource(item)) } class MediaData { constructor(media: MediaSource) { mediaSource = media } constructor(media: MediaItem) { mediaItem = media } private lateinit var mediaSource: MediaSource private lateinit var mediaItem: MediaItem fun addMedia(exoPlayer: ExoPlayer) { if (::mediaSource.isInitialized) { exoPlayer.addMediaSource(mediaSource) } if (::mediaItem.isInitialized) { exoPlayer.addMediaItem(mediaItem) } } } // Prepare playback. fun start() { // Load media. buildMediaSource().forEach { it.addMedia(audioFocusPlayer) } //audioFocusPlayer.setMediaItems(buildMediaSource()) audioFocusPlayer.prepare() // Restore state (after onResume()/onStart()) with(playerState) { // Start playback when media has buffered enough // (whenReady is true by default). audioFocusPlayer.seekTo(window, position) audioFocusPlayer.playWhenReady = whenReady // Add logging. attachLogging(audioFocusPlayer) } } // Stop playback and release resources, but re-use the exoPlayer instance. fun stop() { with(audioFocusPlayer) { // Save state saveState() // Stop the exoPlayer (and release it's resources). The exoPlayer instance can be reused. stop() clearMediaItems() } } fun skip() { with(audioFocusPlayer) { seekTo(currentMediaItemIndex, currentPosition + 85000) } } fun saveState() { with(audioFocusPlayer) { with(playerState) { position = currentPosition window = currentMediaItemIndex whenReady = playWhenReady } } } // Destroy the exoPlayer instance. fun release() { audioFocusPlayer.release() // exoPlayer instance can't be used again. disposable?.dispose() } /** * For more info on ExoPlayer logging, please review this * [codelab](https://codelabs.developers.google.com/codelabs/exoplayer-intro/#5). */ private fun attachLogging(exoPlayer: ExoPlayer) { // Show toasts on state changes. exoPlayer.addListener(object : Player.Listener { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { when (playbackState) { Player.STATE_ENDED -> { playerCallback.onFinish() } } } override fun onPlayerError(error: PlaybackException) { Toaster.toast("Error al reproducir: " + error.message?.replace("%", "%%")) MaterialDialog(this@PlayerHolder.context).show { message(text = error.stackTraceToString().also { (this@PlayerHolder.context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.setPrimaryClip( ClipData.newPlainText("stack", it) ) }) positiveButton(text = "OK") } FirebaseCrashlytics.getInstance().recordException(error) playerCallback.onFinish() } override fun onLoadingChanged(isLoading: Boolean) { playerCallback.onLoadingChange(isLoading) } override fun onPositionDiscontinuity( oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int ) { try { val latestPosition = newPosition.mediaItemIndex if (latestPosition != oldPosition.mediaItemIndex) { playerState.apply { title = playList[listPosition].title() if (reason == 0) { position = 0 } else if (reason in 1..2) { position = oldPosition.positionMs } } doAsync { CacheDB.INSTANCE.playerStateDAO().set(playerState) } listPosition = latestPosition playerCallback.onChangeTitle( (mediaCatalog[listPosition].title ?: "").toString() ) } } catch (e: Exception) { e.printStackTrace() } } /*override fun onPositionDiscontinuity(reason: Int) { try { val latestPosition = audioFocusPlayer.currentWindowIndex if (latestPosition != listPosition) { playerState.apply { title = playList[listPosition].title() if (reason == 0) { position = 0 } else if (reason in 1..2) { position = lastPosition } } doAsync { CacheDB.INSTANCE.playerStateDAO().set(playerState) } listPosition = latestPosition playerCallback.onChangeTitle((mediaCatalog[listPosition].title ?: "").toString()) setUpRetriever(mediaCatalog[listPosition]) } } catch (e: Exception) { e.printStackTrace() } }*/ }) // Write to log on state changes. exoPlayer.addListener(object : Player.Listener { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { //Log.i("Player", "playerStateChanged: ${getStateString(playbackState)}, $playWhenReady") playerCallback.onPlayerStateChanged(playbackState, playWhenReady) } fun getStateString(state: Int): String { return when (state) { Player.STATE_BUFFERING -> "STATE_BUFFERING" Player.STATE_ENDED -> "STATE_ENDED" Player.STATE_IDLE -> "STATE_IDLE" Player.STATE_READY -> "STATE_READY" else -> "?" } } }) } interface PlayerCallback { fun onChangeTitle(title: String) fun onLoadingChange(loading: Boolean) fun onPlayerStateChanged(state: Int, playWhenReady: Boolean) fun onFinish() } } ================================================ FILE: app/src/main/java/knf/kuma/player/VideoActivity.kt ================================================ package knf.kuma.player import android.app.PictureInPictureParams import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Color import android.media.AudioManager import android.os.Build import android.os.Bundle import android.support.v4.media.session.MediaSessionCompat import android.view.View import android.view.animation.AnimationUtils import android.widget.TextView import androidx.annotation.RequiresApi import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.github.vkay94.dtpv.youtube.YouTubeOverlay import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import knf.kuma.R import knf.kuma.commons.EAHelper import knf.kuma.commons.SSLSkipper import knf.kuma.commons.doOnUI import knf.kuma.commons.noCrash import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import knf.kuma.databinding.PlayerViewBinding import knf.kuma.pojos.QueueObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick /** * Allows playback of videos that are in a playlist, using [PlayerHolder] to load the and render * it to the [com.google.android.exoplayer2.ui.PlayerView] to render the video output. Supports * [MediaSessionCompat] and picture in picture as well. */ class VideoActivity : GenericActivity(), PlayerHolder.PlayerCallback { private val mediaSession: MediaSessionCompat by lazy { createMediaSession() } private val mediaSessionConnector: MediaSessionConnector by lazy { createMediaSessionConnector() } private val binding by lazy { PlayerViewBinding.inflate(layoutInflater) } private lateinit var playerState: PlayerState private lateinit var playerHolder: PlayerHolder var playList: List = emptyList() override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE setContentView(binding.root) window.decorView.setBackgroundColor(Color.BLACK) volumeControlStream = AudioManager.STREAM_MUSIC hideUI() SSLSkipper.skip() binding.player.resizeMode = getResizeMode() find(R.id.exit).onClick { onBackPressedDispatcher.onBackPressed() } find(R.id.lock).onClick { lock() } find(R.id.skip).setOnClickListener { playerHolder.skip() } binding.unlock.onClick { unlock() } binding.youtubeOverlay.performListener(object : YouTubeOverlay.PerformListener { override fun onAnimationEnd() { binding.youtubeOverlay.startAnimation(AnimationUtils.loadAnimation(this@VideoActivity, R.anim.fadeout)) binding.youtubeOverlay.isVisible = false find(R.id.exo_ll_controls).isVisible = true } override fun onAnimationStart() { binding.youtubeOverlay.isVisible = true binding.youtubeOverlay.startAnimation(AnimationUtils.loadAnimation(this@VideoActivity, R.anim.fadein)) find(R.id.exo_ll_controls).isVisible = false } }) if (!intent.getBooleanExtra("isPlayList", false)) { find(R.id.lay_next).isVisible = false find(R.id.lay_prev).isVisible = false } lifecycleScope.launch(Dispatchers.IO) { playList = CacheDB.INSTANCE.queueDAO().getAllByAid(intent.getStringExtra("playlist") ?: "empty") playerState = CacheDB.INSTANCE.playerStateDAO().find(intent.getStringExtra("title") ?: "???") ?: PlayerState() if (savedInstanceState != null) { playerState.position = savedInstanceState.getLong("position", 0) playerState.window = savedInstanceState.getInt("window", 0) playerState.whenReady = savedInstanceState.getBoolean("playWhenReady", true) } //createMediaSession() withContext(Dispatchers.Main) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPictureInPictureMode) binding.player.useController = false createPlayer() playerHolder.start() activateMediaSession() } } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) if (::playerHolder.isInitialized) { playerHolder.saveState() outState.putInt("window", playerState.window) outState.putLong("position", playerState.position) outState.putBoolean("playWhenReady", playerState.whenReady) } } private fun lock() { binding.player.hideController() binding.layLocked.isVisible = true } private fun unlock() { binding.player.showController() binding.layLocked.isVisible = false } private fun hideUI() { window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) window.addFlags(View.KEEP_SCREEN_ON) } private fun getResizeMode(): Int { return when (PreferenceManager.getDefaultSharedPreferences(this).getString("player_resize", "0")) { "0" -> AspectRatioFrameLayout.RESIZE_MODE_FIT "1" -> AspectRatioFrameLayout.RESIZE_MODE_FILL "2" -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH "3" -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT else -> AspectRatioFrameLayout.RESIZE_MODE_FIT } } override fun onStart() { super.onStart() if (::playerHolder.isInitialized) { startPlayer() activateMediaSession() } } override fun onResume() { super.onResume() doOnUI { hideUI() } resumePlayer() } override fun onPause() { super.onPause() if (Build.VERSION.SDK_INT >= 24 && isInPictureInPictureMode) resumePlayer() else { if (::playerHolder.isInitialized) if (!playerState.isFinishing) { playerHolder.saveState() playerState.title = find(R.id.video_title).text.toString() saveState() } if (!isFinishing) pausePlayer() else stopPlayer() } } override fun onStop() { deactivateMediaSession() super.onStop() } override fun onDestroy() { super.onDestroy() stopPlayer() releasePlayer() releaseMediaSession() } private fun saveState() { GlobalScope.launch(Dispatchers.IO) { CacheDB.INSTANCE.playerStateDAO().set(playerState) } } // MediaSession related functions. private fun createMediaSession(): MediaSessionCompat = MediaSessionCompat(this, packageName).apply { setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) setCallback(object : MediaSessionCompat.Callback() { override fun onPlay() { super.onPlay() with(playerHolder.audioFocusPlayer) { playWhenReady = true } } override fun onPause() { super.onPause() with(playerHolder.audioFocusPlayer) { playWhenReady = false } } override fun onSkipToNext() { super.onSkipToNext() with(playerHolder.audioFocusPlayer) { if (hasNext()) next() } } override fun onSkipToPrevious() { super.onSkipToPrevious() with(playerHolder.audioFocusPlayer) { if (hasPrevious()) previous() } } }) } private fun createMediaSessionConnector(): MediaSessionConnector = MediaSessionConnector(mediaSession).apply { // If QueueNavigator isn't set, then mediaSessionConnector will not handle following // MediaSession actions (and they won't show up in the minimized PIP activity): // [ACTION_SKIP_PREVIOUS], [ACTION_SKIP_NEXT], [ACTION_SKIP_TO_QUEUE_ITEM] /*setQueueNavigator(object : TimelineQueueNavigator(mediaSession) { override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat { } })*/ } // MediaSession related functions. private fun activateMediaSession() { // Note: do not pass a null to the 3rd param below, it will cause a NullPointerException. // To pass Kotlin arguments to Java varargs, use the Kotlin spread operator `*`. mediaSessionConnector.setPlayer(playerHolder.audioFocusPlayer) mediaSession.isActive = true } private fun deactivateMediaSession() { mediaSessionConnector.setPlayer(null) mediaSession.isActive = false } private fun releaseMediaSession() { mediaSession.release() } // ExoPlayer related functions. private fun createPlayer() { playerHolder = PlayerHolder(this, playerState, binding.player, intent, playList) binding.youtubeOverlay.player(playerHolder.audioFocusPlayer) if (!intent.getBooleanExtra("isPlayList", false)) { find(com.google.android.exoplayer2.R.id.exo_next).visibility = View.GONE find(com.google.android.exoplayer2.R.id.exo_prev).visibility = View.GONE } //exoPlayer.overlayFrameLayout.setOnTouchListener(BVListener(this)) } private fun startPlayer() { if (::playerHolder.isInitialized) playerHolder.start() } private fun stopPlayer() { if (::playerHolder.isInitialized) playerHolder.stop() } private fun resumePlayer() { if (::playerHolder.isInitialized) with(playerHolder.audioFocusPlayer) { playWhenReady = true } } private fun pausePlayer() { if (::playerHolder.isInitialized) with(playerHolder.audioFocusPlayer) { playWhenReady = false } } private fun releasePlayer() { if (::playerHolder.isInitialized) playerHolder.release() } // Picture in Picture related functions. override fun onUserLeaveHint() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ::playerHolder.isInitialized && playerHolder.audioFocusPlayer.playWhenReady && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { noCrash { enterPictureInPictureMode( with(PictureInPictureParams.Builder()) { //setAspectRatio(Rational(16, 9)) build() }) } } } @RequiresApi(Build.VERSION_CODES.O) override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration ) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) if (::playerHolder.isInitialized) playerHolder.saveState() binding.player.useController = !isInPictureInPictureMode } override fun onChangeTitle(title: String) { lifecycleScope.launch(Dispatchers.Main) { find(R.id.video_title).text = title } } override fun onLoadingChange(loading: Boolean) { with(find(R.id.progress)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode) post { visibility = View.GONE } post { visibility = if (loading) View.VISIBLE else View.GONE } } } override fun onPlayerStateChanged(state: Int, playWhenReady: Boolean) { if (state == Player.STATE_READY) doOnUI { hideUI() } } override fun onFinish() { playerState.apply { title = find(R.id.video_title).text.toString() position = 0 isFinishing = true } saveState() unlock() //finish() } } ================================================ FILE: app/src/main/java/knf/kuma/player/WebPlayerActivity.kt ================================================ package knf.kuma.player import android.content.Context import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Bundle import android.view.View import android.webkit.CookieManager import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import knf.kuma.databinding.ActivityBrowserBinding import org.jetbrains.anko.longToast class WebPlayerActivity : AppCompatActivity() { val binding by lazy { ActivityBrowserBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.setBackgroundDrawable(ColorDrawable(Color.BLACK)) try { CookieManager.getInstance() } catch (_: Exception) { longToast("Webview no está disponible en tu dispositivo") finish() return } setContentView(binding.root) binding.webview.apply { setBackgroundColor(Color.TRANSPARENT) settings.apply { javaScriptEnabled = true domStorageEnabled = true } webViewClient = object : WebViewClient() { @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? ): Boolean { return true } /* override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { //view?.loadUrl(url ?: return true) return true }*/ override fun onPageFinished(view: WebView?, url: String?) { binding.loading.isVisible = false } } //loadUrl(intent.dataString?:"about:blank") intent.dataString?.let { loadData(framed(it), "text/html; charset=utf-8", "UTF-8") } ?: finish() } window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) } } fun framed(link: String): String = "" fun openWebPlayer(context: Context, link: String){ context.startActivity(Intent(context,WebPlayerActivity::class.java).apply { data = Uri.parse(link) }) } ================================================ FILE: app/src/main/java/knf/kuma/pojos/Achievement.kt ================================================ package knf.kuma.pojos import android.content.Context import android.graphics.Color import android.graphics.drawable.Drawable import androidx.annotation.Keep import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverters import com.google.firebase.firestore.IgnoreExtraProperties import com.google.gson.annotations.SerializedName import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.commons.EAHelper import knf.kuma.custom.AchievementUnlocked import knf.kuma.database.BaseConverter import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @Keep @Entity @TypeConverters(BaseConverter::class) @IgnoreExtraProperties open class Achievement( @SerializedName("key") @PrimaryKey var key: Long = -1, @SerializedName("name") var name: String = "", @SerializedName("description") var description: String = "", @SerializedName("points") var points: Int = -1, @SerializedName("isSecret") var isSecret: Boolean = false, @SerializedName("group") var group: String? = null, @SerializedName("time") var time: Long = 0, @SerializedName("count") var count: Int = 0, @SerializedName("goal") var goal: Int = 0, @SerializedName("isUnlocked") var isUnlocked: Boolean = false, @SerializedName("isRevealed") var isRevealed: Boolean = false ) { fun getState(): String { return if (isUnlocked) { val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) val calendar = Calendar.getInstance().also { it.timeInMillis = time } dateFormat.format(calendar.time) } else "Bloqueado" } fun usableName(): String { return if (isSecret && !isUnlocked && !isRevealed) "Logro secreto" else name } fun usableDescription(): String { return if (isSecret && !isUnlocked && !isRevealed) "Usa mas la app para desbloquear" else description } fun usableIcon(): Int { return if (isSecret && !isUnlocked && !isRevealed) R.drawable.ic_locked else AchievementManager.getIcon(key) } private fun tintedIcon(context: Context): Drawable? { return try { val drawable = ContextCompat.getDrawable(context, AchievementManager.getIcon(key)) ?: return null val drawableWrap = DrawableCompat.wrap(drawable) DrawableCompat.setTint(drawableWrap, Color.WHITE) drawableWrap } catch (e: Exception) { e.printStackTrace() null } } fun achievementData(context: Context): AchievementUnlocked.AchievementData { return AchievementUnlocked.AchievementData() .setTitle(name) .setSubtitle(description) .setIcon(tintedIcon(context)) .setTextColor(Color.WHITE) .setBackgroundColor(ContextCompat.getColor(context, EAHelper.getThemeColor())) .setIconBackgroundColor(ContextCompat.getColor(context, EAHelper.getThemeColorLight())) //.setPopUpOnClickListener { context.startActivity(Intent(context,)) } } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/AchievementAd.kt ================================================ package knf.kuma.pojos import knf.kuma.ads.AdCallback class AchievementAd(private val adId: String) : Achievement(0, "", "", 0, false), AdCallback { override fun getID(): String = adId } ================================================ FILE: app/src/main/java/knf/kuma/pojos/Anime.java ================================================ package knf.kuma.pojos; import pl.droidsonroids.jspoon.annotation.Selector; public class Anime { @Selector("html") public AnimeObject.WebInfo object; } ================================================ FILE: app/src/main/java/knf/kuma/pojos/AnimeObject.java ================================================ package knf.kuma.pojos; import static java.lang.Math.abs; import android.text.TextUtils; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.core.graphics.drawable.IconCompat; import androidx.room.ColumnInfo; import androidx.room.Embedded; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; import androidx.room.TypeConverter; import androidx.room.TypeConverters; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import org.jetbrains.annotations.NotNull; import org.jsoup.nodes.Element; import java.io.Serializable; import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import knf.kuma.animeinfo.AnimeInfo; import knf.kuma.animeinfo.ktx.ExtensionsKt; import knf.kuma.commons.ExtensionUtilsKt; import knf.kuma.commons.FileWrapper; import knf.kuma.commons.PatternUtil; import knf.kuma.commons.PrefsUtil; import knf.kuma.database.CacheDB; import pl.droidsonroids.jspoon.ElementConverter; import pl.droidsonroids.jspoon.annotation.Selector; @Entity(indices = @Index(value = {"name", "link", "aid", "type", "state", "fileName"}, unique = true)) @TypeConverters(AnimeObject.Converter.class) public class AnimeObject implements Comparable, Serializable { @PrimaryKey @SerializedName("key") public int key; @SerializedName("link") public String link; @SerializedName("sid") public String sid; @SerializedName("name") public String name; @SerializedName("fileName") public String fileName; @Embedded @SerializedName("webInfo") public WebInfo webInfo; @Ignore @NonNull public transient String aid = ""; @Ignore public transient String img; @Ignore public transient String description; @SerializedName("type") public String type; @SerializedName("state") public String state; @SerializedName("day") public Day day; @Ignore public transient String followers; @Ignore public transient String rate_stars; @Ignore public transient String rate_count; @Ignore public transient List genres; @Ignore public transient List related; @SerializedName("chapters") public List chapters; @Ignore public transient IconCompat icon; @Ignore public AnimeObject() { } public AnimeObject(int key, String link, String sid, String name, String fileName, WebInfo webInfo, String type, String state, Day day) { this.key = key; this.link = link; this.sid = sid; this.name = name; this.fileName = fileName; this.webInfo = webInfo; this.type = type; this.state = state; this.day = day; populate(webInfo); } @Ignore public AnimeObject(int key, String link, String sid, String name, String fileName, String type, String state, Day day, List chapters) { this.key = key; this.link = link; this.sid = sid; this.name = name; this.fileName = fileName; this.type = type; this.state = state; this.day = day; this.chapters = chapters; } @Ignore public AnimeObject(String link, WebInfo webInfo) { this.link = link; this.fileName = PatternUtil.INSTANCE.getRootFileName(link); this.sid = extract(link); this.webInfo = webInfo; populate(webInfo); } private void populate(WebInfo webInfo) { this.key = Integer.parseInt(webInfo.aid); this.webInfo = webInfo; this.aid = webInfo.aid; this.name = PatternUtil.INSTANCE.fromHtml(webInfo.name); this.img = "https://www3.animeflv.net" + webInfo.img; this.description = PatternUtil.INSTANCE.fromHtml(webInfo.description); this.type = getType(webInfo.type); this.state = getState(webInfo.state); this.day = webInfo.emisionDay; this.followers = webInfo.followers; this.rate_stars = webInfo.rate_stars; this.rate_count = webInfo.rate_count; this.genres = webInfo.genres; this.related = webInfo.related; completeInfo(webInfo.scripts); } private void completeInfo(List scripts) { try { Element element = findDataScript(scripts); if (element != null) { AnimeInfo animeInfo = new AnimeInfo(element.html()); //this.name = PatternUtil.fromHtml(animeInfo.title); this.day = animeInfo.getDay(); this.chapters = WebInfo.AnimeChapter.create(animeInfo); } } catch (Exception e) { e.printStackTrace(); } } private Element findDataScript(List scripts) { try { for (Element element : scripts) if (element.html().contains("var anime_info")) return element; return null; } catch (Exception e) { return null; } } private String extract(String link) { return link.substring(link.lastIndexOf("/") + 1); } private String getType(String className) { switch (className) { default: case "Type tv": return "Anime"; case "Type ova": return "OVA"; case "Type special": return "Especial"; case "Type movie": return "Película"; } } private String getState(String className) { switch (className) { case "AnmStts": return "En emisión"; case "AnmStts A": return "Finalizado"; default: return "Próximamente"; } } public String getFileName() { if (PrefsUtil.INSTANCE.getSaveWithName()) return fileName; else return aid; } public String getGenresString() { if (genres.size() == 0) return "Sin generos"; StringBuilder builder = new StringBuilder(); for (String genre : genres) { builder.append(genre) .append(", "); } String g = builder.toString(); return g.substring(0, g.lastIndexOf(",")); } public Boolean checkIntegrity() { try { return chapters != null && (chapters.size() == 0 || (chapters.get(0).aid != null && chapters.get(0).eid != null)); } catch (Exception e) { return false; } } @Override public int hashCode() { return aid.hashCode(); } @Override public boolean equals(Object obj) { return obj instanceof AnimeObject && aid.equals(((AnimeObject) obj).aid) && day.equals(((AnimeObject) obj).day) && state.equals(((AnimeObject) obj).state); } @Override public int compareTo(@NonNull AnimeObject o) { return name.compareTo(o.name); } public enum Day { MONDAY(2), TUESDAY(3), WEDNESDAY(4), THURSDAY(5), FRIDAY(6), SATURDAY(7), SUNDAY(1), NONE(0); public final int value; Day(int value) { this.value = value; } public static Day fromValue(int value) { for (Day day : values()) { if (day.value == value) { return day; } } return NONE; } } public static class WebInfo { @Selector(value = "div.Image img[src]", attr = "src", format = "/(\\d+)[/.]") @SerializedName("aid") public String aid; @Selector(value = "h1.Title", defValue = "Error") @ColumnInfo(name = "web_name") @SerializedName("web_name") public String name; @Selector(value = "div.Image img[src]", attr = "src") @SerializedName("img") public String img; @Selector(value = "div.Description", defValue = "Sin descripcion") @SerializedName("description") public String description; @Selector(value = "span[class^=Type]", attr = "class", defValue = "Desconocido") @ColumnInfo(name = "web_type") @SerializedName("web_type") public String type; @Selector(value = "aside.SidebarA.BFixed p", attr = "class", defValue = "Desconocido") @ColumnInfo(name = "web_state") @SerializedName("web_state") public String state; @Selector(value = "div.Title:contains(Seguidores) span", defValue = "0") @SerializedName("followers") public String followers = "0"; @Selector(value = "span.vtprmd", defValue = "0.0") @SerializedName("rate_starts") public String rate_stars; @Selector(value = "span#votes_nmbr", defValue = "0") @SerializedName("rate_count") public String rate_count; @Selector(value = "span.Date.fa-calendar", converter = DayConverter.class) @SerializedName("emissionDay") public Day emisionDay; @Selector("nav.Nvgnrs a[href]") @SerializedName("genres") public List genres = new ArrayList<>(); @Selector("ul.ListAnmRel li:has(a[href~=^\\/[a-z]+\\/.+$])") @SerializedName("related") public List related = new ArrayList<>(); @Ignore @Selector("script:not([src])") transient List scripts; /*@Selector("ul.ListCaps li,ul.ListEpisodes li,ul#episodeList li") public List chapters = new ArrayList<>();*/ @Keep public static class AnimeRelated { @Selector(value = "a", attr = "href") @SerializedName("link") public String link; @Selector(value = "a", converter = AidGetter.class) @SerializedName("aid") public String aid; @Selector("a") @SerializedName("name") public String name; @Selector(value = "li", format = "\\((.*)\\)") @SerializedName("relation") public String relation; public static class AidGetter implements ElementConverter { @Keep public AidGetter() { } @Override public String convert(@NotNull Element node, @NotNull Selector selector) { String aid = CacheDB.Companion.getINSTANCE().animeDAO().findAidByName(node.text()); if (aid != null) return aid; else return "null"; } } } @Entity @Keep public static class AnimeChapter implements Comparable, Serializable { @SerializedName("chapter_key") @PrimaryKey public int key; @SerializedName("chapter_number") public String number; @SerializedName("chapter_eid") public String eid; @SerializedName("chapter_link") public String link; @SerializedName("chapter_name") public String name; @SerializedName("chapter_aid") public String aid; @SerializedName("chapter_img") @Ignore public String img; @SerializedName("chapter_Type") @Ignore public transient ChapterType chapterType; @SerializedName("chapter_ isDownloaded") @Ignore private transient FileWrapper fileWrapper; public AnimeChapter() { } public AnimeChapter(int key, String number, String eid, String link, String name, String aid) { this.key = key; this.number = number; this.eid = eid; this.link = link; this.name = name; this.aid = aid; this.fileWrapper = FileWrapper.Companion.create(ExtensionsKt.getFilePath(this)); } @Ignore public AnimeChapter(String name, String aid, Element element) { this.name = name; if (element.select("img").first() == null) { this.chapterType = ChapterType.OLD; String full = element.select("a").first().text(); this.number = "Episodio " + extract(full, "^.* (\\d+\\.?\\d*):?.*$"); this.link = "https://www3.animeflv.net" + element.select("a").first().attr("href"); this.eid = String.valueOf(abs((aid + number).hashCode())); } else { this.chapterType = ChapterType.NEW; this.number = element.select("p").first().ownText(); this.link = "https://www3.animeflv.net" + element.select("a").first().attr("href"); this.eid = String.valueOf(abs((aid + number).hashCode())); this.img = element.select("img.lazy").first().attr("src"); } this.aid = aid; this.key = (aid + number).hashCode(); this.fileWrapper = FileWrapper.Companion.create(ExtensionsKt.getFilePath(this)); } @Ignore public AnimeChapter(AnimeInfo info, String num, String sid) { this.name = info.getTitle(); this.chapterType = ChapterType.NEW; this.aid = info.getAid(); this.number = "Episodio " + num; this.link = "https://www3.animeflv.net/ver/" + info.getSid() + "-" + num; this.eid = String.valueOf(abs((aid + number).hashCode())); this.img = "https://cdn.animeflv.net/screenshots/" + info.getAid() + "/" + num + "/th_3.jpg"; this.key = (aid + number).hashCode(); this.fileWrapper = FileWrapper.Companion.create(ExtensionsKt.getFilePath(this)); } @NonNull public FileWrapper fileWrapper() { if (fileWrapper == null) this.fileWrapper = FileWrapper.Companion.create(ExtensionsKt.getFilePath(this)); return fileWrapper; } @Ignore public static AnimeChapter fromData(String aid, String chapter, String eid, String url, String name) { return new AnimeChapter((aid + chapter).hashCode(), chapter, eid, url, name, aid); } @Ignore public static AnimeChapter fromRecent(RecentObject object) { return new AnimeChapter((object.aid + object.chapter).hashCode(), object.chapter, object.eid, object.url, object.name, object.aid); } @Ignore public static AnimeChapter fromDownloaded(ExplorerObject.FileDownObj object) { return new AnimeChapter((object.aid + "Episodio " + object.chapter).hashCode(), "Episodio " + object.chapter, object.eid, object.link, object.title, object.aid); } public static List create(String name, String aid, List elements) { List chapters = new ArrayList<>(); for (Element element : elements) { try { chapters.add(new AnimeChapter(name, aid, element)); } catch (Exception e) { e.printStackTrace(); } } return chapters; } public static List create(AnimeInfo info) { List chapters = new ArrayList<>(); try { for (Map.Entry entry : info.getEpMap().entrySet()) { try { chapters.add(new AnimeChapter(info, entry.getKey(), entry.getValue())); } catch (Exception e) { e.printStackTrace(); } } Collections.sort(chapters); } catch (Exception e) { e.printStackTrace(); } return chapters; } public String commentariesLink(String version) { try { return "https://disqus.com/embed/comments/?base=default&f=https-animeflv-net&t_u=" + ExtensionUtilsKt.urlEncode(ExtensionUtilsKt.resolveRedirection(link, 0)) + "&s_o=default#version=" + version; } catch (Exception e) { try { return "https://disqus.com/embed/comments/?base=default&f=https-animeflv-net&t_u=" + ExtensionUtilsKt.urlEncode(link) + "&s_o=default#version=" + version; } catch (Exception ex) { return link; } } } @Override public int compareTo(@NonNull AnimeChapter animeChapter) { double num1 = Double.valueOf(number.substring(number.lastIndexOf(" ") + 1)); double num2 = Double.valueOf(animeChapter.number.substring(animeChapter.number.lastIndexOf(" ") + 1)); return Double.compare(num2, num1); } @Override public boolean equals(Object obj) { return obj instanceof AnimeChapter && eid.equals(((AnimeChapter) obj).eid); } @Override public int hashCode() { return name.hashCode() + number.hashCode(); } private String extract(String st, String regex) { Matcher matcher = Pattern.compile(regex).matcher(st); matcher.find(); return matcher.group(1); } public enum ChapterType { @SerializedName("NEW") NEW(0), @SerializedName("OLD") OLD(1); public int value; ChapterType(int value) { this.value = value; } } } } public static class Converter { @TypeConverter public List stringToList(String json) { return Arrays.asList(json.split(";")); } @TypeConverter public String listToString(List list) { return TextUtils.join(";", list); } @TypeConverter public List stringToRelated(String json) { try { Gson gson = new Gson(); Type type = new TypeToken>() { }.getType(); return gson.fromJson(json, type); } catch (Exception e) { return new ArrayList<>(); } } @TypeConverter public String relatedToString(List list) { Gson gson = new Gson(); Type type = new TypeToken>() { }.getType(); return gson.toJson(list, type); } @TypeConverter public List stringToChapters(String json) { Gson gson = new Gson(); Type type = new TypeToken>() { }.getType(); return gson.fromJson(json, type); } @TypeConverter public String chaptersToString(List list) { try { Gson gson = new Gson(); Type type = new TypeToken>() { }.getType(); return gson.toJson(list, type); } catch (Throwable e) { return "{}"; } } @TypeConverter public Day intToDay(int day) { return Day.fromValue(day); } @TypeConverter public int dayToInt(Day day) { if (day == null) { return 0; } return day.value; } } public static class DayConverter implements ElementConverter { @Keep public DayConverter() { } @Override public Day convert(@NotNull Element node, @NotNull Selector selector) { try { Element element = node.select(selector.value()).first(); if (element == null) return Day.NONE; String date = element.ownText().trim(); Calendar calendar = Calendar.getInstance(); calendar.setTime(new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date)); switch (calendar.get(Calendar.DAY_OF_WEEK)) { case 2: return Day.MONDAY; case 3: return Day.TUESDAY; case 4: return Day.WEDNESDAY; case 5: return Day.THURSDAY; case 6: return Day.FRIDAY; case 7: return Day.SATURDAY; case 1: return Day.SUNDAY; default: return Day.NONE; } } catch (Exception e) { //e.printStackTrace(); return Day.NONE; } } } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/AutoBackupObject.kt ================================================ package knf.kuma.pojos import android.annotation.SuppressLint import android.content.Context import android.provider.Settings import androidx.preference.PreferenceManager import com.google.gson.annotations.SerializedName import com.jaredrummler.android.device.DeviceName import knf.kuma.backup.objects.BackupObject import knf.kuma.commons.noCrashLet open class AutoBackupObject() : BackupObject() { @SerializedName("name") var name: String? = "" @SerializedName("device_id") var device_id: String? = "" @SerializedName("value") var value: String? = null @SuppressLint("HardwareIds") constructor(context: Context?) : this() { if (context != null) { this.name = DeviceName.getDeviceName() this.device_id = Settings.Secure.getString(context.applicationContext.contentResolver, Settings.Secure.ANDROID_ID) this.value = PreferenceManager.getDefaultSharedPreferences(context).getString("auto_backup", "0") } } @SuppressLint("HardwareIds") constructor(context: Context?, newValue: String?) : this() { if (context != null) { this.name = DeviceName.getDeviceName() this.device_id = Settings.Secure.getString(context.applicationContext.contentResolver, Settings.Secure.ANDROID_ID) this.value = newValue } } override fun toString(): String { return "$name ID: $device_id" } override fun hashCode(): Int { return (name + device_id).hashCode() } override fun equals(other: Any?): Boolean { return noCrashLet { other is AutoBackupObject && name == other.name && device_id == other.device_id } ?: false } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/DirectoryPage.kt ================================================ package knf.kuma.pojos import android.util.Log import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.isFullMode import knf.kuma.commons.jsoupCookies import knf.kuma.database.CacheDB import knf.kuma.database.dao.AnimeDAO import pl.droidsonroids.jspoon.Jspoon import pl.droidsonroids.jspoon.annotation.Selector class DirectoryPage { @Selector(value = "article.Anime.alt.B a.Button.Vrnmlk", attr = "href") var links: List = listOf() fun getAnimes(animeDAO: AnimeDAO, jspoon: Jspoon, updateInterface: UpdateInterface, isCloudflareActive: Boolean): List { val animeObjects = ArrayList() for (link in links) { if (Network.isConnected) { if (!animeDAO.existLink("%animeflv.net$link")) { try { val response = jsoupCookies("https://www3.animeflv.net$link", true).execute() val body = response.body() if (response.statusCode() == 200 && body != null) { val webInfo = jspoon.adapter(AnimeObject.WebInfo::class.java).fromHtml(body) if (isFullMode && !PrefsUtil.isFamilyFriendly) { animeObjects.add(AnimeObject("https://www3.animeflv.net$link", webInfo)) Log.e("Directory Getter", "Added: https://www3.animeflv.net$link") } else { if (webInfo.genres.contains("Ecchi")) Log.e("Directory Getter", "Skip: https://www3.animeflv.net$link") else { animeObjects.add( AnimeObject( "https://www3.animeflv.net$link", webInfo ) ) Log.e("Directory Getter", "Added: https://www3.animeflv.net$link") } } updateInterface.onAdd() } else if (response.statusCode() == 404) { CacheDB.INSTANCE.animeDAO().allLinksInEmission } else check(response.statusCode() < 400) { "Response code: ${response.statusCode()}" } } catch (e: Exception) { e.printStackTrace() Log.e("Directory Getter", "Error adding: https://www3.animeflv.net" + link + "\nCause: " + e.message) updateInterface.onError() } if (isCloudflareActive) Thread.sleep(5000) } } else { Log.e("Directory Getter", "Abort: No internet") break } } return animeObjects } fun getAnimesRecreate(jspoon: Jspoon, updateInterface: UpdateInterface, isCloudflareActive: Boolean): List { val animeObjects = ArrayList() for (link in links) { if (Network.isConnected) { try { val webInfo = jspoon.adapter(AnimeObject.WebInfo::class.java).fromHtml(jsoupCookies("https://www3.animeflv.net$link").get().outerHtml()) if (isFullMode && !PrefsUtil.isFamilyFriendly) { animeObjects.add(AnimeObject("https://www3.animeflv.net$link", webInfo)) Log.e("Directory Getter", "Replaced: https://www3.animeflv.net$link") } else { if (webInfo.genres.contains("Ecchi")) Log.e("Directory Getter", "Skip: https://www3.animeflv.net$link") else { animeObjects.add(AnimeObject("https://www3.animeflv.net$link", webInfo)) Log.e("Directory Getter", "Replaced: https://www3.animeflv.net$link") } } updateInterface.onAdd() } catch (e: Exception) { e.printStackTrace() Log.e("Directory Getter", "Error replacing: https://www3.animeflv.net" + link + "\nCause: " + e.message) updateInterface.onError() } if (isCloudflareActive) Thread.sleep(5000) } else { Log.e("Directory Getter", "Abort: No internet") break } } return animeObjects } interface UpdateInterface { fun onAdd() fun onError() } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/DownloadObject.java ================================================ package knf.kuma.pojos; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import androidx.room.TypeConverters; import java.util.Locale; import java.util.concurrent.TimeUnit; import knf.kuma.animeinfo.ktx.ExtensionsKt; import knf.kuma.commons.PatternUtil; import knf.kuma.database.BaseConverter; import knf.kuma.recents.RecentModel; import knf.kuma.videoservers.Headers; @Entity @TypeConverters({BaseConverter.class}) public class DownloadObject { @Ignore public static final int PAUSED = -2; @Ignore public static final int PENDING = -1; @Ignore public static final int DOWNLOADING = 0; @Ignore public static final int COMPLETED = 4; @PrimaryKey(autoGenerate = true) public int key; public String eid; public String file; public String link; public String name; public String chapter; public String did; public String eta; public String speed; public String server; public Headers headers; @Ignore public String title; @Ignore public boolean addQueue = false; public int progress; public long d_bytes; public long t_bytes; public long time; public boolean canResume; public int state; public DownloadObject(int key, String eid, String file, String link, String name, String chapter, String did, String eta, String speed, String server, Headers headers, int progress, long d_bytes, long t_bytes, boolean canResume, int state) { this.key = key; this.eid = eid; this.file = file; this.link = link; this.name = PatternUtil.INSTANCE.fromHtml(name); this.chapter = chapter; this.did = did; this.eta = eta; this.speed = speed; this.server = server; this.headers = headers; this.title = name + chapter.substring(chapter.lastIndexOf(" ")); this.progress = progress; this.d_bytes = d_bytes; this.t_bytes = t_bytes; this.canResume = canResume; this.state = state; } @Ignore public DownloadObject(String eid, String file, String name, String chapter, boolean addQueue) { this.eid = eid; this.file = file; this.name = PatternUtil.INSTANCE.fromHtml(name); this.addQueue = addQueue; this.chapter = chapter; this.did = "0"; this.eta = "-1"; this.speed = "0"; this.title = name + chapter.substring(chapter.lastIndexOf(" ")); this.progress = 0; this.d_bytes = 0; this.t_bytes = -1; this.time = System.currentTimeMillis(); this.canResume = false; this.state = PENDING; } @NonNull public static DownloadObject fromRecent(RecentObject object) { return new DownloadObject(object.eid, object.getFileName(), PatternUtil.INSTANCE.fromHtml(object.name), object.chapter, false); } @NonNull public static DownloadObject fromRecentModel(RecentModel object) { return new DownloadObject(object.extras.getEid(), object.extras.getFileName(), PatternUtil.INSTANCE.fromHtml(object.name), object.chapter, false); } @NonNull public static DownloadObject fromChapter(AnimeObject.WebInfo.AnimeChapter chapter, boolean addQueue) { return new DownloadObject(chapter.eid, ExtensionsKt.getFileName(chapter), PatternUtil.INSTANCE.fromHtml(chapter.name), chapter.number, addQueue); } public boolean isDownloading() { return state == DOWNLOADING || state == PENDING ; } public boolean isDownloadingOrPaused() { return state == DOWNLOADING || state == PAUSED || state == PENDING ; } public int getDid() { if (did == null) return 0; return Integer.parseInt(did); } public void setDid(int did) { this.did = String.valueOf(did); } public long getEta() { if (eta == null) return -1; return Long.parseLong(eta); } public void setEta(long eta) { this.eta = String.valueOf(eta); } private String getSpeed() { if (speed == null) return "0kB/s"; long s = Long.parseLong(speed); if (s < 1024) return s + "B/s"; else if (s < 1024000) return String.format(Locale.getDefault(), "%.0fkB/s", s / 1024f); else return String.format(Locale.getDefault(), "%.1fMB/s", s / 1024000f); } public void setSpeed(long speed) { this.speed = String.valueOf(speed); } public String getTime() { try { long duration = getEta(); long hours = TimeUnit.MILLISECONDS.toHours(duration); long minutes = TimeUnit.MILLISECONDS.toMinutes(duration) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(duration)); long seconds = TimeUnit.MILLISECONDS.toSeconds(duration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration)); StringBuilder builder = new StringBuilder(); if (hours > 0) { builder.append(hours); builder.append("h"); } if (minutes > 0) { if (minutes <= 9) builder.append("0"); builder.append(minutes); builder.append("m"); } if (seconds <= 9) { builder.append("0"); } builder.append(seconds); builder.append("s"); return builder.toString(); } catch (Exception e) { e.printStackTrace(); return ""; } } public String getSubtext() { if (!canResume) return getSize(); long duration = getEta(); if (duration == -1) return "Desconocido"; else if (duration == -2) return "Moviendo..."; else { return getTime() + ", " + getSpeed(); } } public String getSize() { /*if (t_bytes == -1) return ""; String size = formatSize(t_bytes); if (size.endsWith(".0")) size = size.substring(0, size.lastIndexOf(".")); return size;*/ return progress + "%"; } public String getDownloadServer() { if ("".equals(server.trim())) return "Desconocido"; else return server; } private String formatSize(long v) { if (v < 1024) return v + " B"; int z = (63 - Long.numberOfLeadingZeros(v)) / 10; return String.format(Locale.US, "%.1f%sB", (double) v / (1L << (z * 10)), " KMGTPE".charAt(z)); } @Override public int hashCode() { return (file + link + key + state + eta + speed + progress + d_bytes + t_bytes).hashCode(); } @Override public boolean equals(@Nullable Object obj) { return obj instanceof DownloadObject && key == ((DownloadObject) obj).key && state == ((DownloadObject) obj).state && file.equals(((DownloadObject) obj).file) && link.equals(((DownloadObject) obj).link) && eta.equals(((DownloadObject) obj).eta) && speed.equals(((DownloadObject) obj).speed) && progress == ((DownloadObject) obj).progress && d_bytes == ((DownloadObject) obj).d_bytes && t_bytes == ((DownloadObject) obj).t_bytes; } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/EAObject.kt ================================================ package knf.kuma.pojos import androidx.annotation.Keep import androidx.room.Entity import androidx.room.PrimaryKey @Keep @Entity data class EAObject( @PrimaryKey var code: Int = -1 ) ================================================ FILE: app/src/main/java/knf/kuma/pojos/ExplorerObject.java ================================================ package knf.kuma.pojos; import android.content.Context; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import androidx.room.TypeConverter; import androidx.room.TypeConverters; import com.google.firebase.crashlytics.FirebaseCrashlytics; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import knf.kuma.commons.PatternUtil; import knf.kuma.database.CacheDBWrap; import knf.kuma.download.FileAccessHelper; import knf.kuma.emision.AnimeSubObject; import knf.kuma.explorer.ThumbServer; import knf.kuma.explorer.creator.SubFile; import xdroid.toaster.Toaster; @Entity @TypeConverters(ExplorerObject.Converter.class) public class ExplorerObject { @PrimaryKey public int key; public String img; public String link; public String fileName; public String name; public String aid; public int count; public String path; public List chapters = new ArrayList<>(); @Ignore private final MutableLiveData> liveData = new MutableLiveData<>(); @Ignore private boolean isProcessed = false; @Ignore private boolean isProcessing = false; @Ignore private List file_list; public ExplorerObject(int key, String img, String link, String fileName, String name, String aid, int count, String path, List chapters) { this.key = key; this.img = img; this.link = link; this.fileName = fileName; this.name = name; this.aid = aid; this.count = count; this.path = path; this.file_list = FileAccessHelper.INSTANCE.getDownloadsDirectoryFiles(fileName); this.chapters = chapters; } @Ignore public ExplorerObject(ExplorerObject item) { this.key = item.key; this.img = item.img; this.link = item.link; this.fileName = item.fileName; this.name = item.name; this.aid = item.aid; this.count = item.count; this.path = item.path; this.file_list = FileAccessHelper.INSTANCE.getDownloadsDirectoryFiles(fileName); this.chapters = item.chapters; } @Ignore public ExplorerObject(@Nullable AnimeSubObject object) throws IllegalStateException { if (object == null) throw new IllegalStateException("Anime not found!!!"); this.key = object.getKey(); this.img = PatternUtil.INSTANCE.getCover(object.getAid()); this.link = object.getLink(); this.fileName = object.getFinalName(); this.name = object.getName(); this.aid = object.getAid(); file_list = FileAccessHelper.INSTANCE.getDownloadsDirectoryFiles(object.getFileName()); if (file_list.size() == 0) { throw new IllegalStateException("Directory empty: " + object.getFinalName()); } this.count = file_list.size(); this.path = ""; } private void process(Context context) { isProcessing = true; AsyncTask.execute(() -> { try { chapters = new ArrayList<>(); this.file_list = FileAccessHelper.INSTANCE.getDownloadsDirectoryFiles(fileName); for (SubFile chap : file_list) try { String file_name = chap.getName(); chapters.add(new FileDownObj(context, name, aid, PatternUtil.INSTANCE.getNumFromFile(file_name), file_name, chap)); } catch (Exception e) { e.printStackTrace(); } this.count = chapters.size(); if (count == 0) Log.e("Directory empty", fileName); Collections.sort(chapters); isProcessed = true; isProcessing = false; new Handler(Looper.getMainLooper()).post(() -> liveData.setValue(chapters)); CacheDBWrap.INSTANCE.explorerDAO().update(this); } catch (Exception e) { e.printStackTrace(); FirebaseCrashlytics.getInstance().recordException(e); Toaster.toast("Error al obtener lista de episodios"); isProcessed = true; isProcessing = false; } }); } @NonNull public LiveData> getLiveData(Context context) { if (!isProcessed && !isProcessing) process(context); else if (isProcessed || chapters.size() > 0) new Handler(Looper.getMainLooper()).post(() -> liveData.setValue(chapters)); return liveData; } public void clearLiveData(LifecycleOwner owner) { liveData.removeObservers(owner); } public static class FileDownObj implements Comparable { public String title; public String chapter; public String aid; public String eid; public SubFile file; public String time; public String fileName; public String link; public File thumb; FileDownObj(Context context, String title, String aid, String chapter, String name, SubFile file) { this.title = title; this.chapter = chapter; this.aid = aid; this.eid = PatternUtil.INSTANCE.getEidFromFile(name); this.fileName = name; this.file = file; this.time = getTime(context, file); if (time.equals("")) throw new IllegalStateException("No duration"); this.link = "https://www3.animeflv.net/ver/" + fileName.substring(fileName.indexOf("$") + 1).replace(".mp4", ""); } public static String[] getTitles(List list) { List names = new ArrayList<>(); for (FileDownObj file : list) { names.add(file.getChapTitle()); } return names.toArray(new String[]{}); } public static Uri[] getUris(List list) { List uris = new ArrayList<>(); for (FileDownObj file : list) { uris.add(Uri.fromFile(FileAccessHelper.INSTANCE.getFile(file.fileName))); } return uris.toArray(new Uri[]{}); } public String getChapTitle() { return title + " " + chapter; } public String getChapPreviewLink() { if (thumb == null) return "https://www3.animeflv.net/uploads/animes/screenshots/" + aid + "/" + chapter + "/th_2.jpg"; else return ThumbServer.INSTANCE.loadFile(thumb); } private String getTime(Context context, SubFile file) { try { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(context, file.getFileUri()); long duration = Long.parseLong(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); long hours = TimeUnit.MILLISECONDS.toHours(duration); long minutes = TimeUnit.MILLISECONDS.toMinutes(duration) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(duration)); long seconds = TimeUnit.MILLISECONDS.toSeconds(duration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration)); StringBuilder builder = new StringBuilder(); if (hours > 0) { builder.append(hours); builder.append(":"); } if (minutes <= 9) { builder.append("0"); } builder.append(minutes); builder.append(":"); if (seconds <= 9) { builder.append("0"); } builder.append(seconds); return builder.toString(); } catch (Exception e) { e.printStackTrace(); return "??:??"; } } @Override public int compareTo(@NonNull FileDownObj o) { double num1 = Double.valueOf(chapter.substring(chapter.lastIndexOf(" ") + 1)); double num2 = Double.valueOf(o.chapter.substring(o.chapter.lastIndexOf(" ") + 1)); return Double.compare(num1, num2); } } public static class Converter { @TypeConverter public List StringToList(String s) { return new Gson().fromJson(s, new TypeToken>() { }.getType()); } @TypeConverter public String ListToString(List list) { return new Gson().toJson(list, new TypeToken>() { }.getType()); } @TypeConverter public String UriToString(Uri uri) { return uri.toString(); } @TypeConverter public Uri StringToUri(String text) { return Uri.parse(text); } } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/FakeAutoBackup.java ================================================ package knf.kuma.pojos; public class FakeAutoBackup extends AutoBackupObject { } ================================================ FILE: app/src/main/java/knf/kuma/pojos/FavSection.java ================================================ package knf.kuma.pojos; import knf.kuma.search.SearchAdvObject; public class FavSection extends FavoriteObject { private FavSection(int key, String aid, String name, String img, String type, String link, String category) { super(key, aid, name, img, type, link, category); } private FavSection(AnimeObject object) { super(object); } public FavSection(String name) { super((SearchAdvObject) null); if (name.equals(CATEGORY_NONE)) this.name = "Sin categoría"; else this.name = name; this.isSection = true; } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/FavoriteObject.java ================================================ package knf.kuma.pojos; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.List; import knf.kuma.search.SearchAdvObject; @Keep @Entity public class FavoriteObject implements Comparable { @SerializedName("CATEGORY_NONE") @Ignore public static final String CATEGORY_NONE = "_NONE_"; @SerializedName("key") @PrimaryKey public int key; @SerializedName("aid") public String aid; @SerializedName("name") public String name; @SerializedName("img") public String img; @SerializedName("type") public String type; @SerializedName("link") public String link; @SerializedName("category") public String category; @SerializedName("isSection") @Ignore public boolean isSection = false; @Ignore public FavoriteObject() { } public FavoriteObject(int key, String aid, String name, String img, String type, String link, String category) { this.key = key; this.aid = aid; this.name = name; this.img = img; this.type = type; this.link = link; this.category = category; } @Ignore public FavoriteObject(AnimeObject object) { if (object != null) { this.key = object.key; this.aid = object.aid; this.name = object.name; this.img = object.img; this.type = object.type; this.link = object.link; this.category = CATEGORY_NONE; } } @Ignore public FavoriteObject(SearchAdvObject object) { if (object != null) { this.key = object.getKey(); this.aid = object.getAid(); this.name = object.getName(); this.img = object.getImg(); this.type = object.getType(); this.link = object.getLink(); this.category = CATEGORY_NONE; } } public static List getNames(List list) { List strings = new ArrayList<>(); for (FavoriteObject object : list) { strings.add(object.name); } return strings; } public static List getCategories(List list) { List strings = new ArrayList<>(); for (FavoriteObject object : list) { if (object.category.equals(CATEGORY_NONE)) strings.add("Sin categoría"); else strings.add(object.category); } return strings; } public static Integer[] getIndex(List list, String category) { List index = new ArrayList<>(); int i = 0; for (FavoriteObject object : list) { if (object.category.equals(category)) index.add(i); i++; } return index.toArray(new Integer[0]); } public void setCategory(String category) { if (category == null) this.category = CATEGORY_NONE; else this.category = category; } @Override public int hashCode() { return name.hashCode() + (isSection ? 1 : -1); } @Override public boolean equals(Object obj) { return (obj instanceof FavSection || obj instanceof FavoriteObject) && name.equals(((FavoriteObject) obj).name); } @Override public int compareTo(@NonNull FavoriteObject o) { return name.compareTo(o.name); } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/GenreStatusObject.java ================================================ package knf.kuma.pojos; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import com.google.firebase.firestore.IgnoreExtraProperties; import java.util.ArrayList; import java.util.List; /** * Created by jordy on 26/03/2018. */ @Keep @Entity @IgnoreExtraProperties public class GenreStatusObject implements Comparable { public String name; public int count; @PrimaryKey public int key; public GenreStatusObject() { this.key = 0; this.name = ""; this.count = 0; } public GenreStatusObject(int key, String name, int count) { this.key = key; this.name = name; this.count = count; } @Ignore public GenreStatusObject(String name) { this.key = Math.abs(name.hashCode()); this.name = name; this.count = 0; } public static List names(List list) { List names = new ArrayList<>(); for (GenreStatusObject object : list) names.add(object.name); return names; } public boolean isBlocked() { return count < 0; } public void add(int number) { count += number; } public void sub(int number) { count -= number; if (count < 0) count = 0; } public void block() { count = -1; } public void reset() { count = 0; } @Override public int compareTo(@NonNull GenreStatusObject o) { return name.compareTo(o.name); } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/NotificationObj.java ================================================ package knf.kuma.pojos; import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import knf.kuma.recents.RecentsNotReceiver; @Entity public class NotificationObj { @Ignore public static final int RECENT=0; @PrimaryKey public int key; public int type; public NotificationObj(int key, int type) { this.key = key; this.type = type; } @NonNull public static NotificationObj fromIntent(Intent intent){ return new NotificationObj(intent.getIntExtra("key",-1),intent.getIntExtra("type",-1)); } public Intent getBroadcast(Context context) { return new Intent(context, RecentsNotReceiver.class).putExtra("key", key).putExtra("type", type); } @Override public boolean equals(Object obj) { return obj instanceof NotificationObj && key==((NotificationObj)obj).key; } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/QueueObject.java ================================================ package knf.kuma.pojos; import android.net.Uri; import androidx.annotation.Keep; import androidx.room.Embedded; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import androidx.room.RoomWarnings; import androidx.room.TypeConverters; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import knf.kuma.database.BaseConverter; import knf.kuma.database.CacheDBWrap; @Keep @Entity @TypeConverters({BaseConverter.class}) @SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED) public class QueueObject implements Serializable { @PrimaryKey public int id; public boolean isFile; public String uri; public long time; @Embedded public AnimeObject.WebInfo.AnimeChapter chapter; @Ignore public int count = -1; public QueueObject() { this.id = 0; this.isFile = false; this.uri = ""; this.time = 0; this.chapter = null; } public QueueObject(int id, boolean isFile, String uri, long time, AnimeObject.WebInfo.AnimeChapter chapter) { this.id = id; this.isFile = isFile; this.uri = uri; this.time = time; this.chapter = chapter; } @Ignore public QueueObject(Uri uri, boolean isFile, AnimeObject.WebInfo.AnimeChapter chapter) { this.id = chapter.key; this.uri = uri.toString(); this.isFile = isFile; this.time = System.currentTimeMillis(); this.chapter = chapter; } public static Uri[] uris(List list) { List uris = new ArrayList<>(); for (QueueObject object : list) uris.add(object.createUri()); return uris.toArray(new Uri[]{}); } public static List takeOne(List list) { List aids = new ArrayList<>(); List n_list = new ArrayList<>(); for (QueueObject object : list) { if (!aids.contains(object.chapter.aid)) { aids.add(object.chapter.aid); object.count = CacheDBWrap.INSTANCE.queueDAO().countAlone(object.chapter.aid); n_list.add(object); } } return n_list; } public static String[] getTitles(List list) { List titles = new ArrayList<>(); for (QueueObject object : list) titles.add(object.title()); return titles.toArray(new String[]{}); } public Uri createUri() { return Uri.parse(uri); } public String title() { try { return chapter.name + chapter.number.substring(chapter.number.lastIndexOf(" ")); } catch (Exception e) { return chapter.name; } } @Override public int hashCode() { return chapter.eid.hashCode(); } @Override public boolean equals(Object obj) { return obj instanceof QueueObject && chapter.eid.equals(((QueueObject) obj).chapter.eid); } public boolean equalsAnime(Object obj) { return obj instanceof QueueObject && chapter.aid.equals(((QueueObject) obj).chapter.aid); } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/RecentObject.java ================================================ package knf.kuma.pojos; import static java.lang.Math.abs; import androidx.annotation.NonNull; import androidx.room.Embedded; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import java.util.ArrayList; import java.util.List; import knf.kuma.commons.FileWrapper; import knf.kuma.commons.PatternUtil; import knf.kuma.commons.PrefsUtil; import knf.kuma.database.CacheDBWrap; import knf.kuma.database.dao.AnimeDAO; import knf.kuma.search.SearchObject; import pl.droidsonroids.jspoon.annotation.Selector; @Entity public class RecentObject { @PrimaryKey public int key; @Ignore @NonNull public String aid = ""; @Ignore @NonNull public String eid = ""; @Ignore @NonNull public String name = ""; @Ignore @NonNull public String chapter = ""; @Ignore @NonNull public String url = ""; @Ignore @NonNull public String anime = ""; @Ignore @NonNull public String img = ""; @Ignore public boolean isNew; @Ignore public boolean isDownloading; @Ignore public boolean isFav; @Ignore public boolean isSeen; @Ignore private FileWrapper fileWrapper; @Ignore public int downloadState; @Ignore public SearchObject animeObject; @Embedded public WebInfo webInfo; @Ignore public RecentObject() { } public RecentObject(int key, WebInfo webInfo) { this.key = key; populate(webInfo); } private RecentObject(AnimeDAO dao, WebInfo webInfo) { this.webInfo = webInfo; populate(dao, webInfo); } public static List create(List infos) { AnimeDAO dao = CacheDBWrap.INSTANCE.animeDAO(); List objects = new ArrayList<>(); for (WebInfo info : infos) { try { objects.add(new RecentObject(dao, info)); } catch (Exception e) { e.printStackTrace(); } } return objects; } public String getFileName() { if (PrefsUtil.INSTANCE.getSaveWithName()) return eid + "$" + PatternUtil.INSTANCE.getFileName(url); else return eid + "$" + aid + "-" + chapter.substring(chapter.lastIndexOf(" ") + 1) + ".mp4"; } public String getFilePath() { if (PrefsUtil.INSTANCE.getSaveWithName()) return "$" + PatternUtil.INSTANCE.getFileName(url); else return "$" + aid + "-" + chapter.substring(chapter.lastIndexOf(" ") + 1) + ".mp4"; } public String getEpTitle() { return name + chapter.substring(chapter.lastIndexOf(" ")); } public FileWrapper fileWrapper() { if (fileWrapper == null) fileWrapper = FileWrapper.Companion.create(getFilePath()); return fileWrapper; } @Override public boolean equals(Object obj) { return obj instanceof RecentObject && ( eid.equals(((RecentObject) obj).eid) || (name.equals(((RecentObject) obj).name) && chapter.equals(((RecentObject) obj).chapter))); } @Override public int hashCode() { return name.hashCode() + chapter.hashCode(); } private void populate(WebInfo webInfo) { this.key = (webInfo.aid + webInfo.chapter).hashCode(); this.aid = webInfo.aid; this.chapter = webInfo.chapter.trim(); this.eid = String.valueOf(abs((aid + chapter).hashCode())); this.name = PatternUtil.INSTANCE.fromHtml(webInfo.name); this.url = "https://www3.animeflv.net" + webInfo.url; this.img = "https://www3.animeflv.net" + webInfo.img.replace("thumbs", "covers"); this.isNew = chapter.matches("^.* [10]$"); this.anime = PatternUtil.INSTANCE.getAnimeUrl(this.url, this.aid); //File file = FileAccessHelper.INSTANCE.findFile(getFilePath()); DownloadObject downloadObject = CacheDBWrap.INSTANCE.downloadsDAO().getByEid(eid); this.isDownloading = downloadObject != null && downloadObject.state == DownloadObject.DOWNLOADING; if (downloadObject != null) { this.downloadState = downloadObject.state; } else { this.downloadState = -8; } this.animeObject = CacheDBWrap.INSTANCE.animeDAO().getSOByAid(aid); this.isFav = CacheDBWrap.INSTANCE.favsDAO().isFav(Integer.parseInt(aid)); this.isSeen = CacheDBWrap.INSTANCE.seenDAO().chapterIsSeen(aid, chapter); } private void populate(AnimeDAO dao, WebInfo webInfo) { if (isNotNumeric(webInfo.aid)) throw new IllegalStateException("Aid must be number"); populate(webInfo); this.animeObject = dao.getSOByAid(aid); this.isFav = CacheDBWrap.INSTANCE.favsDAO().isFav(Integer.parseInt(aid)); this.isSeen = CacheDBWrap.INSTANCE.seenDAO().chapterIsSeen(aid, chapter); } private boolean isNotNumeric(String number) { try { Integer.parseInt(number); return false; } catch (Exception e) { e.printStackTrace(); return true; } } public static class WebInfo { @Selector(value = "img[src]", attr = "src", format = "/(\\d+)\\.\\w+") public String aid; @Selector(value = "a", attr = "href", format = "/(.*)$") public String eid; @Selector(value = ".Title") public String name; @Selector(".Capi") public String chapter; @Selector(value = "a", attr = "href") public String url; @Selector(value = "img[src]", attr = "src") public String img; } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/RecentWrap.kt ================================================ package knf.kuma.pojos import knf.kuma.database.CacheDB class RecentWrap(val obj: RecentObject) { var isSeen = CacheDB.INSTANCE.seenDAO().chapterIsSeen(obj.aid,obj.chapter) var isFav = CacheDB.INSTANCE.favsDAO().isFav(obj.aid.toInt()) } fun RecentObject.wrap(): RecentWrap = RecentWrap(this) ================================================ FILE: app/src/main/java/knf/kuma/pojos/Recents.java ================================================ package knf.kuma.pojos; import java.util.List; import pl.droidsonroids.jspoon.annotation.Selector; public class Recents { @Selector("ul.ListEpisodios li:not(article), ul.List-Episodes li:not(article)") public List list; } ================================================ FILE: app/src/main/java/knf/kuma/pojos/RecordObject.java ================================================ package knf.kuma.pojos; import androidx.annotation.Keep; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import com.google.gson.annotations.SerializedName; import knf.kuma.database.CacheDBWrap; import knf.kuma.recents.RecentModel; import knf.kuma.search.SearchObject; @Keep @Entity public class RecordObject { @SerializedName("key") @PrimaryKey public int key; @SerializedName("name") public String name; @SerializedName("chapter") public String chapter; @SerializedName("aid") public String aid; @SerializedName("eid") public String eid; @SerializedName("date") public long date; @SerializedName("animeObject") @Ignore public transient SearchObject animeObject; public RecordObject(int key, String name, String chapter, String aid, String eid, long date) { this.key = key; this.name = name; this.chapter = chapter; this.aid = aid; this.eid = eid; this.date = date; this.animeObject = CacheDBWrap.INSTANCE.animeDAO().getByAidSimple(aid); } @Ignore private RecordObject() { } @Ignore public static RecordObject fromRecent(RecentObject recentObject) { RecordObject object = new RecordObject(); object.key = Integer.parseInt(recentObject.aid); object.name = recentObject.name; object.chapter = recentObject.chapter; object.aid = recentObject.aid; object.eid = recentObject.eid; object.date = System.currentTimeMillis(); return object; } @Ignore public static RecordObject fromRecentModel(RecentModel recentObject) { RecordObject object = new RecordObject(); object.key = Integer.parseInt(recentObject.aid); object.name = recentObject.name; object.chapter = recentObject.chapter; object.aid = recentObject.aid; object.eid = recentObject.extras.getEid(); object.date = System.currentTimeMillis(); return object; } @Ignore public static RecordObject fromDownloaded(ExplorerObject.FileDownObj downloadedObject) { RecordObject object = new RecordObject(); object.key = Integer.parseInt(downloadedObject.aid); object.name = downloadedObject.title; object.chapter = "Episodio " + downloadedObject.chapter; object.aid = downloadedObject.aid; object.eid = downloadedObject.eid; object.date = System.currentTimeMillis(); return object; } @Ignore public static RecordObject fromChapter(AnimeObject.WebInfo.AnimeChapter chapter) { RecordObject object = new RecordObject(); object.key = Integer.parseInt(chapter.aid); object.name = chapter.name; object.chapter = chapter.number; object.aid = chapter.aid; object.eid = chapter.eid; object.date = System.currentTimeMillis(); return object; } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/SeeingObject.java ================================================ package knf.kuma.pojos; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.recyclerview.widget.DiffUtil; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; import com.google.gson.annotations.SerializedName; import knf.kuma.database.CacheDBWrap; import knf.kuma.seeing.FavToSeeing; @Keep @Entity public class SeeingObject { @SerializedName("STATE_WATCHING") @Ignore public static final int STATE_WATCHING = 1; @SerializedName("STATE_CONSIDERING") @Ignore public static final int STATE_CONSIDERING = 2; @SerializedName("STATE_COMPLETED") @Ignore public static final int STATE_COMPLETED = 3; @SerializedName("STATE_DROPPED") @Ignore public static final int STATE_DROPPED = 4; @SerializedName("STATE_PAUSED") @Ignore public static final int STATE_PAUSED = 5; @SerializedName("key") @PrimaryKey public int key; @SerializedName("img") public String img; @SerializedName("link") public String link; @SerializedName("aid") public String aid; @SerializedName("title") public String title; @SerializedName("chapter") public String chapter; @SerializedName("state") public int state; @SerializedName("lastChapter") @Ignore public SeenObject lastChapter; @Ignore public static DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(@NonNull SeeingObject oldItem, @NonNull SeeingObject newItem) { return oldItem.aid.equals(newItem.aid) && oldItem.key == newItem.key; } @Override public boolean areContentsTheSame(@NonNull SeeingObject oldItem, @NonNull SeeingObject newItem) { return oldItem.state == newItem.state && oldItem.chapter.equals(newItem.chapter); } }; public SeeingObject(int key, String img, String link, String aid, String title, String chapter, int state) { this.key = key; this.img = img; this.link = link; this.aid = aid; this.title = title; this.chapter = chapter; this.lastChapter = FavToSeeing.INSTANCE.getLast(CacheDBWrap.INSTANCE.seenDAO().getAllByAid(aid)); this.state = state; } @Ignore private SeeingObject() { } @Ignore public static SeeingObject fromAnime(FavoriteObject favoriteObject) { SeeingObject item = new SeeingObject(); item.key = Integer.parseInt(favoriteObject.aid); item.img = favoriteObject.img; item.link = favoriteObject.link; item.aid = favoriteObject.aid; item.title = favoriteObject.name; item.chapter = "No empezado"; return item; } @Ignore public static SeeingObject fromAnime(AnimeObject animeObject, int state) { SeeingObject item = new SeeingObject(); item.key = Integer.parseInt(animeObject.aid); item.img = animeObject.img; item.link = animeObject.link; item.aid = animeObject.aid; item.title = animeObject.name; item.chapter = "No empezado"; item.state = state; return item; } } ================================================ FILE: app/src/main/java/knf/kuma/pojos/SeenObject.kt ================================================ package knf.kuma.pojos import android.util.Log import androidx.annotation.Keep import androidx.room.Entity import androidx.room.PrimaryKey import knf.kuma.commons.noCrashLetNullable import knf.kuma.database.CacheDB import knf.kuma.recents.RecentModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.util.Locale @Keep @Entity data class SeenObject(@PrimaryKey val eid: String = "", val aid: String = "", val number: String = "") { companion object { fun fromChapter(chapter: AnimeObject.WebInfo.AnimeChapter): SeenObject = SeenObject(chapter.eid, chapter.aid, chapter.number) fun fromRecent(recent: RecentObject): SeenObject = SeenObject(recent.eid, recent.aid, recent.chapter) fun fromRecentModel(recent: RecentModel): SeenObject = SeenObject(recent.extras.eid, recent.aid, recent.chapter) fun fromDownloaded(download: ExplorerObject.FileDownObj) = SeenObject(download.eid, download.aid, String.format(Locale.getDefault(), "Episodio %s", download.chapter)) } } fun migrateSeen() { GlobalScope.launch(Dispatchers.IO) { if (CacheDB.INSTANCE.chaptersDAO().count != 0) { var total: Int CacheDB.INSTANCE.seenDAO().addAll(CacheDB.INSTANCE.chaptersDAO().all.mapNotNull { noCrashLetNullable { SeenObject.fromChapter(it) } }.also { total = it.size }) CacheDB.INSTANCE.chaptersDAO().clear() Log.e("Seen", "Migrated $total") } } } ================================================ FILE: app/src/main/java/knf/kuma/preferences/AdsPreferenceActivity.kt ================================================ package knf.kuma.preferences import android.os.Bundle import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityAdsSettingsBinding import org.jetbrains.anko.sdk27.coroutines.onClick class AdsPreferenceActivity : GenericActivity() { private val binding by lazy { ActivityAdsSettingsBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.title = "Configuracion de anuncios" supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.switchNative.isChecked = PrefsUtil.isNativeAdsEnabled binding.preferenceNative.onClick { binding.switchNative.toggle() PrefsUtil.isNativeAdsEnabled = binding.switchNative.isChecked } binding.switchFull.isChecked = PrefsUtil.isFullAdsEnabled binding.preferenceFullText.isEnabled = binding.switchFull.isChecked binding.preferenceFullTextExtra.isEnabled = binding.switchFull.isChecked binding.sliderFull.isEnabled = binding.switchFull.isChecked binding.sliderFullExtra.isEnabled = binding.switchFull.isChecked binding.probabilityFull.isEnabled = binding.switchFull.isChecked binding.probabilityFullExtra.isEnabled = binding.switchFull.isChecked binding.preferenceFull.onClick { binding.switchFull.toggle() PrefsUtil.isFullAdsEnabled = binding.switchFull.isChecked binding.preferenceFullText.isEnabled = binding.switchFull.isChecked binding.preferenceFullTextExtra.isEnabled = binding.switchFull.isChecked binding.sliderFull.isEnabled = binding.switchFull.isChecked binding.sliderFullExtra.isEnabled = binding.switchFull.isChecked binding.probabilityFull.isEnabled = binding.switchFull.isChecked binding.probabilityFullExtra.isEnabled = binding.switchFull.isChecked } binding.sliderFull.value = PrefsUtil.fullAdsProbability binding.sliderFullExtra.value = PrefsUtil.fullAdsExtraProbability binding.probabilityFull.text = "${PrefsUtil.fullAdsProbability.toInt()}%" binding.probabilityFullExtra.text = "${PrefsUtil.fullAdsExtraProbability.toInt()}%" binding.sliderFull.addOnChangeListener { _, value, _ -> binding.probabilityFull.text = "${value.toInt()}%" PrefsUtil.fullAdsProbability = value } binding.sliderFullExtra.addOnChangeListener { _, value, _ -> binding.probabilityFullExtra.text = "${value.toInt()}%" PrefsUtil.fullAdsExtraProbability = value } } override fun onSupportNavigateUp(): Boolean { onBackPressedDispatcher.onBackPressed() return super.onSupportNavigateUp() } } ================================================ FILE: app/src/main/java/knf/kuma/preferences/BottomPreferencesFragment.kt ================================================ package knf.kuma.preferences import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.commons.EAHelper class BottomPreferencesFragment : BottomFragment() { var count = 1 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { EAHelper.enter1("C") return inflater.inflate(R.layout.fragment_preferences, container, false) } override fun onReselect() { EAHelper.enter1("C") count++ if (count == 20) AchievementManager.unlock(listOf(40)) } companion object { fun get(): BottomPreferencesFragment { return BottomPreferencesFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/preferences/BottomPreferencesMaterialFragment.kt ================================================ package knf.kuma.preferences import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.commons.EAHelper class BottomPreferencesMaterialFragment : BottomFragment() { var count = 1 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { EAHelper.enter1("C") return inflater.inflate(R.layout.fragment_preferences_material, container, false) } override fun onReselect() { EAHelper.enter1("C") count++ if (count == 20) AchievementManager.unlock(listOf(40)) } companion object { fun get(): BottomPreferencesMaterialFragment { return BottomPreferencesMaterialFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/preferences/ConfigurationFragment.kt ================================================ package knf.kuma.preferences import android.app.Activity import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.View import android.widget.ListView import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.ViewCompat import androidx.lifecycle.Observer import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceManager import androidx.preference.SwitchPreference import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onCancel import com.afollestad.materialdialogs.input.getInputField import com.afollestad.materialdialogs.input.getInputLayout import com.afollestad.materialdialogs.input.input import com.afollestad.materialdialogs.list.listItems import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.Main import knf.kuma.R import knf.kuma.ads.AdsUtils import knf.kuma.backup.Backups import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.DesignUtils import knf.kuma.commons.EAHelper import knf.kuma.commons.FileUtil import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.admFile import knf.kuma.commons.canGroupNotifications import knf.kuma.commons.decrypt import knf.kuma.commons.doOnUI import knf.kuma.commons.encryptOrThrow import knf.kuma.commons.ffFile import knf.kuma.commons.getPackage import knf.kuma.commons.isNull import knf.kuma.commons.noCrash import knf.kuma.commons.safeContext import knf.kuma.commons.safeDelete import knf.kuma.commons.safeShow import knf.kuma.custom.PreferenceFragmentCompat import knf.kuma.database.CacheDB import knf.kuma.directory.DirManager import knf.kuma.directory.DirectoryService import knf.kuma.directory.DirectoryUpdateService import knf.kuma.download.DownloadManagerCentral import knf.kuma.download.FileAccessHelper import knf.kuma.jobscheduler.BackUpWork import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.jobscheduler.RecentsWork import knf.kuma.pojos.AutoBackupObject import knf.kuma.widgets.emision.WEmisionProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jetbrains.anko.support.v4.toast import xdroid.toaster.Toaster import java.io.FileOutputStream class ConfigurationFragment : PreferenceFragmentCompat() { companion object { private const val keyCustomTone = "custom_tone" private const val keyAutoBackup = "auto_backup" private const val keyMaxParallelDownloads = "max_parallel_downloads" private const val keyBufferSize = "buffer_size" private const val keyThemeColor = "theme_color" private const val keyAchievementsPermissions = "achievements_permissions" private const val keyAdsEnabled = "ads_enabled_new" } private var uaChangeListener: UAChangeListener? = null override fun onAttach(activity: Activity) { uaChangeListener = activity as? UAChangeListener super.onAttach(activity) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { if (activity != null && context != null) doOnUI { addPreferencesFromResource(R.xml.preferences) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) preferenceScreen.findPreference(keyCustomTone)?.summary = "Abrir configuración" else if (FileAccessHelper.toneFile.exists()) preferenceScreen.findPreference(keyCustomTone)?.summary = "Personalizado" if (!DesignUtils.isFlat) preferenceScreen.findPreference("recentActionType")?.isVisible = false preferenceScreen.findPreference(keyCustomTone)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) noCrash { startActivity( Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) .putExtra(Settings.EXTRA_CHANNEL_ID, RecentsWork.CHANNEL_RECENTS) .putExtra(Settings.EXTRA_APP_PACKAGE, this@ConfigurationFragment.context?.packageName) ) } else activity?.let { MaterialDialog(it).safeShow { title(text = "Tono de notificación") listItems(items = listOf("Cambiar tono", "Tono de sistema")) { _, index, _ -> when (index) { 0 -> startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType("audio/*"), 4784) 1 -> { FileAccessHelper.toneFile.safeDelete() preferenceScreen.findPreference(keyCustomTone)?.summary = "Sistema" } } } } } true } if (Backups.type == Backups.Type.DROPBOX) { if (Network.isConnected) { activity?.let { preferenceScreen.findPreference(keyAutoBackup)?.summary = "Cargando..." Backups.search(null, Backups.keyAutoBackup) { doOnUI { try { val autoBackupObject = it as? AutoBackupObject if (autoBackupObject != null) { if (autoBackupObject == AutoBackupObject(activity)) preferenceScreen.findPreference( keyAutoBackup )?.summary = "%s" else preferenceScreen.findPreference( keyAutoBackup )?.summary = "Solo " + autoBackupObject.name if (autoBackupObject.value == null) GlobalScope.launch(Dispatchers.Main) { Backups.createService()?.backup( AutoBackupObject(App.context), Backups.keyAutoBackup ) preferenceScreen.findPreference( keyAutoBackup )?.summary = "%s" } else preferenceManager.sharedPreferences?.edit() ?.putString( keyAutoBackup, autoBackupObject.value )?.apply() } else { preferenceScreen.findPreference(keyAutoBackup)?.summary = "%s (NE)" } preferenceScreen.findPreference(keyAutoBackup)?.isEnabled = true } catch (e: Exception) { FirebaseCrashlytics.getInstance().recordException(e) preferenceScreen.findPreference(keyAutoBackup)?.summary = "Error al buscar archivo: ${e.message}" preferenceScreen.findPreference(keyAutoBackup)?.isEnabled = true } } } } } else { preferenceScreen.findPreference(keyAutoBackup)?.summary = "Sin internet" } } else if (Backups.type == Backups.Type.NONE) { preferenceScreen.findPreference(keyAutoBackup)?.summary = "Sin cuenta para respaldos" } else { preferenceScreen.findPreference(keyAutoBackup)?.isVisible = false } preferenceScreen.findPreference(keyAutoBackup)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> BackUpWork.reSchedule(Integer.valueOf((newValue as? String) ?: "0")) GlobalScope.launch(Dispatchers.Main) { Backups.createService()?.backup(AutoBackupObject(App.context, (newValue as? String) ?: "0"), Backups.keyAutoBackup) preferenceScreen.findPreference(keyAutoBackup)?.summary = "%s" } true } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { preferenceScreen.removePreferenceRecursively("download_type") val preferenceDownloads = preferenceScreen.findPreference("download_type_q") preferenceDownloads?.summary = PrefsUtil.storageType preferenceDownloads?.onPreferenceClickListener = Preference.OnPreferenceClickListener { FileAccessHelper.openTreeChooser(this@ConfigurationFragment) Toaster.toastLong("Por favor selecciona la raiz del almacenamiento") true } } else { preferenceScreen.removePreferenceRecursively("download_type_q") preferenceScreen.findPreference("download_type")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue == "1" && !FileAccessHelper.canDownload( this@ConfigurationFragment, newValue as? String ) ) Toaster.toast("Por favor selecciona la raiz de tu SD") else PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() .putString("tree_uri", null).apply() true } } if (PrefsUtil.downloaderType == 0) { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = false preferenceScreen.findPreference(keyBufferSize)?.isEnabled = true } else { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = true preferenceScreen.findPreference(keyBufferSize)?.isEnabled = false } preferenceScreen.findPreference("downloader_type")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue == "0") { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = false preferenceScreen.findPreference(keyBufferSize)?.isEnabled = true } else { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = true preferenceScreen.findPreference(keyBufferSize)?.isEnabled = false } true } preferenceScreen.findPreference("default_useragent")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> GlobalScope.launch(Dispatchers.Main) { delay(1000) uaChangeListener?.onUAChange() } true } preferenceScreen.findPreference("theme_option")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> AppCompatDelegate.setDefaultNightMode(((newValue as? String) ?: "0").toInt()) PreferenceManager.getDefaultSharedPreferences(safeContext).edit().putString("theme_value", newValue.toString()).apply() WEmisionProvider.update(safeContext) activity?.recreate() true } preferenceScreen.findPreference("ads_enabled_new")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (BuildConfig.DEBUG || PrefsUtil.isSubscriptionEnabled) return@OnPreferenceChangeListener true if (newValue == false) { context?.let { FirestoreManager.doSignOut(it) } Backups.type = Backups.Type.NONE } true } preferenceScreen.findPreference("family_friendly_enabled")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> if (newValue == true) { activity?.let { MaterialDialog(it).safeShow { title(text = "Configurar contraseña") input { _, inputText -> MaterialDialog(it).safeShow { title(text = "Repetir contraseña") input { _, input -> if (input.toString() == inputText.toString()) doOnUI(onLog = { PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false toast("Error al encriptar") }) { val encrypted = input.toString().encryptOrThrow() check(encrypted.decrypt() == input.toString()) PrefsUtil.ffPass = encrypted val file = ffFile file.parentFile?.mkdirs() if (!file.exists()) file.createNewFile() file.writeText(encrypted) doAsync { CacheDB.INSTANCE.animeDAO().nukeEcchi() } } else { toast("Las contraseñas no coinciden") PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) onCancel { PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false } } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) onCancel { PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false } } } } else { activity?.let { MaterialDialog(it).safeShow { title(text = "Ingresa contraseña") input { _, input -> doOnUI(onLog = { PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true toast("Error al desencriptar") }) { val file = ffFile if (file.exists() && !admFile.exists()) { val text = file.readText() val decrypt = text.decrypt() if (decrypt == input.toString()) { PrefsUtil.ffPass = "" file.delete() DirectoryUpdateService.run(context) } else { PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true Toaster.toast("Contraseña incorrecta") } } else if (admFile.exists()) { PrefsUtil.ffPass = "" file.delete() DirectoryUpdateService.run(context) } else { if (PrefsUtil.ffPass != "") { val decrypt = PrefsUtil.ffPass.decrypt() if (decrypt == null || decrypt != input.toString()) { file.createNewFile() file.writeText(PrefsUtil.ffPass) PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true Toaster.toast("Contraseña incorrecta") } else { PrefsUtil.ffPass = "" DirectoryUpdateService.run(context) } } } } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) onCancel { PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true } } } } true } preferenceScreen.findPreference("recents_time")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> preferenceScreen.findPreference("notify_favs")?.isEnabled = "0" != newValue RecentsWork.reSchedule(newValue.toString().toInt() * 15) true } preferenceScreen.findPreference("dir_update_time")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> DirUpdateWork.reSchedule(newValue.toString().toInt() * 15) true } preferenceScreen.findPreference("security_blocking_firestore")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> FirestoreManager.start() true } preferenceScreen.findPreference("dir_update")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { try { if (!DirectoryUpdateService.isRunning && !DirectoryService.isRunning) DirectoryUpdateService.run(App.context) else if (DirectoryUpdateService.isRunning) Toaster.toast("Ya se esta actualizando") } catch (e: Exception) { e.printStackTrace() } false } if (!canGroupNotifications) preferenceScreen.removePreference(preferenceScreen.findPreference("group_notifications")!!) preferenceScreen.findPreference("dir_destroy")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { try { if (!DirectoryUpdateService.isRunning && !DirectoryService.isRunning) activity?.let { safe -> MaterialDialog(safe).safeShow { message(text = "¿Desea recrear el directorio?") positiveButton(text = "continuar") { doAsync { CacheDB.INSTANCE.animeDAO().nuke() PrefsUtil.isDirectoryFinished = false DirManager.checkPreDir() DirectoryService.run(safeContext) } } negativeButton(text = "cancelar") } } else if (DirectoryService.isRunning) Toaster.toast("Ya se esta creando") } catch (e: Exception) { e.printStackTrace() } false } when (EAHelper.phase) { 4 -> preferenceScreen.findPreference(keyThemeColor)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> startActivity(Intent(activity, Main::class.java).putExtra("start_position", 3)) activity?.finish() true } 0 -> { val category = preferenceScreen.findPreference("category_design") as? PreferenceCategory category?.removePreference(preferenceScreen.findPreference(keyThemeColor)!!) val pref = Preference(requireContext()) pref.title = "Color de tema" pref.summary = "Resuelve el secreto para desbloquear" pref.setIcon(R.drawable.ic_palette) pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { Toaster.toast(EAHelper.eaMessage) true } category?.addPreference(pref) } else -> { preferenceScreen.findPreference(keyThemeColor)?.summary = "Resuelve el secreto para desbloquear" preferenceScreen.findPreference(keyThemeColor)?.isEnabled = false } } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(this@ConfigurationFragment.context)) (preferenceScreen.findPreference(keyAchievementsPermissions) as? SwitchPreference)?.apply { isChecked = true isEnabled = false } else if (!Settings.canDrawOverlays(this@ConfigurationFragment.context)) { (preferenceScreen.findPreference(keyAchievementsPermissions) as? SwitchPreference)?.apply { isChecked = false isEnabled = true } } preferenceScreen.findPreference(keyAchievementsPermissions)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).setData(Uri.parse("package:${getPackage()}")), 5879) } catch (e: ActivityNotFoundException) { Toaster.toast("No se pudo abrir la configuracion") } true } preferenceScreen.findPreference("hide_chaps")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (!FileAccessHelper.NOMEDIA_CREATING) { FileAccessHelper.checkNoMedia(newValue as? Boolean == true) true } else { (preferenceScreen.findPreference("hide_chaps") as? SwitchPreference)?.isChecked = newValue as? Boolean != true false } } preferenceScreen.findPreference("max_parallel_downloads")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> DownloadManagerCentral.setParallelDownloads(newValue as? String) true } preferenceScreen.findPreference("remember_server")?.apply { val lastServer = PrefsUtil.lastServer if (lastServer.isNull()) isEnabled = false else { summary = lastServer onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> if (newValue as? Boolean == false) { PrefsUtil.lastServer = null preference.summary = null preference.isEnabled = false } true } } } preferenceScreen.findPreference(keyAdsEnabled)?.apply { isChecked = PrefsUtil.isAdsEnabled isEnabled = PrefsUtil.isSubscriptionEnabled || !AdsUtils.remoteConfigs.getBoolean("ads_forced") if (!isEnabled) summary = "Estas en una prueba temporal!" setOnPreferenceChangeListener { _, newValue -> preferenceScreen.findPreference("ads_settings")?.isEnabled = newValue == true true } } preferenceScreen.findPreference("ads_settings")?.apply { isEnabled = PrefsUtil.isAdsEnabled setOnPreferenceClickListener { startActivity(Intent(requireContext(), AdsPreferenceActivity::class.java)) true } } if (BuildConfig.DEBUG) { preferenceScreen.findPreference("reset_recents")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { doAsync { CacheDB.INSTANCE.recentsDAO().clear() RecentsWork.run() } true } } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { view.findViewById(android.R.id.list)?.let { ViewCompat.setNestedScrollingEnabled(it, true) } super.onViewCreated(view, savedInstanceState) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { noCrash { if (requestCode == FileAccessHelper.SD_REQUEST && resultCode == Activity.RESULT_OK) { val validation = FileAccessHelper.isUriValid(data?.data) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") FileAccessHelper.openTreeChooser(this) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) preferenceScreen.findPreference("download_type_q")?.summary = PrefsUtil.storageType } else if (requestCode == 4784 && resultCode == Activity.RESULT_OK) { if (!FileAccessHelper.toneFile.exists()) FileAccessHelper.toneFile.createNewFile() FileUtil.moveFile( safeContext.contentResolver, data?.data, FileOutputStream(FileAccessHelper.toneFile), false) .observe(this, Observer { try { if (it != null) { if (it.second) { if (it.first == -1) { FileAccessHelper.toneFile.safeDelete() Toaster.toast("Error al copiar") } else { Toaster.toast("Tono seleccionado!") } } } } catch (e: Exception) { Toaster.toast("Error al importar") } }) } else if (requestCode == 5879) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context)) (preferenceScreen.findPreference("achievements_permissions") as? SwitchPreference)?.apply { isChecked = true isEnabled = false } else (preferenceScreen.findPreference("achievements_permissions") as? SwitchPreference)?.apply { isChecked = false isEnabled = true } } } } interface UAChangeListener { fun onUAChange() } } ================================================ FILE: app/src/main/java/knf/kuma/preferences/ConfigurationFragmentMaterial.kt ================================================ package knf.kuma.preferences import android.app.Activity import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import android.util.Log import android.view.View import android.widget.ListView import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.ViewCompat import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceManager import androidx.preference.SwitchPreference import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onCancel import com.afollestad.materialdialogs.input.getInputField import com.afollestad.materialdialogs.input.getInputLayout import com.afollestad.materialdialogs.input.input import com.afollestad.materialdialogs.list.listItems import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.App import knf.kuma.BuildConfig import knf.kuma.MainMaterial import knf.kuma.R import knf.kuma.ads.AdsUtils import knf.kuma.backup.Backups import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.DesignUtils import knf.kuma.commons.EAHelper import knf.kuma.commons.FileUtil import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.admFile import knf.kuma.commons.canGroupNotifications import knf.kuma.commons.decrypt import knf.kuma.commons.doOnUI import knf.kuma.commons.ffFile import knf.kuma.commons.getPackage import knf.kuma.commons.isNull import knf.kuma.commons.noCrash import knf.kuma.commons.safeContext import knf.kuma.commons.safeDelete import knf.kuma.commons.safeShow import knf.kuma.custom.PreferenceFragmentCompat import knf.kuma.database.CacheDB import knf.kuma.directory.DirManager import knf.kuma.directory.DirectoryService import knf.kuma.directory.DirectoryUpdateService import knf.kuma.download.DownloadManagerCentral import knf.kuma.download.FileAccessHelper import knf.kuma.jobscheduler.BackUpWork import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.jobscheduler.RecentsWork import knf.kuma.pojos.AutoBackupObject import knf.kuma.widgets.emision.WEmisionProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jetbrains.anko.support.v4.toast import xdroid.toaster.Toaster import java.io.FileOutputStream class ConfigurationFragmentMaterial : PreferenceFragmentCompat() { companion object { private const val keyCustomTone = "custom_tone" private const val keyAutoBackup = "auto_backup" private const val keyMaxParallelDownloads = "max_parallel_downloads" private const val keyBufferSize = "buffer_size" private const val keyThemeColor = "theme_color" private const val keyAchievementsPermissions = "achievements_permissions" private const val keyAdsEnabled = "ads_enabled_new" } private var uaChangeListener: UAChangeListener? = null override fun onAttach(activity: Activity) { uaChangeListener = activity as? UAChangeListener super.onAttach(activity) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { if (activity != null && context != null) doOnUI { addPreferencesFromResource(R.xml.preferences) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) preferenceScreen.findPreference(keyCustomTone)?.summary = "Abrir configuración" else if (FileAccessHelper.toneFile.exists()) preferenceScreen.findPreference(keyCustomTone)?.summary = "Personalizado" if (!DesignUtils.isFlat) preferenceScreen.findPreference("recentActionType")?.isVisible = false preferenceScreen.findPreference(keyCustomTone)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) noCrash { startActivity( Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) .putExtra(Settings.EXTRA_CHANNEL_ID, RecentsWork.CHANNEL_RECENTS) .putExtra(Settings.EXTRA_APP_PACKAGE, this@ConfigurationFragmentMaterial.context?.packageName) ) } else activity?.let { MaterialDialog(it).safeShow { title(text = "Tono de notificación") listItems(items = listOf("Cambiar tono", "Tono de sistema")) { _, index, _ -> when (index) { 0 -> startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType("audio/*"), 4784) 1 -> { FileAccessHelper.toneFile.safeDelete() preferenceScreen.findPreference(keyCustomTone)?.summary = "Sistema" } } } } } true } if (Backups.type == Backups.Type.DROPBOX) { if (Network.isConnected) { activity?.let { preferenceScreen.findPreference(keyAutoBackup)?.summary = "Cargando..." Backups.search(null, Backups.keyAutoBackup) { doOnUI { try { val autoBackupObject = it as? AutoBackupObject if (autoBackupObject != null) { if (autoBackupObject == AutoBackupObject(activity)) preferenceScreen.findPreference( keyAutoBackup )?.summary = "%s" else preferenceScreen.findPreference( keyAutoBackup )?.summary = "Solo " + autoBackupObject.name if (autoBackupObject.value == null) GlobalScope.launch(Dispatchers.Main) { Backups.createService()?.backup( AutoBackupObject(App.context), Backups.keyAutoBackup ) preferenceScreen.findPreference( keyAutoBackup )?.summary = "%s" } else preferenceManager.sharedPreferences?.edit() ?.putString( keyAutoBackup, autoBackupObject.value )?.apply() } else { preferenceScreen.findPreference(keyAutoBackup)?.summary = "%s (NE)" } preferenceScreen.findPreference(keyAutoBackup)?.isEnabled = true } catch (e: Exception) { FirebaseCrashlytics.getInstance().recordException(e) preferenceScreen.findPreference(keyAutoBackup)?.summary = "Error al buscar archivo: ${e.message}" preferenceScreen.findPreference(keyAutoBackup)?.isEnabled = true } } } } } else { preferenceScreen.findPreference(keyAutoBackup)?.summary = "Sin internet" } } else if (Backups.type == Backups.Type.NONE) { preferenceScreen.findPreference(keyAutoBackup)?.summary = "Sin cuenta para respaldos" } else { preferenceScreen.findPreference(keyAutoBackup)?.isVisible = false } preferenceScreen.findPreference(keyAutoBackup)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> BackUpWork.reSchedule(Integer.valueOf((newValue as? String) ?: "0")) GlobalScope.launch(Dispatchers.Main) { Backups.createService()?.backup(AutoBackupObject(App.context, (newValue as? String) ?: "0"), Backups.keyAutoBackup) preferenceScreen.findPreference(keyAutoBackup)?.summary = "%s" } true } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { preferenceScreen.removePreferenceRecursively("download_type") val preferenceDownloads = preferenceScreen.findPreference("download_type_q") preferenceDownloads?.summary = PrefsUtil.storageType preferenceDownloads?.onPreferenceClickListener = Preference.OnPreferenceClickListener { FileAccessHelper.openTreeChooser(this@ConfigurationFragmentMaterial) Toaster.toastLong("Por favor selecciona la raiz del almacenamiento") true } } else { preferenceScreen.removePreferenceRecursively("download_type_q") preferenceScreen.findPreference("download_type")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue == "1" && !FileAccessHelper.canDownload( this@ConfigurationFragmentMaterial, newValue as? String ) ) Toaster.toast("Por favor selecciona la raiz de tu SD") else PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() .putString("tree_uri", null).apply() true } } if (PrefsUtil.downloaderType == 0) { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = false preferenceScreen.findPreference(keyBufferSize)?.isEnabled = true } else { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = true preferenceScreen.findPreference(keyBufferSize)?.isEnabled = false } preferenceScreen.findPreference("downloader_type")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue == "0") { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = false preferenceScreen.findPreference(keyBufferSize)?.isEnabled = true } else { preferenceScreen.findPreference(keyMaxParallelDownloads)?.isEnabled = true preferenceScreen.findPreference(keyBufferSize)?.isEnabled = false } true } preferenceScreen.findPreference("default_useragent")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> GlobalScope.launch(Dispatchers.Main) { delay(1000) uaChangeListener?.onUAChange() } true } preferenceScreen.findPreference("theme_option")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> AppCompatDelegate.setDefaultNightMode(((newValue as? String) ?: "0").toInt()) PreferenceManager.getDefaultSharedPreferences(safeContext).edit().putString("theme_value", newValue.toString()).apply() WEmisionProvider.update(safeContext) activity?.recreate() true } preferenceScreen.findPreference("ads_enabled_new")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (BuildConfig.DEBUG || PrefsUtil.isSubscriptionEnabled) return@OnPreferenceChangeListener true if (newValue == false) { context?.let { FirestoreManager.doSignOut(it) } Backups.type = Backups.Type.NONE } true } preferenceScreen.findPreference("family_friendly_enabled")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> if (newValue == true) { activity?.let { MaterialDialog(it).safeShow { title(text = "Configurar contraseña") input { _, inputText -> MaterialDialog(it).safeShow { title(text = "Repetir contraseña") input { _, input -> if (input.toString() == inputText.toString()) doOnUI(onLog = { PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false toast("Error al encriptar") }) { val encrypted = input.toString().hashCode().toString() PrefsUtil.ffPass = encrypted val file = ffFile file.parentFile?.mkdirs() if (!file.exists()) file.createNewFile() file.writeText(encrypted) doAsync { CacheDB.INSTANCE.animeDAO().nukeEcchi() } } else { toast("Las contraseñas no coinciden") PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) onCancel { PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false } } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) onCancel { PrefsUtil.isFamilyFriendly = false preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = false } } } } else { activity?.let { MaterialDialog(it).safeShow { title(text = "Ingresa contraseña") input { _, input -> doOnUI(onLog = { PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true toast("Error al desencriptar") }) { val file = ffFile if (file.exists() && !admFile.exists()) { val text = file.readText() Log.e("Compare pass","$text == ${input.toString().hashCode()}") if (text == input.toString().hashCode().toString() || text.decrypt() == input.toString()) { PrefsUtil.ffPass = "" file.delete() DirectoryUpdateService.run(context) } else { PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true Toaster.toast("Contraseña incorrecta") } } else if (admFile.exists()) { PrefsUtil.ffPass = "" file.delete() DirectoryUpdateService.run(context) } else { if (PrefsUtil.ffPass != "") { val isCorrect = input.hashCode().toString() == PrefsUtil.ffPass || PrefsUtil.ffPass.decrypt() == input.toString() if (!isCorrect) { file.createNewFile() file.writeText(PrefsUtil.ffPass) PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true Toaster.toast("Contraseña incorrecta") } else { PrefsUtil.ffPass = "" DirectoryUpdateService.run(context) } } } } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) onCancel { PrefsUtil.isFamilyFriendly = true preferenceScreen.findPreference("family_friendly_enabled")?.isChecked = true } } } } true } preferenceScreen.findPreference("recents_time")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> preferenceScreen.findPreference("notify_favs")?.isEnabled = "0" != newValue RecentsWork.reSchedule(newValue.toString().toInt() * 15) true } preferenceScreen.findPreference("dir_update_time")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> DirUpdateWork.reSchedule(newValue.toString().toInt() * 15) true } preferenceScreen.findPreference("security_blocking_firestore")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> FirestoreManager.start() true } preferenceScreen.findPreference("dir_update")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { try { if (!DirectoryUpdateService.isRunning && !DirectoryService.isRunning) DirectoryUpdateService.run(App.context) else if (DirectoryUpdateService.isRunning) Toaster.toast("Ya se esta actualizando") } catch (e: Exception) { e.printStackTrace() } false } if (!canGroupNotifications) preferenceScreen.removePreference(preferenceScreen.findPreference("group_notifications")!!) preferenceScreen.findPreference("dir_destroy")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { try { if (!DirectoryUpdateService.isRunning && !DirectoryService.isRunning) activity?.let { safe -> MaterialDialog(safe).safeShow { message(text = "¿Desea recrear el directorio?") positiveButton(text = "continuar") { doAsync { CacheDB.INSTANCE.animeDAO().nuke() PrefsUtil.isDirectoryFinished = false DirManager.checkPreDir() DirectoryService.run(safeContext) } } negativeButton(text = "cancelar") } } else if (DirectoryService.isRunning) Toaster.toast("Ya se esta creando") } catch (e: Exception) { e.printStackTrace() } false } when (EAHelper.phase) { 4 -> preferenceScreen.findPreference(keyThemeColor)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> startActivity(Intent(activity, MainMaterial::class.java).putExtra("start_position", 3)) activity?.finish() true } 0 -> { val category = preferenceScreen.findPreference("category_design") as? PreferenceCategory category?.removePreference(preferenceScreen.findPreference(keyThemeColor)!!) val pref = Preference(requireContext()) pref.title = "Color de tema" pref.summary = "Resuelve el secreto para desbloquear" pref.setIcon(R.drawable.ic_palette) pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { Toaster.toast(EAHelper.eaMessage) true } category?.addPreference(pref) } else -> { preferenceScreen.findPreference(keyThemeColor)?.summary = "Resuelve el secreto para desbloquear" preferenceScreen.findPreference(keyThemeColor)?.isEnabled = false } } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(this@ConfigurationFragmentMaterial.context)) (preferenceScreen.findPreference(keyAchievementsPermissions) as? SwitchPreference)?.apply { isChecked = true isEnabled = false } else if (!Settings.canDrawOverlays(this@ConfigurationFragmentMaterial.context)) { (preferenceScreen.findPreference(keyAchievementsPermissions) as? SwitchPreference)?.apply { isChecked = false isEnabled = true } } preferenceScreen.findPreference(keyAchievementsPermissions)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).setData(Uri.parse("package:${getPackage()}")), 5879) } catch (e: ActivityNotFoundException) { Toaster.toast("No se pudo abrir la configuracion") } true } preferenceScreen.findPreference("hide_chaps")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (!FileAccessHelper.NOMEDIA_CREATING) { FileAccessHelper.checkNoMedia(newValue as? Boolean == true) true } else { (preferenceScreen.findPreference("hide_chaps") as? SwitchPreference)?.isChecked = newValue as? Boolean != true false } } preferenceScreen.findPreference("max_parallel_downloads")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> DownloadManagerCentral.setParallelDownloads(newValue as? String) true } preferenceScreen.findPreference("remember_server")?.apply { val lastServer = PrefsUtil.lastServer if (lastServer.isNull()) isEnabled = false else { summary = lastServer onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> if (newValue as? Boolean == false) { PrefsUtil.lastServer = null preference.summary = null preference.isEnabled = false } true } } } preferenceScreen.findPreference(keyAdsEnabled)?.apply { isChecked = PrefsUtil.isAdsEnabled isEnabled = PrefsUtil.isSubscriptionEnabled || !AdsUtils.remoteConfigs.getBoolean("ads_forced") if (!isEnabled) summary = "Estas en una prueba temporal!" setOnPreferenceChangeListener { _, newValue -> preferenceScreen.findPreference("ads_settings")?.isEnabled = newValue == true true } } preferenceScreen.findPreference("ads_settings")?.apply { isEnabled = PrefsUtil.isAdsEnabled setOnPreferenceClickListener { startActivity(Intent(requireContext(), AdsPreferenceActivity::class.java)) true } } if (BuildConfig.DEBUG) { preferenceScreen.findPreference("reset_recents")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { doAsync { CacheDB.INSTANCE.recentsDAO().clear() RecentsWork.run() } true } } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { view.findViewById(android.R.id.list)?.let { ViewCompat.setNestedScrollingEnabled(it, true) } super.onViewCreated(view, savedInstanceState) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { noCrash { if (requestCode == FileAccessHelper.SD_REQUEST && resultCode == Activity.RESULT_OK) { val validation = FileAccessHelper.isUriValid(data?.data) if (!validation.isValid) { Toaster.toast("Directorio invalido: $validation") FileAccessHelper.openTreeChooser(this) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) preferenceScreen.findPreference("download_type_q")?.summary = PrefsUtil.storageType } else if (requestCode == 4784 && resultCode == Activity.RESULT_OK) { if (!FileAccessHelper.toneFile.exists()) FileAccessHelper.toneFile.createNewFile() FileUtil.moveFile( safeContext.contentResolver, data?.data, FileOutputStream(FileAccessHelper.toneFile), false) .observe(this, { try { if (it != null) { if (it.second) { if (it.first == -1) { FileAccessHelper.toneFile.safeDelete() Toaster.toast("Error al copiar") } else { Toaster.toast("Tono seleccionado!") } } } } catch (e: Exception) { Toaster.toast("Error al importar") } }) } else if (requestCode == 5879) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context)) (preferenceScreen.findPreference("achievements_permissions") as? SwitchPreference)?.apply { isChecked = true isEnabled = false } else (preferenceScreen.findPreference("achievements_permissions") as? SwitchPreference)?.apply { isChecked = false isEnabled = true } } } } interface UAChangeListener { fun onUAChange() } } ================================================ FILE: app/src/main/java/knf/kuma/profile/TopActivity.kt ================================================ package knf.kuma.profile import android.content.Context import android.content.Intent import android.graphics.Color import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.recyclerview.widget.DividerItemDecoration import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.input.getInputField import com.afollestad.materialdialogs.input.getInputLayout import com.afollestad.materialdialogs.input.input import com.google.android.material.snackbar.Snackbar import com.google.firebase.auth.UserProfileChangeRequest import com.google.firebase.firestore.ListenerRegistration import knf.kuma.R import knf.kuma.ads.AdsUtils import knf.kuma.ads.FullscreenAdLoader import knf.kuma.ads.getFAdLoaderInterstitial import knf.kuma.ads.getFAdLoaderRewarded import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.backup.firestore.data.TopData import knf.kuma.commons.EAHelper import knf.kuma.commons.Economy import knf.kuma.commons.PrefsUtil import knf.kuma.commons.diceOf import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.databinding.RecyclerLoaderBinding import org.jetbrains.anko.doAsync import org.jetbrains.anko.toast import kotlin.contracts.ExperimentalContracts class TopActivity : GenericActivity() { private lateinit var listener: ListenerRegistration private var snackbar: Snackbar? = null private var isEditing = false private val topAdapter: TopAdapter by lazy { TopAdapter() } private val rewardedAd: FullscreenAdLoader by lazy { getFAdLoaderRewarded(this) } private var interstitial: FullscreenAdLoader = getFAdLoaderInterstitial(this) private var topList: List = emptyList() private val binding by lazy { RecyclerLoaderBinding.inflate(layoutInflater) } private fun showAd() { diceOf<() -> Unit> { put({ rewardedAd.show() }, AdsUtils.remoteConfigs.getDouble("rewarded_percent")) put({ interstitial.show() }, AdsUtils.remoteConfigs.getDouble("interstitial_percent")) }() } @OptIn(ExperimentalContracts::class) override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) setSupportActionBar(binding.toolbar) title = "Videos vistos" supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { finish() } binding.recycler.apply { addItemDecoration(DividerItemDecoration(this@TopActivity, DividerItemDecoration.VERTICAL)) adapter = topAdapter } binding.loading.show() listener = FirestoreManager.listenTop { topList -> this.topList = topList reload() } rewardedAd.load() interstitial.load() } private fun reload() { doOnUI { binding.loading.show() } doAsync { val sorted = topList.sortedByDescending { it.number } val list = sorted.take(PrefsUtil.topCount).mapIndexed { index, topData -> TopItem(index + 1, topData) }.toMutableList() val current = FirestoreManager.uid?.let { uid -> sorted.find { it.uid == uid } } val currentPosition = current?.let { sorted.indexOf(it) } ?: 999 if (current != null && currentPosition > PrefsUtil.topCount - 1) list.add(TopItem(currentPosition + 1, current)) doOnUI { binding.loading.hide() topAdapter.submitList(list) } } } private fun showSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT) { snackbar?.dismiss() snackbar = Snackbar.make(binding.recycler, text, duration).also { it.show() } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_top, menu) when (PrefsUtil.topCount) { 25 -> menu.findItem(R.id.top25)?.isChecked = true 50 -> menu.findItem(R.id.top50)?.isChecked = true 75 -> menu.findItem(R.id.top75)?.isChecked = true 100 -> menu.findItem(R.id.top100)?.isChecked = true } return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.coins -> { Economy.showWallet(this) { showAd() } } R.id.editName -> { if (!isEditing) MaterialDialog(this).safeShow { title(text = "Editar nombre") input(prefill = FirestoreManager.user?.displayName ?: PrefsUtil.instanceName, maxLength = 30) { _, text -> val checked = text.replace("[^\\wA-zÀ-ú &\\-()\\[\\]\"#\$!¿?¡%{}@_/]*".toRegex(), "").trim() if (checked.isEmpty() || checked.length <= 3) { if (checked.isEmpty()) toast("El nombre no puede estar vacío o con caracteres invalidos") else if (checked.length <= 3) toast("El nombre debe tener mas de 3 caracteres") return@input } if (FirestoreManager.isLoggedIn) { isEditing = true FirestoreManager.user?.updateProfile(UserProfileChangeRequest.Builder().setDisplayName(checked).build())?.apply { addOnSuccessListener { isEditing = false showSnackbar("Nombre editado exitosamente") FirestoreManager.updateTop() } addOnFailureListener { isEditing = false showSnackbar("Error al editar nombre\n${it.message}", Snackbar.LENGTH_LONG) } } ?: showSnackbar("Error al editar nombre") showSnackbar("Editando nombre...", Snackbar.LENGTH_INDEFINITE) } else { PrefsUtil.instanceName = checked showSnackbar("Nombre editado exitosamente") FirestoreManager.updateTop() } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) } } R.id.top25 -> { PrefsUtil.topCount = 25 reload() invalidateOptionsMenu() } R.id.top50 -> { PrefsUtil.topCount = 50 reload() invalidateOptionsMenu() } R.id.top75 -> { PrefsUtil.topCount = 75 reload() invalidateOptionsMenu() } R.id.top100 -> { PrefsUtil.topCount = 100 reload() invalidateOptionsMenu() } } return super.onOptionsItemSelected(item) } override fun onDestroy() { super.onDestroy() if (::listener.isInitialized) listener.remove() } companion object { fun open(context: Context) { context.startActivity(Intent(context, TopActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/profile/TopActivityMaterial.kt ================================================ package knf.kuma.profile import android.content.Context import android.content.Intent import android.graphics.Color import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.input.getInputField import com.afollestad.materialdialogs.input.getInputLayout import com.afollestad.materialdialogs.input.input import com.google.android.material.snackbar.Snackbar import com.google.firebase.auth.UserProfileChangeRequest import com.google.firebase.firestore.ListenerRegistration import knf.kuma.R import knf.kuma.ads.AdsUtils import knf.kuma.ads.FullscreenAdLoader import knf.kuma.ads.getFAdLoaderInterstitial import knf.kuma.ads.getFAdLoaderRewarded import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.backup.firestore.data.TopData import knf.kuma.commons.EAHelper import knf.kuma.commons.Economy import knf.kuma.commons.PrefsUtil import knf.kuma.commons.diceOf import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.databinding.RecyclerLoaderMaterialBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.anko.toast import kotlin.contracts.ExperimentalContracts class TopActivityMaterial : GenericActivity() { private lateinit var listener: ListenerRegistration private var snackbar: Snackbar? = null private var isEditing = false private val topAdapter: TopAdapter by lazy { TopAdapter() } private val rewardedAd: FullscreenAdLoader by lazy { getFAdLoaderRewarded(this) } private var interstitial: FullscreenAdLoader = getFAdLoaderInterstitial(this) private val binding by lazy { RecyclerLoaderMaterialBinding.inflate(layoutInflater) } private var topList: List = emptyList() private val sync: String? by lazy { FirestoreManager.updateTopSync() } private fun showAd() { diceOf<() -> Unit> { put({ rewardedAd.show() }, AdsUtils.remoteConfigs.getDouble("rewarded_percent")) put({ interstitial.show() }, AdsUtils.remoteConfigs.getDouble("interstitial_percent")) }() } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) setSupportActionBar(binding.toolbar) title = "Videos vistos" supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { finish() } binding.recycler.apply { addItemDecoration(DividerItemDecoration(this@TopActivityMaterial, DividerItemDecoration.VERTICAL)) adapter = topAdapter } binding.loading.show() listen() rewardedAd.load() interstitial.load() } @OptIn(ExperimentalContracts::class) private fun listen() { if (::listener.isInitialized) listener.remove() listener = FirestoreManager.listenTop { topList -> this.topList = topList reload() } } private fun reload() { lifecycleScope.launch(Dispatchers.IO) { sync launch(Dispatchers.Main) { binding.loading.show() } val sorted = topList.sortedByDescending { it.number } val list = sorted.take(PrefsUtil.topCount).mapIndexed { index, topData -> TopItem(index + 1, topData) }.toMutableList() val current = FirestoreManager.uid?.let { uid -> sorted.find { it.uid == uid } } val currentPosition = current?.let { sorted.indexOf(it) } ?: 999 if (current != null && currentPosition > PrefsUtil.topCount - 1) list.add(TopItem(currentPosition + 1, current)) launch(Dispatchers.Main) { binding.loading.hide() topAdapter.submitList(list) } } } private fun showSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT) { snackbar?.dismiss() snackbar = Snackbar.make(binding.recycler, text, duration).also { it.show() } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_top, menu) when (PrefsUtil.topCount) { 25 -> menu.findItem(R.id.top25)?.isChecked = true 50 -> menu.findItem(R.id.top50)?.isChecked = true 75 -> menu.findItem(R.id.top75)?.isChecked = true 100 -> menu.findItem(R.id.top100)?.isChecked = true } return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.coins -> { Economy.showWallet(this) { showAd() } } R.id.editName -> { if (!isEditing) MaterialDialog(this).safeShow { title(text = "Editar nombre") input(prefill = FirestoreManager.user?.displayName ?: PrefsUtil.instanceName, maxLength = 30) { _, text -> val checked = text.replace("[^\\wA-zÀ-ú &\\-()\\[\\]\"#\$!¿?¡%{}@_/]*".toRegex(), "").trim() if (checked.isEmpty() || checked.length <= 3) { if (checked.isEmpty()) toast("El nombre no puede estar vacío o con caracteres invalidos") else if (checked.length <= 3) toast("El nombre debe tener mas de 3 caracteres") return@input } if (FirestoreManager.isLoggedIn) { isEditing = true FirestoreManager.user?.updateProfile(UserProfileChangeRequest.Builder().setDisplayName(checked).build())?.apply { addOnSuccessListener { isEditing = false showSnackbar("Nombre editado exitosamente") FirestoreManager.updateTop() } addOnFailureListener { isEditing = false showSnackbar("Error al editar nombre\n${it.message}", Snackbar.LENGTH_LONG) } } ?: showSnackbar("Error al editar nombre") showSnackbar("Editando nombre...", Snackbar.LENGTH_INDEFINITE) } else { PrefsUtil.instanceName = checked showSnackbar("Nombre editado exitosamente") FirestoreManager.updateTop() } } getInputLayout().boxBackgroundColor = Color.TRANSPARENT getInputField().setBackgroundColor(Color.TRANSPARENT) } } R.id.top25 -> { PrefsUtil.topCount = 25 reload() invalidateOptionsMenu() } R.id.top50 -> { PrefsUtil.topCount = 50 reload() invalidateOptionsMenu() } R.id.top75 -> { PrefsUtil.topCount = 75 reload() invalidateOptionsMenu() } R.id.top100 -> { PrefsUtil.topCount = 100 reload() invalidateOptionsMenu() } } return super.onOptionsItemSelected(item) } override fun onDestroy() { super.onDestroy() if (::listener.isInitialized) listener.remove() } companion object { fun open(context: Context) { context.startActivity(Intent(context, TopActivityMaterial::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/profile/TopAdapter.kt ================================================ package knf.kuma.profile import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.inflate import org.jetbrains.anko.find class TopAdapter : ListAdapter(TopItem.diffCallback) { private val uid = FirestoreManager.uid override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder = when (viewType) { 1 -> ItemHolder(parent.inflate(R.layout.item_top)) else -> ItemHolder(parent.inflate(R.layout.item_top_current)) } override fun onBindViewHolder(holder: ItemHolder, position: Int) { holder.bind(getItem(position), position) } override fun getItemViewType(position: Int): Int = if (getItem(position).data.uid == uid) 0 else 1 class ItemHolder(val view: View) : RecyclerView.ViewHolder(view) { fun bind(item: TopItem, position: Int) { view.find(R.id.ranking).text = "#${item.position}" view.find(R.id.name).text = item.data.name view.find(R.id.counter).text = item.data.number.toString() view.find(R.id.trophy).apply { visibility = if (position in 0..2) { setImageResource(when (position) { 0 -> R.drawable.ic_trophy_gold 1 -> R.drawable.ic_trophy_silver else -> R.drawable.ic_trophy_bronze }) View.VISIBLE } else View.INVISIBLE } } } } ================================================ FILE: app/src/main/java/knf/kuma/profile/TopItem.kt ================================================ package knf.kuma.profile import androidx.recyclerview.widget.DiffUtil import knf.kuma.backup.firestore.data.TopData data class TopItem(val position: Int, val data: TopData) { companion object { val diffCallback = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: TopItem, newItem: TopItem): Boolean = oldItem.data.uid == newItem.data.uid override fun areContentsTheSame(oldItem: TopItem, newItem: TopItem): Boolean { return oldItem.position == newItem.position && oldItem.data.name == newItem.data.name && oldItem.data.number == newItem.data.number } } } } ================================================ FILE: app/src/main/java/knf/kuma/queue/ItemTouchHelperAdapter.kt ================================================ package knf.kuma.queue interface ItemTouchHelperAdapter { fun onItemMove(fromPosition: Int, toPosition: Int) fun onItemDismiss(position: Int) } ================================================ FILE: app/src/main/java/knf/kuma/queue/NoTouchHelperCallback.kt ================================================ package knf.kuma.queue import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView class NoTouchHelperCallback : ItemTouchHelper.Callback() { override fun isLongPressDragEnabled(): Boolean { return false } override fun isItemViewSwipeEnabled(): Boolean { return false } override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { return makeMovementFlags(0, 0) } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueActivity.kt ================================================ package knf.kuma.queue import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.view.animation.AnimationUtils import android.widget.FrameLayout import android.widget.LinearLayout import androidx.activity.addCallback import androidx.annotation.LayoutRes import androidx.appcompat.widget.Toolbar import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.gridColumns import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantGridLayoutManager import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.pojos.QueueObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster class QueueActivity : GenericActivity(), QueueAnimesAdapter.OnAnimeSelectedListener, QueueAllAdapter.OnStartDragListener { val toolbar: Toolbar by bind(R.id.toolbar) val recyclerView: RecyclerView by bind(R.id.recycler) private val listToolbar: Toolbar by bind(R.id.list_toolbar) private val listRecyclerView: RecyclerView by bind(R.id.list_recycler) val cardView: MaterialCardView by bind(R.id.bottom_card) val errorView: View by bind(R.id.error) internal var bottomSheetBehavior: BottomSheetBehavior? = null private var listAdapter: QueueListAdapter? = null private var mItemTouchHelper: ItemTouchHelper? = null private var current: QueueObject? = null private var currentData: LiveData> = MutableLiveData() private var isFirst = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.activity_queue } else { R.layout.activity_queue_grid } @SuppressLint("CheckResult") override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(layout) toolbar.title = "Pendientes" setSupportActionBar(toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } menuInflater.inflate(R.menu.menu_play_queue, listToolbar.menu) listToolbar.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.play -> { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN QueueManager.startQueue(applicationContext, listAdapter?.list ?: listOf()) } R.id.clear -> MaterialDialog(this@QueueActivity).safeShow { message(text = "¿Remover los episodios pendientes?") positiveButton(text = "remover") { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN QueueManager.remove(listAdapter?.list ?: mutableListOf()) } negativeButton(text = "cancelar") } } true } bottomSheetBehavior = BottomSheetBehavior.from(cardView) bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN val backCallback = onBackPressedDispatcher.addCallback(this, false) { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN } bottomSheetBehavior?.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { backCallback.isEnabled = newState == BottomSheetBehavior.STATE_EXPANDED if (newState == BottomSheetBehavior.STATE_HIDDEN) current = null } override fun onSlide(bottomSheet: View, slideOffset: Float) { } }) setLayoutManager(!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("queue_is_grouped", true)) listRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayout.VERTICAL)) listAdapter = QueueListAdapter { closeSheet() } listRecyclerView.adapter = listAdapter find(R.id.adContainer).implBanner(AdsType.QUEUE_BANNER, true) /*Aesthetic.get().colorAccent().take(1).subscribe { listToolbar.backgroundColor = it }*/ reload() if (savedInstanceState != null && savedInstanceState.getBoolean("isOpen", false)) onSelect(savedInstanceState.getSerializable("current") as QueueObject) } private fun reload() { currentData.removeObservers(this@QueueActivity) if (PreferenceManager.getDefaultSharedPreferences(this@QueueActivity).getBoolean("queue_is_grouped", true)) { currentData = CacheDB.INSTANCE.queueDAO().all currentData.observe(this@QueueActivity, Observer { list -> errorView.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE val animesAdapter = QueueAnimesAdapter(this@QueueActivity) recyclerView.adapter = animesAdapter dettachHelper() mItemTouchHelper = ItemTouchHelper(NoTouchHelperCallback()) mItemTouchHelper?.attachToRecyclerView(recyclerView) doAsync { animesAdapter.update(QueueObject.takeOne(list)) if (isFirst) { isFirst = false openInitial(list) } } }) } else { currentData = CacheDB.INSTANCE.queueDAO().allAsort currentData.observe(this@QueueActivity, object : Observer> { override fun onChanged(value: MutableList) { isFirst = false clearInterfaces() errorView.visibility = if (value.isEmpty()) View.VISIBLE else View.GONE val allAdapter = QueueAllAdapter(this@QueueActivity) recyclerView.adapter = allAdapter dettachHelper() mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(allAdapter)) mItemTouchHelper?.attachToRecyclerView(recyclerView) allAdapter.update(value) currentData.removeObserver(this) } }) } } private fun openInitial(list: List) { val initialID = intent.getStringExtra("initial") ?: return list.forEach { if (it.chapter.aid == initialID) { onSelect(it) return@forEach } } } private fun dettachHelper() { if (mItemTouchHelper != null) mItemTouchHelper?.attachToRecyclerView(null) } private fun clearInterfaces() { if (recyclerView.adapter is QueueAnimesAdapter) (recyclerView.adapter as QueueAnimesAdapter).clear() } private fun setLayoutManager(isFull: Boolean) { if (isFull || PrefsUtil.layType == "0") { recyclerView.layoutManager = VariantLinearLayoutManager(this) recyclerView.layoutAnimation = AnimationUtils.loadLayoutAnimation(this, R.anim.layout_fall_down) } else { recyclerView.layoutManager = VariantGridLayoutManager(this, gridColumns()) recyclerView.layoutAnimation = AnimationUtils.loadLayoutAnimation(this, R.anim.grid_fall_down) } } override fun onSelect(queueObject: QueueObject) { if (queueObject.equalsAnime(current)) { closeSheet() } else { doOnUI { try { listToolbar.title = queueObject.chapter.name lifecycleScope.launch(Dispatchers.Main) { val list = withContext(Dispatchers.IO) { CacheDB.INSTANCE.queueDAO().getByAidUnique(queueObject.chapter.aid) } if (list.isEmpty()) bottomSheetBehavior?.setState(BottomSheetBehavior.STATE_HIDDEN) else { listAdapter?.update( queueObject.chapter.aid, withContext(Dispatchers.IO) { try { list.sortedBy { it.chapter.number.substringAfterLast(" ").toFloat() }.toMutableList() } catch (_: Exception) { list } } ) bottomSheetBehavior?.setState(BottomSheetBehavior.STATE_EXPANDED) } current = queueObject } } catch (_: Exception) { doAsync { CacheDB.INSTANCE.queueDAO().allRaw.forEach { try { it.chapter.aid } catch (_: Exception) { CacheDB.INSTANCE.queueDAO().remove(it) } } syncData { queue() } } } } } } private fun closeSheet() { current = null bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN } override fun onStartDrag(holder: RecyclerView.ViewHolder) { mItemTouchHelper?.startDrag(holder) } override fun onListCleared() { errorView.post { errorView.visibility = View.VISIBLE } } override fun onCreateOptionsMenu(menu: Menu): Boolean { if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("queue_is_grouped", true)) menuInflater.inflate(R.menu.menu_queue_group, menu) else menuInflater.inflate(R.menu.menu_queue_list, menu) val preferences = PreferenceManager.getDefaultSharedPreferences(this) if (!preferences.getBoolean("is_queue_info_shown", false)) { preferences.edit().putBoolean("is_queue_info_shown", true).apply() onOptionsItemSelected(menu.findItem(R.id.info)) } return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN when (item.itemId) { R.id.info -> MaterialDialog(this).safeShow { message(text = "Los episodios añadidos desde servidor podrían dejar de funcionar después de días sin reproducir") positiveButton(text = "OK") } R.id.queue_group -> { PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("queue_is_grouped", true).apply() setLayoutManager(false) reload() invalidateOptionsMenu() } R.id.queue_list -> { PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("queue_is_grouped", false).apply() setLayoutManager(true) reload() invalidateOptionsMenu() } R.id.play -> { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN val list = (recyclerView.adapter as? QueueAllAdapter)?.list ?: mutableListOf() if (list.size > 0) QueueManager.startQueue(applicationContext, list) else Toaster.toast("La lista esta vacia") } } return super.onOptionsItemSelected(item) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) if (current != null) { outState.putSerializable("current", current) outState.putBoolean("isOpen", true) } else outState.putBoolean("isOpen", false) } companion object { fun open(context: Context?) { context ?: return context.startActivity(Intent(context, QueueActivity::class.java)) } fun open(context: Context?, aid: String) { context ?: return context.startActivity(Intent(context, QueueActivity::class.java).apply { putExtra("initial", aid) }) } } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueActivityMaterial.kt ================================================ package knf.kuma.queue import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.view.animation.AnimationUtils import android.widget.FrameLayout import android.widget.LinearLayout import androidx.activity.addCallback import androidx.annotation.LayoutRes import androidx.appcompat.widget.Toolbar import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.gridColumns import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantGridLayoutManager import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.pojos.QueueObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster class QueueActivityMaterial : GenericActivity(), QueueAnimesAdapterMaterial.OnAnimeSelectedListener, QueueAllAdapterMaterial.OnStartDragListener { val toolbar: Toolbar by bind(R.id.toolbar) val recyclerView: RecyclerView by bind(R.id.recycler) private val listToolbar: Toolbar by bind(R.id.list_toolbar) private val listRecyclerView: RecyclerView by bind(R.id.list_recycler) val cardView: MaterialCardView by bind(R.id.bottom_card) val errorView: View by bind(R.id.error) internal var bottomSheetBehavior: BottomSheetBehavior? = null private var listAdapter: QueueListAdapter? = null private var mItemTouchHelper: ItemTouchHelper? = null private var current: QueueObject? = null private var currentData: LiveData> = MutableLiveData() private var isFirst = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.activity_queue_material } else { R.layout.activity_queue_grid_material } @SuppressLint("CheckResult") override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(layout) toolbar.title = "Pendientes" setSupportActionBar(toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } menuInflater.inflate(R.menu.menu_play_queue, listToolbar.menu) listToolbar.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.play -> { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN QueueManager.startQueue(applicationContext, listAdapter?.list ?: listOf()) } R.id.clear -> MaterialDialog(this@QueueActivityMaterial).safeShow { message(text = "¿Remover los episodios pendientes?") positiveButton(text = "remover") { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN QueueManager.remove(listAdapter?.list ?: mutableListOf()) } negativeButton(text = "cancelar") } } true } bottomSheetBehavior = BottomSheetBehavior.from(cardView) bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN val backCallback = onBackPressedDispatcher.addCallback(this, false) { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN } bottomSheetBehavior?.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { backCallback.isEnabled = newState == BottomSheetBehavior.STATE_EXPANDED if (newState == BottomSheetBehavior.STATE_HIDDEN) current = null } override fun onSlide(bottomSheet: View, slideOffset: Float) { } }) setLayoutManager(!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("queue_is_grouped", true)) listRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayout.VERTICAL)) listAdapter = QueueListAdapter { closeSheet() } listRecyclerView.adapter = listAdapter find(R.id.adContainer).implBanner(AdsType.QUEUE_BANNER, true) /*Aesthetic.get().colorAccent().take(1).subscribe { listToolbar.backgroundColor = it }*/ reload() if (savedInstanceState != null && savedInstanceState.getBoolean("isOpen", false)) onSelect(savedInstanceState.getSerializable("current") as QueueObject) } private fun reload() { currentData.removeObservers(this@QueueActivityMaterial) if (PreferenceManager.getDefaultSharedPreferences(this@QueueActivityMaterial).getBoolean("queue_is_grouped", true)) { currentData = CacheDB.INSTANCE.queueDAO().all currentData.observe(this@QueueActivityMaterial, Observer { list -> errorView.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE val animesAdapter = QueueAnimesAdapterMaterial(this@QueueActivityMaterial) recyclerView.adapter = animesAdapter dettachHelper() mItemTouchHelper = ItemTouchHelper(NoTouchHelperCallback()) mItemTouchHelper?.attachToRecyclerView(recyclerView) doAsync { animesAdapter.update(QueueObject.takeOne(list)) if (isFirst) { isFirst = false openInitial(list) } } }) } else { currentData = CacheDB.INSTANCE.queueDAO().allAsort currentData.observe(this@QueueActivityMaterial, object : Observer> { override fun onChanged(value: MutableList) { isFirst = false clearInterfaces() errorView.visibility = if (value.isEmpty()) View.VISIBLE else View.GONE val allAdapter = QueueAllAdapterMaterial(this@QueueActivityMaterial) recyclerView.adapter = allAdapter dettachHelper() mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(allAdapter)) mItemTouchHelper?.attachToRecyclerView(recyclerView) allAdapter.update(value) currentData.removeObserver(this) } }) } } private fun openInitial(list: List) { val initialID = intent.getStringExtra("initial") ?: return list.forEach { if (it.chapter.aid == initialID) { onSelect(it) return@forEach } } } private fun dettachHelper() { if (mItemTouchHelper != null) mItemTouchHelper?.attachToRecyclerView(null) } private fun clearInterfaces() { if (recyclerView.adapter is QueueAnimesAdapterMaterial) (recyclerView.adapter as QueueAnimesAdapterMaterial).clear() } private fun setLayoutManager(isFull: Boolean) { if (isFull || PrefsUtil.layType == "0") { recyclerView.layoutManager = VariantLinearLayoutManager(this) recyclerView.layoutAnimation = AnimationUtils.loadLayoutAnimation(this, R.anim.layout_fall_down) } else { recyclerView.layoutManager = VariantGridLayoutManager(this, gridColumns()) recyclerView.layoutAnimation = AnimationUtils.loadLayoutAnimation(this, R.anim.grid_fall_down) } } override fun onSelect(queueObject: QueueObject) { if (queueObject.equalsAnime(current)) { closeSheet() } else { doOnUI { try { listToolbar.title = queueObject.chapter.name lifecycleScope.launch(Dispatchers.Main) { val list = withContext(Dispatchers.IO) { CacheDB.INSTANCE.queueDAO().getByAidUnique(queueObject.chapter.aid) } if (list.isEmpty()) bottomSheetBehavior?.setState(BottomSheetBehavior.STATE_HIDDEN) else { listAdapter?.update( queueObject.chapter.aid, withContext(Dispatchers.IO) { try { list.sortedBy { it.chapter.number.substringAfterLast(" ").toInt() }.toMutableList() } catch (_: Exception) { list } } ) bottomSheetBehavior?.setState(BottomSheetBehavior.STATE_EXPANDED) } current = queueObject } } catch (_: Exception) { doAsync { CacheDB.INSTANCE.queueDAO().allRaw.forEach { try { it.chapter.aid } catch (_: Exception) { CacheDB.INSTANCE.queueDAO().remove(it) } } syncData { queue() } } } } } } private fun closeSheet() { current = null bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN } override fun onStartDrag(holder: RecyclerView.ViewHolder) { mItemTouchHelper?.startDrag(holder) } override fun onListCleared() { errorView.post { errorView.visibility = View.VISIBLE } } override fun onCreateOptionsMenu(menu: Menu): Boolean { if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("queue_is_grouped", true)) menuInflater.inflate(R.menu.menu_queue_group, menu) else menuInflater.inflate(R.menu.menu_queue_list, menu) val preferences = PreferenceManager.getDefaultSharedPreferences(this) if (!preferences.getBoolean("is_queue_info_shown", false)) { preferences.edit().putBoolean("is_queue_info_shown", true).apply() onOptionsItemSelected(menu.findItem(R.id.info)) } return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN when (item.itemId) { R.id.info -> MaterialDialog(this).safeShow { message(text = "Los episodios añadidos desde servidor podrían dejar de funcionar después de días sin reproducir") positiveButton(text = "OK") } R.id.queue_group -> { PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("queue_is_grouped", true).apply() setLayoutManager(false) reload() invalidateOptionsMenu() } R.id.queue_list -> { PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("queue_is_grouped", false).apply() setLayoutManager(true) reload() invalidateOptionsMenu() } R.id.play -> { bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN val list = (recyclerView.adapter as? QueueAllAdapterMaterial)?.list ?: mutableListOf() if (list.size > 0) QueueManager.startQueue(applicationContext, list) else Toaster.toast("La lista esta vacia") } } return super.onOptionsItemSelected(item) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) if (current != null) { outState.putSerializable("current", current) outState.putBoolean("isOpen", true) } else outState.putBoolean("isOpen", false) } companion object { fun open(context: Context?) { context ?: return context.startActivity(Intent(context, QueueActivityMaterial::class.java)) } fun open(context: Context?, aid: String) { context ?: return context.startActivity(Intent(context, QueueActivityMaterial::class.java).apply { putExtra("initial", aid) }) } } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueAllAdapter.kt ================================================ package knf.kuma.queue import android.annotation.SuppressLint import android.app.Activity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.notSameContent import knf.kuma.pojos.QueueObject import org.jetbrains.anko.find import java.util.Collections internal class QueueAllAdapter internal constructor(activity: Activity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { private val dragListener: OnStartDragListener = activity as OnStartDragListener var list: MutableList = ArrayList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimeHolder { return AnimeHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_queue_full, parent, false)) } @SuppressLint("ClickableViewAccessibility") override fun onBindViewHolder(holder: AnimeHolder, position: Int) { val queueObject = list[position] holder.title.text = PatternUtil.fromHtml(queueObject.chapter.name) holder.chapter.text = queueObject.chapter.number holder.state.setImageResource(if (queueObject.isFile) R.drawable.ic_queue_file else R.drawable.ic_web) holder.dragView.setOnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_DOWN) { dragListener.onStartDrag(holder) } false } } override fun getItemCount(): Int { return list.size } fun update(list: MutableList) { if (this.list notSameContent list) { this.list = list notifyDataSetChanged() } } override fun onItemMove(fromPosition: Int, toPosition: Int) { if (fromPosition < toPosition) { for (i in fromPosition until toPosition) { val fromTime = list[i].time list[i].time = list[i + 1].time list[i + 1].time = fromTime QueueManager.update(list[i], list[i + 1]) Collections.swap(list, i, i + 1) } } else { for (i in fromPosition downTo toPosition + 1) { val fromTime = list[i].time list[i].time = list[i - 1].time list[i - 1].time = fromTime QueueManager.update(list[i], list[i - 1]) Collections.swap(list, i, i - 1) } } notifyItemMoved(fromPosition, toPosition) } override fun onItemDismiss(position: Int) { QueueManager.remove(list[position]) list.removeAt(position) notifyItemRemoved(position) if (list.size == 0) dragListener.onListCleared() } internal interface OnStartDragListener { fun onStartDrag(holder: RecyclerView.ViewHolder) fun onListCleared() } internal class AnimeHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView = itemView.find(R.id.card) val dragView: ImageView = itemView.find(R.id.drag) val title: TextView = itemView.find(R.id.title) val chapter: TextView = itemView.find(R.id.chapter) val state: ImageView = itemView.find(R.id.state) } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueAllAdapterMaterial.kt ================================================ package knf.kuma.queue import android.annotation.SuppressLint import android.app.Activity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.noCrash import knf.kuma.commons.notSameContent import knf.kuma.pojos.QueueObject import org.jetbrains.anko.find import java.util.Collections internal class QueueAllAdapterMaterial internal constructor(activity: Activity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { private val dragListener: OnStartDragListener = activity as OnStartDragListener var list: MutableList = ArrayList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimeHolder { return AnimeHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_queue_full_material, parent, false)) } @SuppressLint("ClickableViewAccessibility") override fun onBindViewHolder(holder: AnimeHolder, position: Int) { val queueObject = list[position] noCrash { holder.title.text = PatternUtil.fromHtml(queueObject.chapter.name) holder.chapter.text = queueObject.chapter.number } holder.state.setImageResource(if (queueObject.isFile) R.drawable.ic_queue_file else R.drawable.ic_web) holder.dragView.setOnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_DOWN) { dragListener.onStartDrag(holder) } false } } override fun getItemCount(): Int { return list.size } fun update(list: MutableList) { if (this.list notSameContent list) { this.list = list notifyDataSetChanged() } } override fun onItemMove(fromPosition: Int, toPosition: Int) { if (fromPosition < toPosition) { for (i in fromPosition until toPosition) { val fromTime = list[i].time list[i].time = list[i + 1].time list[i + 1].time = fromTime QueueManager.update(list[i], list[i + 1]) Collections.swap(list, i, i + 1) } } else { for (i in fromPosition downTo toPosition + 1) { val fromTime = list[i].time list[i].time = list[i - 1].time list[i - 1].time = fromTime QueueManager.update(list[i], list[i - 1]) Collections.swap(list, i, i - 1) } } notifyItemMoved(fromPosition, toPosition) } override fun onItemDismiss(position: Int) { QueueManager.remove(list[position]) list.removeAt(position) notifyItemRemoved(position) if (list.size == 0) dragListener.onListCleared() } internal interface OnStartDragListener { fun onStartDrag(holder: RecyclerView.ViewHolder) fun onListCleared() } internal class AnimeHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val dragView: ImageView = itemView.find(R.id.drag) val title: TextView = itemView.find(R.id.title) val chapter: TextView = itemView.find(R.id.chapter) val state: ImageView = itemView.find(R.id.state) } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueAnimesAdapter.kt ================================================ package knf.kuma.queue import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.load import knf.kuma.commons.notSameContent import knf.kuma.pojos.QueueObject import java.util.Locale internal class QueueAnimesAdapter internal constructor(private val activity: Activity) : RecyclerView.Adapter() { private var listener: OnAnimeSelectedListener? = null private var list: MutableList = ArrayList() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") R.layout.item_anim_queue else R.layout.item_anim_queue_grid init { this.listener = activity as OnAnimeSelectedListener } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimeHolder { return AnimeHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: AnimeHolder, position: Int) { val queueObject = list[position] val img = PatternUtil.getCover(queueObject.chapter.aid) holder.imageView.load(img) holder.title.text = PatternUtil.fromHtml(queueObject.chapter.name) holder.type.text = String.format(Locale.getDefault(), if (queueObject.count == 1) "%d episodio" else "%d episodios", queueObject.count) holder.cardView.setOnClickListener { listener?.onSelect(queueObject) } holder.cardView.setOnLongClickListener { ActivityAnime.open(activity, queueObject, holder.imageView) true } } override fun getItemCount(): Int { return list.size } fun update(list: MutableList) { if (this.list notSameContent list) { this.list = list doOnUIGlobal { notifyDataSetChanged() } } } fun clear() { listener = null } internal interface OnAnimeSelectedListener { fun onSelect(queueObject: QueueObject) } internal class AnimeHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView by itemView.bind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueAnimesAdapterMaterial.kt ================================================ package knf.kuma.queue import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.notSameContent import knf.kuma.pojos.QueueObject import java.util.Locale internal class QueueAnimesAdapterMaterial internal constructor(private val activity: Activity) : RecyclerView.Adapter() { private var listener: OnAnimeSelectedListener? = null private var list: MutableList = ArrayList() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") R.layout.item_anim_queue_material else R.layout.item_anim_queue_grid_material init { this.listener = activity as OnAnimeSelectedListener } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimeHolder { return AnimeHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: AnimeHolder, position: Int) { val queueObject = list[position] noCrash { holder.imageView.load(PatternUtil.getCover(queueObject.chapter.aid)) holder.title.text = PatternUtil.fromHtml(queueObject.chapter.name) } holder.type.text = String.format(Locale.getDefault(), if (queueObject.count == 1) "%d episodio" else "%d episodios", queueObject.count) holder.cardView.setOnClickListener { listener?.onSelect(queueObject) } holder.cardView.setOnLongClickListener { ActivityAnimeMaterial.open(activity, queueObject, holder.imageView) true } } override fun getItemCount(): Int { return list.size } fun update(list: MutableList) { if (this.list notSameContent list) { this.list = list doOnUIGlobal { notifyDataSetChanged() } } } fun clear() { listener = null } internal interface OnAnimeSelectedListener { fun onSelect(queueObject: QueueObject) } internal class AnimeHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView by itemView.bind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueListAdapter.kt ================================================ package knf.kuma.queue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.commons.notSameContent import knf.kuma.database.CacheDB import knf.kuma.pojos.QueueObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.find internal class QueueListAdapter(val callback: () -> Unit) : RecyclerView.Adapter() { private var current = "0000" var list: MutableList = ArrayList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemHolder { return ListItemHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_queue, parent, false)) } override fun onBindViewHolder(holder: ListItemHolder, position: Int) { val queueObject = list[position] holder.chapter.text = queueObject.chapter.number holder.icon.setImageResource(if (queueObject.isFile) R.drawable.ic_chap_down else R.drawable.ic_web) holder.actionDelete.setOnClickListener { remove(holder.adapterPosition) } } override fun getItemCount(): Int { return list.size } fun update(aid: String, list: MutableList) { if (current != aid && this.list notSameContent list) { current = aid this.list = list notifyDataSetChanged() } } fun remove(position: Int) { if (position != -1) { GlobalScope.launch(Dispatchers.Main){ withContext(Dispatchers.IO) { CacheDB.INSTANCE.queueDAO().remove(list[position]) } list.removeAt(position) notifyItemRemoved(position) if (list.isEmpty()) callback.invoke() } } } internal class ListItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val chapter: TextView = itemView.find(R.id.chapter) val icon: ImageView = itemView.find(R.id.icon) val actionDelete: ImageButton = itemView.find(R.id.action_delete) } } ================================================ FILE: app/src/main/java/knf/kuma/queue/QueueManager.kt ================================================ package knf.kuma.queue import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import androidx.preference.PreferenceManager import knf.kuma.achievements.AchievementManager import knf.kuma.animeinfo.ktx.fileName import knf.kuma.backup.firestore.syncData import knf.kuma.commons.FileWrapper import knf.kuma.commons.PrefsUtil import knf.kuma.database.CacheDB import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.ExplorerObject import knf.kuma.pojos.QueueObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeenObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import xdroid.toaster.Toaster object QueueManager { suspend fun isInQueue(eid: String): Boolean { return withContext(Dispatchers.IO) { CacheDB.INSTANCE.queueDAO().isInQueue(eid) } } fun add(uri: Uri, isFile: Boolean, chapter: AnimeObject.WebInfo.AnimeChapter?) { if (chapter == null) return doAsync { CacheDB.INSTANCE.queueDAO().add(QueueObject(uri, isFile, chapter)) syncData { queue() } Toaster.toast("Episodio añadido a cola") } } fun add(wrapper: FileWrapper<*>, dobject: DownloadObject?, isFile: Boolean, chapter: AnimeObject.WebInfo.AnimeChapter?) { if (chapter == null) return doAsync { val file = wrapper.file() ?: wrapper.let { it.reset(); it.file() } ?: dobject?.let { FileWrapper.fromFileName(it.file) } ?: return@doAsync CacheDB.INSTANCE.queueDAO().add(QueueObject(Uri.fromFile(file), isFile, chapter)) syncData { queue() } Toaster.toast("Episodio añadido a cola") } } fun remove(queueObject: QueueObject) { doAsync { CacheDB.INSTANCE.queueDAO().remove(queueObject) syncData { queue() } } } fun update(vararg objects: QueueObject) { doAsync { CacheDB.INSTANCE.queueDAO().update(*objects) syncData { queue() } } } fun remove(list: MutableList) { doAsync { CacheDB.INSTANCE.queueDAO().remove(list) syncData { queue() } } } fun remove(eid: String?) { if (eid == null) return doAsync { CacheDB.INSTANCE.queueDAO().removeByEID(eid) syncData { queue() } } } fun removeAll(aid: String) { CacheDB.INSTANCE.queueDAO().removeByID(aid) } fun nuke() { doAsync { CacheDB.INSTANCE.queueDAO().nuke() syncData { queue() } } } internal fun startQueue(context: Context, list: List) { if (list.isNotEmpty()) { AchievementManager.onPlayQueue(list.size) markAllSeen(list) if (PreferenceManager.getDefaultSharedPreferences(context).getString("player_type", "0") == "0" || isMxInstalled(context) == null) startQueueInternal(context, list) else startQueueExternal(context, list) } else Toaster.toast("La lista esta vacía") } internal suspend fun startQueueDownloaded(context: Context?, list: List) { if (context == null) return if (list.isNotEmpty()) { markAllSeenDownloaded(list) if (PreferenceManager.getDefaultSharedPreferences(context).getString("player_type", "0") == "0" || isMxInstalled(context) == null) startQueueInternalDownloaded(context, list) else startQueueExternalDownloaded(context, list) } else Toaster.toast("La lista esta vacía") } private fun startQueueInternal(context: Context, list: List) { val intent = PrefsUtil.getPlayerIntent() .putExtra("isPlayList", true) .putExtra("playlist", list[0].chapter.aid) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } private fun startQueueInternalDownloaded(context: Context, list: List) { doAsync { for (file in list) CacheDB.INSTANCE.queueDAO().add( QueueObject( FileAccessHelper.getFileUri(file.fileName), true, AnimeObject.WebInfo.AnimeChapter.fromDownloaded(file))) val intent = PrefsUtil.getPlayerIntent() .putExtra("isPlayList", true) .putExtra("playlist", list[0].aid) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } } private fun startQueueExternal(context: Context, list: List) { val startUri = if (list[0].isFile) FileAccessHelper.getDataUri(list[0].chapter.fileName) else list[0].createUri() val titles = QueueObject.getTitles(list) val uris = QueueObject.uris(list) uris[0] = startUri ?: Uri.EMPTY val intent = Intent(Intent.ACTION_VIEW) .setPackage(isMxInstalled(context)) .setDataAndType(startUri, "video/mp4") .putExtra("title", titles[0]) .putExtra("video_list_is_explicit", true) .putExtra("video_list", uris) .putExtra("video_list.name", titles) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } private fun startQueueExternalDownloaded(context: Context, list: List) { val startUri = FileAccessHelper.getDataUri(list[0].fileName) val titles = ExplorerObject.FileDownObj.getTitles(list) val uris = ExplorerObject.FileDownObj.getUris(list) uris[0] = startUri ?: Uri.EMPTY val intent = Intent(Intent.ACTION_VIEW) .setPackage(isMxInstalled(context)) .setDataAndType(startUri, "video/mp4") .putExtra("title", titles[0]) .putExtra("video_list_is_explicit", true) .putExtra("video_list", uris) .putExtra("video_list.name", titles) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } private fun isMxInstalled(context: Context): String? { val pm = context.packageManager try { pm.getPackageInfo("com.mxtech.videoplayer.pro", PackageManager.GET_ACTIVITIES) return "com.mxtech.videoplayer.pro" } catch (e: PackageManager.NameNotFoundException) { } try { pm.getPackageInfo("com.mxtech.videoplayer.ad", PackageManager.GET_ACTIVITIES) return "com.mxtech.videoplayer.ad" } catch (e: PackageManager.NameNotFoundException) { } return null } private fun markAllSeen(list: List) { if (list.isNotEmpty()) doAsync { CacheDB.INSTANCE.seenDAO().addAll(list.map { SeenObject.fromChapter(it.chapter) }) CacheDB.INSTANCE.recordsDAO().add(RecordObject.fromChapter(list.last().chapter)) syncData { history() seen() } } } suspend fun markAllSeenDownloaded(list: List) { if (list.isNotEmpty()){ CacheDB.INSTANCE.seenDAO().addAll(list.map { SeenObject.fromDownloaded(it) }) CacheDB.INSTANCE.recordsDAO().add(RecordObject.fromChapter(AnimeObject.WebInfo.AnimeChapter.fromDownloaded(list.last()))) syncData { history() seen() } } } } ================================================ FILE: app/src/main/java/knf/kuma/queue/SimpleItemTouchHelperCallback.kt ================================================ package knf.kuma.queue import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView class SimpleItemTouchHelperCallback internal constructor(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() { override fun isLongPressDragEnabled(): Boolean { return false } override fun isItemViewSwipeEnabled(): Boolean { return true } override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END return makeMovementFlags(dragFlags, swipeFlags) } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { mAdapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition) return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { mAdapter.onItemDismiss(viewHolder.adapterPosition) } } ================================================ FILE: app/src/main/java/knf/kuma/random/RandomActivity.kt ================================================ package knf.kuma.random import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.annotation.LayoutRes import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView import com.github.stephenvinouze.materialnumberpickercore.MaterialNumberPicker import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.commons.verifyManager import knf.kuma.custom.BannerContainerView import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.anko.doAsync import org.jetbrains.anko.find class RandomActivity : GenericActivity(), SwipeRefreshLayout.OnRefreshListener { val toolbar: Toolbar by bind(R.id.toolbar) private val refreshLayout: SwipeRefreshLayout by bind(R.id.refresh) val recyclerView: RecyclerView by bind(R.id.recycler) private var adapter: RandomAdapter? = null private var counter = 0 private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_refresh } else { R.layout.recycler_refresh_grid } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(layout) toolbar.title = "Random" setSupportActionBar(toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } refreshLayout.setOnRefreshListener(this) adapter = RandomAdapter(this) recyclerView.verifyManager() recyclerView.adapter = adapter refreshLayout.isRefreshing = true refreshLayout.setColorSchemeResources(EAHelper.getThemeColor(), EAHelper.getThemeColorLight(), R.color.colorPrimary) refreshList() lifecycleScope.launch(Dispatchers.IO) { delay(1000) find(R.id.adContainer).implBanner(AdsType.RANDOM_BANNER, true) } } private fun refreshList() { counter++ if (counter >= 15) AchievementManager.unlock(listOf(32)) doAsync { val list = CacheDB.INSTANCE.animeDAO().getRandom(PrefsUtil.randomLimit) doOnUI { refreshLayout.isRefreshing = false adapter?.update(list) recyclerView.scheduleLayoutAnimation() } } } override fun onRefresh() { refreshList() } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_random, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { val picker = MaterialNumberPicker( this, 5, 100, PrefsUtil.randomLimit, ContextCompat.getColor(this, R.color.colorAccent), ContextCompat.getColor(this, R.color.textPrimary), resources.getDimensionPixelSize(R.dimen.num_picker)) MaterialDialog(this@RandomActivity).safeShow { title(text = "Numero de resultados") customView(view = picker, scrollable = false) positiveButton(text = "OK") { PrefsUtil.randomLimit = picker.value refreshLayout.post { refreshLayout.isRefreshing = true } refreshList() } } return super.onOptionsItemSelected(item) } companion object { fun open(context: Context) { context.startActivity(Intent(context, RandomActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/random/RandomActivityMaterial.kt ================================================ package knf.kuma.random import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.annotation.LayoutRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import biz.kasual.materialnumberpicker.MaterialNumberPicker import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.inflate import knf.kuma.commons.setSurfaceBars import knf.kuma.commons.verifyManager import knf.kuma.custom.BannerContainerView import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch class RandomActivityMaterial : GenericActivity(), SwipeRefreshLayout.OnRefreshListener { val toolbar: Toolbar by bind(R.id.toolbar) private val refreshLayout: SwipeRefreshLayout by bind(R.id.refresh) val recyclerView: RecyclerView by bind(R.id.recycler) private var adapter: RandomAdapterMaterial? = null private var counter = 0 private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_refresh_material } else { R.layout.recycler_refresh_grid_material } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(layout) toolbar.title = "Random" setSupportActionBar(toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } refreshLayout.setOnRefreshListener(this) adapter = RandomAdapterMaterial(this) recyclerView.verifyManager() recyclerView.adapter = adapter refreshLayout.isRefreshing = true refreshLayout.setColorSchemeResources(EAHelper.getThemeColor(), EAHelper.getThemeColorLight(), R.color.colorPrimary) refreshList() lifecycleScope.launch(Dispatchers.IO) { delay(1000) findViewById(R.id.adContainer).implBanner(AdsType.RANDOM_BANNER, true) } } private fun refreshList() { counter++ if (counter >= 15) AchievementManager.unlock(listOf(32)) lifecycleScope.launch(Dispatchers.IO) { val list = CacheDB.INSTANCE.animeDAO().getRandom(PrefsUtil.randomLimit) doOnUI { refreshLayout.isRefreshing = false adapter?.update(list) recyclerView.scheduleLayoutAnimation() } } } override fun onRefresh() { refreshList() } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_random, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { AlertDialog.Builder(this).apply { setTitle("Numero de resultados") val view = inflate(this@RandomActivityMaterial, R.layout.dialog_random_picker) val picker = view.findViewById(R.id.picker) picker.value = PrefsUtil.randomLimit setView(view) setPositiveButton("OK") { _, _ -> PrefsUtil.randomLimit = picker.value refreshLayout.post { refreshLayout.isRefreshing = true } refreshList() } }.show() return super.onOptionsItemSelected(item) } companion object { fun open(context: Context) { context.startActivity(Intent(context, RandomActivityMaterial::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/random/RandomAdapter.kt ================================================ package knf.kuma.random import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.commons.notSameContent internal class RandomAdapter(private val activity: Activity) : RecyclerView.Adapter() { private var list: List = ArrayList() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_fav } else { R.layout.item_fav_grid } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RandomItem { return RandomItem(LayoutInflater.from(activity).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: RandomItem, position: Int) { val animeObject = list[position] holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.title.text = animeObject.name holder.type.text = animeObject.type holder.cardView.setOnClickListener { ActivityAnime.open(activity, animeObject, holder.imageView, false, true) } } override fun getItemCount(): Int { return list.size } fun update(list: List) { if (this.list notSameContent list) { this.list = list notifyDataSetChanged() } } internal class RandomItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView by itemView.bind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/random/RandomAdapterMaterial.kt ================================================ package knf.kuma.random import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.commons.notSameContent internal class RandomAdapterMaterial(private val activity: Activity) : RecyclerView.Adapter() { private var list: List = ArrayList() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_fav_material } else { R.layout.item_fav_grid_material } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RandomItem { return RandomItem(LayoutInflater.from(activity).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: RandomItem, position: Int) { val animeObject = list[position] holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.title.text = animeObject.name holder.type.text = animeObject.type holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(activity, animeObject, holder.imageView, false, true) } } override fun getItemCount(): Int { return list.size } fun update(list: List) { if (this.list notSameContent list) { this.list = list notifyDataSetChanged() } } internal class RandomItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView by itemView.bind(R.id.type) } } ================================================ FILE: app/src/main/java/knf/kuma/random/RandomObject.kt ================================================ package knf.kuma.random import knf.kuma.search.SearchObject class RandomObject : SearchObject() { var type = "" } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentFragment.kt ================================================ package knf.kuma.recents import android.content.pm.ActivityInfo import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.viewModels import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.custom.BannerContainerView import knf.kuma.home.HomeFragment import knf.kuma.pojos.RecentObject import knf.kuma.recents.viewholders.RecyclerRefreshHolder import knf.kuma.videoservers.FileActions import knf.kuma.videoservers.ServersFactory import org.jetbrains.anko.support.v4.find class RecentFragment : BottomFragment(), SwipeRefreshLayout.OnRefreshListener { private val viewModel: RecentsViewModel by viewModels() private var holder: RecyclerRefreshHolder? = null private var adapter: RecentsAdapter? = null override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.dbLiveData.observe(viewLifecycleOwner) { objects -> holder?.setError(objects.isEmpty()) holder?.setRefreshing(false) adapter?.updateList(objects) { holder?.recyclerView?.scheduleLayoutAnimation() } scrollByKey(objects) } updateList() if (!PrefsUtil.isNativeAdsEnabled) find(R.id.adContainer).implBanner(AdsType.RECENT_BANNER) } private fun scrollByKey(list: List) { if (list.isEmpty()) return val initial = arguments?.getInt("initial", -1) ?: -1 if (initial == -1) return val find = list.find { it.key == initial } ?: return holder?.scrollTo(list.indexOf(find)) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.recycler_refresh_fragment, container, false) holder = RecyclerRefreshHolder(view).also { it.refreshLayout.setOnRefreshListener(this@RecentFragment) adapter = RecentsAdapter(this@RecentFragment, it.recyclerView) it.recyclerView.adapter = adapter it.setRefreshing(true) } EAHelper.enter1("R") return view } override fun onRefresh() { updateList() } private fun updateList() { if (!Network.isConnected) { holder?.setRefreshing(false) } else { viewModel.reload() } } override fun onReselect() { EAHelper.enter1("R") holder?.scrollToTop() } override fun onDestroyView() { super.onDestroyView() ServersFactory.clear() FileActions.reset() (activity as? AppCompatActivity)?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } companion object { fun get(initialKey: Int): BottomFragment { val fragment = RecentFragment() val bundle = Bundle() bundle.putInt("initial", initialKey.also { Log.e("Recent", "Add argument key: $it") }) fragment.arguments = bundle return fragment } fun get(): BottomFragment { return if (PrefsUtil.useHome) HomeFragment() else RecentFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentModel.kt ================================================ package knf.kuma.recents import android.content.Context import android.content.Intent import android.net.Uri import androidx.core.text.isDigitsOnly import androidx.recyclerview.widget.DiffUtil import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey import com.google.errorprone.annotations.Keep import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.commons.DesignUtils import knf.kuma.commons.FileWrapper import knf.kuma.commons.PatternUtil import knf.kuma.commons.PatternUtil.getAnimeUrl import knf.kuma.commons.PatternUtil.getFileName import knf.kuma.commons.PrefsUtil import knf.kuma.commons.PrefsUtil.saveWithName import knf.kuma.commons.distinct import knf.kuma.database.CacheDB import knf.kuma.database.CacheDBWrap import knf.kuma.database.dao.SeenDAO import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.SeenObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jsoup.Jsoup import org.jsoup.nodes.Element import pl.droidsonroids.jspoon.ElementConverter import pl.droidsonroids.jspoon.annotation.Selector import kotlin.math.abs @Entity @Keep open class RecentModel { @JvmField @PrimaryKey var key: Int = -1 @JvmField @Selector(value = "img[src]", attr = "src", format = "/(\\d+)\\.\\w+") var aid: String = "0" @JvmField @Selector(value = ".Title") var name: String = "" @JvmField @Selector(".Capi") var chapter: String = "" @JvmField @Selector(value = "a", converter = AFixer::class) var chapterUrl: String = "" @JvmField @Selector(value = "img[src]", converter = ImageFixer::class) var img: String = "" @Ignore lateinit var extras: RecentExtras @Ignore lateinit var state: RecentState fun generate(element: Element) { aid = element.select("img[src]").attr("src").let { Regex("/(\\d+)\\.\\w+").find(it)?.groupValues?.get(1) ?: "0" } name = element.select(".Title").text() chapter = element.select(".Capi").text() chapterUrl = element.select("a").attr("href") img = element.select("img[src]").attr("src") } fun prepare() { if (!aid.isDigitsOnly()) aid = "0" if (!::extras.isInitialized) extras = RecentExtras(this) if (!::state.isInitialized) state = RecentState(this) } val isNew: Boolean get() = chapter.matches("^.* [10]$".toRegex()) override fun equals(other: Any?): Boolean = other is RecentModel && other.chapter == chapter && other.name == name && other.aid == aid && other.key == key override fun hashCode(): Int = name.hashCode() + chapter.hashCode() companion object { val DIFF = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: RecentModel, newItem: RecentModel): Boolean = oldItem::class == newItem::class && if (oldItem is RecentModelAd) oldItem.id == (newItem as RecentModelAd).id else oldItem.extras.eid == newItem.extras.eid override fun areContentsTheSame(oldItem: RecentModel, newItem: RecentModel): Boolean = if (oldItem !is RecentModelAd) oldItem == newItem else true } } } class RecentExtras(model: RecentModel) { val eid: String by lazy { abs("${model.aid}${model.chapter}".hashCode()).toString() } val isNewChapter: Boolean by lazy { model.chapter.matches("^.* [10]$".toRegex()) } val animeUrl: String by lazy { getAnimeUrl(model.chapterUrl, model.aid) } val filePath: String by lazy { if (saveWithName) "$" + getFileName(model.chapterUrl) else "$" + model.aid + "-" + model.chapter.substring(model.chapter.lastIndexOf(" ") + 1) + ".mp4" } val fileName: String by lazy { eid + filePath } val chapterTitle: String by lazy { model.name + model.chapter.substring(model.chapter.lastIndexOf(" ")) } val fileWrapper: FileWrapper<*> by lazy { FileWrapper.create(filePath) } } class RecentState(val model: RecentModel) { var isFavorite = CacheDB.INSTANCE.favsDAO().isFav(model.aid.toInt()) val favoriteLive = CacheDB.INSTANCE.favsDAO().isFavLive(model.aid.toInt()).distinct var isSeen = CacheDB.INSTANCE.seenDAO().chapterIsSeen(model.aid, model.chapter) val seenLive = CacheDB.INSTANCE.seenDAO().chapterIsSeenLive(model.aid, model.chapter).distinct var downloadObject: DownloadObject? = CacheDBWrap.INSTANCE.downloadsDAO().getByEid(model.extras.eid) var isDeleting = false val downloadLive = CacheDB.INSTANCE.downloadsDAO().getLiveByEid(model.extras.eid) var isDownloaded: Boolean get() = model.extras.fileWrapper.exist set(value) { model.extras.fileWrapper.exist = value } val checkIsDownloaded: Boolean get() = model.extras.fileWrapper.existForced() val canPlay: Boolean get() = downloadObject?.isDownloadingOrPaused == false && checkIsDownloaded } @Keep class RecentsPage { @Selector("ul.ListEpisodios li:not(article), ul.List-Episodes li:not(article)") var list: List = emptyList() fun create(html: String): RecentsPage { val doc = Jsoup.parse(html, "https://www3.animeflv.net") val episodes = doc.select("ul.ListEpisodios li:not(article), ul.List-Episodes li:not(article)") list = episodes.map { RecentModel().apply { generate(it) } } return this } } class AFixer : ElementConverter { override fun convert(node: Element, selector: Selector): String { return "https://www3.animeflv.net${node.attr("href")}" } } class ImageFixer : ElementConverter { override fun convert(node: Element, selector: Selector): String { return "https://www3.animeflv.net${node.attr("src")}" } } fun RecentModel.toggleSeen(scope: CoroutineScope, seenDAO: SeenDAO) { scope.launch(Dispatchers.IO) { if (!state.isSeen) seenDAO.addChapter(SeenObject.fromRecentModel(this@toggleSeen)) else seenDAO.deleteChapter(aid, chapter) syncData { seen() } } } val RecentModel.menuHideList: List get() = mutableListOf().apply { if (PrefsUtil.recentActionType == "0") add(R.id.streaming) if (PrefsUtil.recentActionType == "1" || state.downloadObject?.isDownloadingOrPaused == true || state.canPlay) add(R.id.download) if (state.isDeleting || !state.canPlay) add(R.id.delete) } fun RecentModel.openInfo(context: Context) { context.startActivity(Intent(context, DesignUtils.infoClass).apply { data = Uri.parse(this@openInfo.extras.animeUrl) putExtra("title", name) putExtra("img", PatternUtil.getCover(aid)) }) } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentModelAd.kt ================================================ package knf.kuma.recents //import com.google.android.gms.ads.nativead.NativeAd data class RecentModelAd(val id: Int/*, val unifiedNativeAd: NativeAd*/) : RecentModel() ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentModelCh.kt ================================================ package knf.kuma.recents data class RecentModelCh( val name: String, val chapter: String, val url: String, val aid: String, val eid: String ) ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentModelsAdapter.kt ================================================ package knf.kuma.recents //import com.google.android.gms.ads.nativead.NativeAdView import android.annotation.SuppressLint import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.google.android.material.chip.Chip import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.progressindicator.CircularProgressIndicator import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.cast.CastMedia import knf.kuma.commons.CastUtil import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.inflate import knf.kuma.commons.isFullMode import knf.kuma.commons.isVisibleAnimate import knf.kuma.commons.load import knf.kuma.commons.onClickMenu import knf.kuma.commons.safeShow import knf.kuma.database.CacheDB import knf.kuma.download.DownloadManagerCentral import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import knf.kuma.videoservers.FileActions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.find import org.jetbrains.anko.sdk27.coroutines.onClick import java.util.Locale class RecentModelsAdapter(private val fragment: Fragment) : ListAdapter(RecentModel.DIFF) { private val lifecycleScope = fragment.lifecycleScope private val chaptersDAO by lazy { CacheDB.INSTANCE.seenDAO() } private val recordsDAO by lazy { CacheDB.INSTANCE.recordsDAO() } private var adsList = emptyList() override fun getItemId(position: Int): Long = getItem(position).let { if (it is RecentModelAd) it.id.toLong() else it.hashCode().toLong() } override fun getItemViewType(position: Int): Int = getItem(position).let { if (it is RecentModelAd) 0 else 1 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = if (viewType == 0) AdsViewHolder(fragment.lifecycleScope, parent.inflate(R.layout.item_ad_recents_material)) else ModelsViewHolder(fragment.viewLifecycleOwner, parent.inflate(R.layout.item_recents_material)) override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = getItem(position) if (holder is ModelsViewHolder) { holder.apply { image.load(PatternUtil.getCover(item.aid)) chapter.text = item.chapter name.text = item.name newIndicator.isVisible = item.extras.isNewChapter && !item.state.isFavorite seenIndicator.isVisible = item.state.isSeen favIndicator.isVisible = item.state.isFavorite setUp(item) if (isFullMode) actionMenu.onClickMenu(R.menu.menu_download_info, true, { item.menuHideList }) { when (it.itemId) { R.id.download -> { FileActions.download( fragment.requireContext(), fragment.viewLifecycleOwner, item, fragment.view ) { state, _ -> if (state == FileActions.CallbackState.START_DOWNLOAD) item.state.isDownloaded = true } } R.id.streaming -> { FileActions.stream( fragment.requireContext(), fragment.viewLifecycleOwner, item, fragment.view ) { state, extra -> when (state) { FileActions.CallbackState.START_STREAM -> { setAsSeen(item) } FileActions.CallbackState.START_CAST -> { CastUtil.get().play(fragment.requireView(), CastMedia.create(item, extra as? String)) setAsSeen(item) } else -> { } } } } R.id.delete -> { MaterialDialog(fragment.requireContext()).safeShow { lifecycleOwner(fragment.viewLifecycleOwner) message(text = "¿Eliminar el ${item.chapter.lowercase(Locale.ENGLISH)} de ${item.name}?") positiveButton(text = "CONFIRMAR") { GlobalScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.Main) { item.state.isDownloaded = false this@apply.downloadedChip.isVisibleAnimate = false } item.state.isDeleting = true FileAccessHelper.deletePath(item.extras.filePath, false) item.state.isDeleting = false item.state.checkIsDownloaded DownloadManagerCentral.cancel(item.extras.eid) QueueManager.remove(item.extras.eid) } } negativeButton(text = "CANCELAR") } } R.id.info -> { item.openInfo(fragment.requireContext()) } } } else actionMenu.isVisible = false root.setOnClickListener { if (!isFullMode) { item.openInfo(fragment.requireContext()) return@setOnClickListener } if (item.state.isDownloaded) { if (CastUtil.get().connected()) CastUtil.get().play(fragment.requireView(), CastMedia.create(item)) else FileActions.startPlay( fragment.requireContext(), item.extras.chapterTitle, item.extras.fileWrapper.name() ) setAsSeen(item) } else { val callback: (FileActions.CallbackState, Any?) -> Unit = { state, extra -> when (state) { FileActions.CallbackState.START_STREAM -> { setAsSeen(item) } FileActions.CallbackState.START_CAST -> { CastUtil.get().play(fragment.requireView(), CastMedia.create(item, extra as? String)) setAsSeen(item) } FileActions.CallbackState.START_DOWNLOAD -> { item.state.isDownloaded = true } else -> { } } } if (PrefsUtil.recentActionType == "0") FileActions.stream(fragment.requireContext(), fragment.viewLifecycleOwner, item, fragment.view, callback) else FileActions.download(fragment.requireContext(), fragment.viewLifecycleOwner, item, fragment.view, callback) } } root.setOnLongClickListener { item.toggleSeen(lifecycleScope, chaptersDAO) true } } } else if (holder is AdsViewHolder) { holder.setAd(item as RecentModelAd) } } private fun setAsSeen(item: RecentModel) { fragment.lifecycleScope.launch(Dispatchers.IO) { chaptersDAO.addChapter(SeenObject.fromRecentModel(item)) recordsDAO.add(RecordObject.fromRecentModel(item)) syncData { history() seen() } } } override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) if (holder is ModelsViewHolder) holder.recycle() } fun updateList(list: List, aList: List = adsList, callback: () -> Unit = {}) { fragment.lifecycleScope.launch(Dispatchers.IO) { list.forEach { if (it !is RecentModelAd) it.prepare() } val fList = list.toMutableList() if (list.isNotEmpty() && aList.isNotEmpty()) { adsList = aList fList.add(0, adsList[0]) if (adsList.size >= 2) fList.add(8, adsList[1]) if (adsList.size >= 3) fList.add(16, adsList[2]) } launch(Dispatchers.Main) { submitList(fList) callback() } } } fun hasAds(): Boolean = adsList.isNotEmpty() class ModelsViewHolder(private val lifecycleOwner: LifecycleOwner, view: View) : ViewHolder(view) { val root: View = itemView.find(R.id.root) val image: ImageView = itemView.find(R.id.image) val chapter: TextView = itemView.find(R.id.chapter) val name: TextView = itemView.find(R.id.name) val newIndicator: ImageView = itemView.find(R.id.newIndicator) val seenIndicator: View = itemView.find(R.id.seenIndicator) val favIndicator: ImageView = itemView.find(R.id.favIndicator) val actionMenu: View = itemView.find(R.id.actionMenu) val downloadedChip: Chip = itemView.find(R.id.downloadedChip) private val layDownloading: View = itemView.find(R.id.layDownloading) private val progressIndicator: CircularProgressIndicator = itemView.find(R.id.progressIndicator) private val actionCancel: View = itemView.find(R.id.actionCancel) private lateinit var state: RecentState private var checkJob: Job? = null private val favoriteObserver = Observer { if (state.isFavorite == it) return@Observer state.isFavorite = it favIndicator.isVisibleAnimate = it newIndicator.isVisible = !it && state.isFavorite } private val seenObserver = Observer { if (state.isSeen == it > 0) return@Observer state.isSeen = it > 0 seenIndicator.isVisibleAnimate = it > 0 } private val downloadObserver = Observer { if (state.downloadObject == it) return@Observer state.downloadObject = it if (it != null && it.isDownloadingOrPaused) { if (!layDownloading.isVisible) layDownloading.isVisibleAnimate = true when (it.state) { DownloadObject.DOWNLOADING, DownloadObject.PAUSED -> { progressIndicator.isVisible = false progressIndicator.isIndeterminate = false progressIndicator.isVisible = true if (it.getEta() == -2L || PrefsUtil.downloaderType == 0) { var progress = it.progress if (it.getEta() == -2L && PrefsUtil.downloaderType != 0) { progressIndicator.max = 200 progress += 100 } else { progressIndicator.max = 100 } progressIndicator.setProgressCompat(progress, true) } else { progressIndicator.max = 200 progressIndicator.setProgressCompat(it.progress, true) } } DownloadObject.PENDING -> { progressIndicator.isVisible = false progressIndicator.isIndeterminate = true progressIndicator.isVisible = true } } } else { lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { if (layDownloading.isVisible) layDownloading.isVisibleAnimate = false state.checkIsDownloaded if (withContext(Dispatchers.IO) { state.canPlay }) { if (!downloadedChip.isVisible) downloadedChip.isVisibleAnimate = true } } } } fun setUp(item: RecentModel) { this.state = item.state state.favoriteLive.observe(lifecycleOwner, favoriteObserver) state.seenLive.observe(lifecycleOwner, seenObserver) state.downloadLive.observe(lifecycleOwner, downloadObserver) setUpDownloadIndicators(item) } private fun setUpDownloadIndicators(item: RecentModel) { checkJob = GlobalScope.launch(Dispatchers.Main) { layDownloading.isVisible = false downloadedChip.isVisible = false when { state.downloadObject?.isDownloadingOrPaused == true -> { if (!isActive) return@launch layDownloading.isVisible = true downloadedChip.isVisible = false state.downloadObject?.let { when (it.state) { DownloadObject.DOWNLOADING, DownloadObject.PAUSED -> { progressIndicator.isVisible = false progressIndicator.isIndeterminate = false progressIndicator.isVisible = true if (it.getEta() == -2L || PrefsUtil.downloaderType == 0) { var progress = it.progress if (it.getEta() == -2L && PrefsUtil.downloaderType != 0) { progressIndicator.max = 200 progress += 100 } else { progressIndicator.max = 100 } progressIndicator.setProgressCompat(progress, true) } else { progressIndicator.max = 200 progressIndicator.setProgressCompat(it.progress, true) } } DownloadObject.PENDING -> { progressIndicator.isVisible = false progressIndicator.isIndeterminate = true progressIndicator.isVisible = true } } } } withContext(Dispatchers.IO) { state.isDownloaded } -> { if (!isActive) return@launch layDownloading.isVisible = false downloadedChip.isVisible = true } else -> { if (!isActive) return@launch layDownloading.isVisible = false downloadedChip.isVisible = false } } } actionCancel.onClick { MaterialDialog(itemView.context).safeShow { lifecycleOwner(lifecycleOwner) message(text = "¿Deseas cancelar esta descarga?") positiveButton(text = "confirmar") { item.state.isDownloaded = false lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { item.state.isDownloaded = false FileAccessHelper.deletePath(item.extras.filePath, true) DownloadManagerCentral.cancel(item.extras.eid) QueueManager.remove(item.extras.eid) } } negativeButton(text = "abortar") } } } fun recycle() { checkJob?.cancel() if (!::state.isInitialized) return state.favoriteLive.removeObserver(favoriteObserver) state.seenLive.removeObserver(seenObserver) state.downloadLive.removeObserver(downloadObserver) } } class AdsDealsViewHolder(private val scope: CoroutineScope, view: View): ViewHolder(view) class AdsViewHolder(private val scope: CoroutineScope, view: View) : ViewHolder(view) { //private val nativeAdView: NativeAdView = itemView.find(R.id.nativeAdView) private val iconV: ShapeableImageView = itemView.find(R.id.icon) /*private val primary: TextView = itemView.find(R.id.primary) private val secondary: TextView = itemView.find(R.id.secondary) private val cta: Chip = itemView.find(R.id.cta)*/ /*init { nativeAdView.apply { iconView = iconV headlineView = primary bodyView = secondary callToActionView = cta } }*/ @SuppressLint("DefaultLocale") fun setAd(modelAd: RecentModelAd) { /*modelAd.unifiedNativeAd.apply { scope.launch(Dispatchers.Main) { if (icon == null) iconV.setImageDrawable( ColorDrawable( ContextCompat.getColor( App.context, EAHelper.getThemeColorLight() ) ) ) else { iconV.setImageDrawable(icon!!.drawable) } primary.text = headline secondary.text = body if (callToAction != null) { cta.text = callToAction!!.lowercase(Locale.getDefault()) .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } nativeAdView.setNativeAd(modelAd.unifiedNativeAd) } }*/ } } } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentModelsFragment.kt ================================================ package knf.kuma.recents import android.content.pm.ActivityInfo import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.AdsUtils import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.noCrash import knf.kuma.custom.BannerContainerView import knf.kuma.home.HomeFragmentMaterial import knf.kuma.recents.viewholders.RecyclerRefreshHolder import knf.kuma.videoservers.FileActions import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.jetbrains.anko.support.v4.find class RecentModelsFragment : BottomFragment(), SwipeRefreshLayout.OnRefreshListener { private val viewModel: RecentModelsViewModel by viewModels() private val holder: RecyclerRefreshHolder by lazy { RecyclerRefreshHolder(requireView()).also { it.refreshLayout.setOnRefreshListener(this@RecentModelsFragment) it.recyclerView.adapter = adapter } } private val adapter: RecentModelsAdapter by lazy { RecentModelsAdapter(this) } private var isFirstLoad = true override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) lifecycleScope.launch(Dispatchers.Main) { viewModel.dbLiveData.collectLatest { objects -> holder.setError(objects.isEmpty()) holder.setRefreshing(false) if (adapter.itemCount == 0 || objects.isNotEmpty() && objects[0].hashCode().toLong() != adapter.getItemId(0)) { adapter.updateList(objects) { loadAds(objects) if (isFirstLoad) { holder.recyclerView.scheduleLayoutAnimation() isFirstLoad = false } else { holder.scrollToTop() } } scrollByKey(objects) } } } updateList() } private fun scrollByKey(list: List) { if (list.isEmpty()) return val initial = arguments?.getInt("initial", -1) ?: -1 if (initial == -1) { noCrash { holder.scrollToTop() } return } val find = list.find { it.key == initial } ?: return holder.scrollTo(list.indexOf(find)) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_recent_material, container, false) EAHelper.enter1("R") return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) holder.setRefreshing(true) } private fun loadAds(list: List) { if (PrefsUtil.isAdsEnabled) { val adContainer = find(R.id.adContainer) if (AdsUtils.isAdmobEnabled && PrefsUtil.isNativeAdsEnabled) { if (adapter.hasAds()) return /*lifecycleScope.launch(Dispatchers.Main) { NativeManager.take(this, 3) { if (it.isEmpty()) adContainer.implBanner(AdsType.RECENT_BANNER, true) else { adapter.updateList(list, it.mapIndexed { index, unifiedNativeAd -> RecentModelAd(index, unifiedNativeAd) }) { holder.scrollToTop() } } } }*/ } else adContainer.implBanner(AdsType.RECENT_BANNER, true) } } override fun onRefresh() { updateList() } private fun updateList() { if (!Network.isConnected) { holder.setRefreshing(false) } else { viewModel.reload() } } override fun onReselect() { EAHelper.enter1("R") noCrash { holder.scrollToTop() } } override fun onDestroyView() { super.onDestroyView() ServersFactory.clear() FileActions.reset() (activity as? AppCompatActivity)?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } companion object { fun get(initialKey: Int): BottomFragment { val fragment = RecentModelsFragment() val bundle = Bundle() bundle.putInt("initial", initialKey.also { Log.e("Recent", "Add argument key: $it") }) fragment.arguments = bundle return fragment } fun get(): BottomFragment { return if (PrefsUtil.useHome) HomeFragmentMaterial() else RecentModelsFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentModelsViewModel.kt ================================================ package knf.kuma.recents import androidx.lifecycle.ViewModel import knf.kuma.database.CacheDB import knf.kuma.retrofit.Repository import kotlinx.coroutines.flow.Flow class RecentModelsViewModel : ViewModel() { private val repository = Repository() val dbLiveData: Flow> = CacheDB.INSTANCE.recentModelsDAO().allFlow /*.onEach { list -> withContext(Dispatchers.IO){ list.forEach { it.prepare() it.extras.fileWrapper.exist } } }*/ fun reload() { repository.reloadRecentsAlt() } } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentsActivity.kt ================================================ package knf.kuma.recents import androidx.fragment.app.Fragment import knf.kuma.custom.SingleFragmentActivity class RecentsActivity : SingleFragmentActivity() { override fun createFragment(): Fragment = RecentFragment.get(intent.getIntExtra("initial", -1)) override fun getActivityTitle(): String = "Recientes" } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentsAdapter.kt ================================================ package knf.kuma.recents import android.content.Context import android.content.pm.ActivityInfo import android.os.Build import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.ads.AdCallback import knf.kuma.ads.AdCardItemHolder import knf.kuma.ads.AdRecentObject import knf.kuma.ads.AdsUtilsMob import knf.kuma.ads.implAdsRecent import knf.kuma.animeinfo.ActivityAnime import knf.kuma.backup.firestore.syncData import knf.kuma.cast.CastMedia import knf.kuma.commons.CastUtil import knf.kuma.commons.FileWrapper import knf.kuma.commons.Network import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.distinct import knf.kuma.commons.doOnUI import knf.kuma.commons.isFullMode import knf.kuma.commons.load import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashLet import knf.kuma.commons.safeShow import knf.kuma.custom.SeenAnimeOverlay import knf.kuma.database.CacheDB import knf.kuma.download.DownloadManagerCentral import knf.kuma.download.FileAccessHelper import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeenObject import knf.kuma.queue.QueueManager import knf.kuma.videoservers.ServersFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster.toast import java.util.Locale class RecentsAdapter internal constructor(private val fragment: Fragment, private val view: View) : RecyclerView.Adapter() { private val context: Context? = fragment.context private var list: MutableList = ArrayList() private val dao = CacheDB.INSTANCE.favsDAO() private val animeDAO = CacheDB.INSTANCE.animeDAO() private val chaptersDAO = CacheDB.INSTANCE.seenDAO() private val recordsDAO = CacheDB.INSTANCE.recordsDAO() private val downloadsDAO = CacheDB.INSTANCE.downloadsDAO() private var isNetworkAvailable: Boolean = Network.isConnected override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { if (viewType == 1) return AdCardItemHolder(parent).also { it.loadAd(fragment.lifecycleScope, object : AdCallback { override fun getID(): String = AdsUtilsMob.RECENT_BANNER }, 500) } return ItemHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_recents, parent, false)) } override fun getItemViewType(position: Int): Int { return noCrashLet { if (list[position] is AdRecentObject) 1 else 0 } ?: 1 } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { noCrash { if (context == null || list.isEmpty()) return@noCrash if (holder is ItemHolder) { holder.unsetObservers() val recentObject = noCrashLet { list[position] } ?: return@noCrash holder.imageView.load(PatternUtil.getCover(recentObject.aid)) holder.setNew(recentObject.isNew) holder.setFav(recentObject.isFav) holder.setSeen(recentObject.isSeen) dao.favObserver(Integer.parseInt(recentObject.aid)).distinct.observe(fragment) { object1 -> holder.setFav((object1 != null).also { recentObject.isFav = it }) } holder.setChapterObserver(chaptersDAO.chapterSeen(recentObject.aid, recentObject.chapter).distinct, fragment, Observer { chapter -> holder.setSeen(chapter != null) }) holder.setState(isNetworkAvailable, recentObject.isDownloading) holder.apply { fileWrapperJob?.cancel() fileWrapperJob = fragment.lifecycleScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { recentObject.fileWrapper() } if (!isActive) return@launch holder.setState(isNetworkAvailable, recentObject.fileWrapper().exist || recentObject.isDownloading) holder.setDownloadObserver(downloadsDAO.getLiveByEid(recentObject.eid).distinct, fragment, Observer { downloadObject -> fragment.lifecycleScope.launch(Dispatchers.Main) { holder.setDownloadState(downloadObject) if (downloadObject == null) { recentObject.downloadState = -8 recentObject.isDownloading = false } else { recentObject.isDownloading = downloadObject.state == DownloadObject.DOWNLOADING || downloadObject.state == DownloadObject.PENDING || downloadObject.state == DownloadObject.PAUSED recentObject.downloadState = downloadObject.state if (downloadObject.state == DownloadObject.DOWNLOADING || downloadObject.state == DownloadObject.PENDING) holder.downIcon.setImageResource(R.drawable.ic_download) else if (downloadObject.state == DownloadObject.PAUSED) holder.downIcon.setImageResource(R.drawable.ic_pause_normal) withContext(Dispatchers.IO) { recentObject.fileWrapper().reset() } } holder.setState(isNetworkAvailable, recentObject.fileWrapper().exist || recentObject.isDownloading) } }) holder.setCastingObserver(fragment, Observer { s -> if (recentObject.eid == s) { holder.setCasting(true, recentObject.fileWrapper()) holder.streaming.setOnClickListener { CastUtil.get().openControls() } } else { holder.setCasting(false, recentObject.fileWrapper()) holder.streaming.setOnClickListener { if (recentObject.fileWrapper().exist || recentObject.isDownloading) { MaterialDialog(context).safeShow { message(text = "¿Eliminar el ${recentObject.chapter.lowercase( Locale.ENGLISH )} de ${recentObject.name}?") positiveButton(text = "CONFIRMAR") { FileAccessHelper.deletePath(recentObject.filePath, true) DownloadManagerCentral.cancel(recentObject.eid) QueueManager.remove(recentObject.eid) recentObject.fileWrapper().exist = false holder.setState(isNetworkAvailable, false) } negativeButton(text = "CANCELAR") } } else { holder.setLocked(true) ServersFactory.start(context, recentObject.url, DownloadObject.fromRecent(recentObject), true, object : ServersFactory.ServersInterface { override fun onFinish(started: Boolean, success: Boolean) { if (!started && success) { doAsync { chaptersDAO.addChapter(SeenObject.fromRecent(recentObject)) recordsDAO.add(RecordObject.fromRecent(recentObject)) syncData { history() seen() } } recentObject.isSeen = true } holder.setLocked(false) } override fun onCast(url: String?) { CastUtil.get().play(view, CastMedia.create(recentObject, url)) doAsync { chaptersDAO.addChapter(SeenObject.fromRecent(recentObject)) recordsDAO.add(RecordObject.fromRecent(recentObject)) syncData { history() seen() } } recentObject.isSeen = true holder.setSeen(true) holder.setLocked(false) } override fun onProgressIndicator(boolean: Boolean) { fragment.doOnUI { if (boolean) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE } else holder.progressBarRoot.visibility = View.GONE } } override fun getView(): View { return view } }) } } } }) } } holder.title.text = recentObject.name holder.chapter.text = recentObject.chapter if (!isFullMode) holder.layButtons.visibility = View.INVISIBLE holder.cardView.setOnClickListener { if (recentObject.animeObject != null) { ActivityAnime.open(fragment, recentObject.animeObject, holder.imageView) } else { if (!isFullMode && PrefsUtil.isDirectoryFinished) { toast("Anime deshabilitado para esta version") } else if (PrefsUtil.isFamilyFriendly && PrefsUtil.isDirectoryFinished) { toast("Anime no familiar") } else { fragment.lifecycleScope.launch(Dispatchers.Main) { val animeObject = withContext(Dispatchers.IO) { animeDAO.getByAid(recentObject.aid) } if (animeObject != null) { ActivityAnime.open(fragment, animeObject, holder.imageView) } else { ActivityAnime.open(fragment, recentObject, holder.imageView) } } } } } holder.cardView.setOnLongClickListener { if (!recentObject.isSeen) { doAsync { chaptersDAO.addChapter(SeenObject.fromRecent(recentObject)) } recentObject.isSeen = true holder.animeOverlay.setSeen(seen = true, animate = true) } else { doAsync { chaptersDAO.deleteChapter(recentObject.aid, recentObject.chapter) } recentObject.isSeen = false holder.animeOverlay.setSeen(seen = false, animate = true) } syncData { seen() } true } holder.download.setOnClickListener { fragment.lifecycleScope.launch(Dispatchers.Main){ val obj = withContext(Dispatchers.IO) { downloadsDAO.getByEid(recentObject.eid) } if (FileAccessHelper.canDownload(fragment) && !recentObject.fileWrapper().exist && !recentObject.isDownloading && recentObject.downloadState != DownloadObject.PENDING) { holder.setLocked(true) ServersFactory.start(context, recentObject.url, AnimeObject.WebInfo.AnimeChapter.fromRecent(recentObject), isStream = false, addQueue = false, serversInterface = object : ServersFactory.ServersInterface { override fun onFinish(started: Boolean, success: Boolean) { if (started) { recentObject.fileWrapper().exist = true holder.setState(isNetworkAvailable, true) } holder.setLocked(false) } override fun onCast(url: String?) { } override fun onProgressIndicator(boolean: Boolean) { fragment.doOnUI { if (boolean) { holder.progressBar.isIndeterminate = true holder.progressBarRoot.visibility = View.VISIBLE } else holder.progressBarRoot.visibility = View.GONE } } override fun getView(): View { return view } }) } else if (recentObject.fileWrapper().exist && (obj == null || obj.state == DownloadObject.DOWNLOADING || obj.state == DownloadObject.COMPLETED)) { doAsync { chaptersDAO.addChapter(SeenObject.fromRecent(recentObject)) recordsDAO.add(RecordObject.fromRecent(recentObject)) syncData { history() seen() } } recentObject.isSeen = true holder.setSeen(true) ServersFactory.startPlay(context, recentObject.epTitle, recentObject.fileWrapper().name()) } else { toast("Aun no se está descargando") } } } holder.download.setOnLongClickListener { fragment.lifecycleScope.launch(Dispatchers.Main){ val obj = withContext(Dispatchers.IO) { downloadsDAO.getByEid(recentObject.eid) } if (CastUtil.get().connected() && recentObject.fileWrapper().exist && (obj == null || obj.state == DownloadObject.COMPLETED)) { doAsync { chaptersDAO.addChapter(SeenObject.fromRecent(recentObject)) syncData { seen() } } recentObject.isSeen = true CastUtil.get().play(view, CastMedia.create(recentObject)) } } true } } } } private fun setOrientation(block: Boolean) { noCrash { if (block) (fragment.activity as? AppCompatActivity)?.requestedOrientation = when { context?.resources?.getBoolean(R.bool.isLandscape) == true -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } else (fragment.activity as? AppCompatActivity)?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { if (holder is ItemHolder) holder.unsetObservers() super.onViewRecycled(holder) } internal fun updateList(list: MutableList, updateListener: UpdateListener) { this.isNetworkAvailable = Network.isConnected val wasEmpty = this.list.isEmpty() fragment.lifecycleScope.launch(Dispatchers.IO) { this@RecentsAdapter.list = list.distinctBy { it.eid } as MutableList if (PrefsUtil.isNativeAdsEnabled) this@RecentsAdapter.list.implAdsRecent() if (this@RecentsAdapter.list.isNotEmpty()) withContext(Dispatchers.Main) { notifyDataSetChanged() if (wasEmpty) updateListener.invoke() } } } /*override fun getItemId(position: Int): Long { return noCrashLet { list[position].key.toLong() } ?: 0 }*/ override fun getItemCount(): Int { return list.size } inner class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView = itemView.find(R.id.card) val imageView: ImageView = itemView.find(R.id.img) val title: TextView = itemView.find(R.id.title) val chapter: TextView = itemView.find(R.id.chapter) val streaming: Button = itemView.find(R.id.streaming) val download: Button = itemView.find(R.id.download) val animeOverlay: SeenAnimeOverlay = itemView.find(R.id.seenOverlay) val downIcon: ImageView = itemView.find(R.id.down_icon) private val newIcon: ImageView = itemView.find(R.id.new_icon) private val favIcon: ImageView = itemView.find(R.id.fav_icon) val progressBar: ProgressBar = itemView.find(R.id.progress) val progressBarRoot: View = itemView.find(R.id.progress_root) val layButtons: View = itemView.find(R.id.lay_buttons) var fileWrapperJob: Job? = null private var chapterLiveData: LiveData = MutableLiveData() private var downloadLiveData: LiveData = MutableLiveData() private var chapterObserver: Observer? = null private var downloadObserver: Observer? = null private var castingObserver: Observer? = null fun setChapterObserver(chapterLiveData: LiveData, owner: LifecycleOwner, observer: Observer) { this.chapterLiveData = chapterLiveData this.chapterObserver = observer this.chapterLiveData.observe(owner, observer) } private fun unsetChapterObserver() { chapterObserver?.let { chapterLiveData.removeObserver(it) chapterObserver = null } } fun setDownloadObserver(downloadLiveData: LiveData, owner: LifecycleOwner, observer: Observer) { this.downloadLiveData = downloadLiveData this.downloadObserver = observer this.downloadLiveData.observe(owner, observer) } private fun unsetDownloadObserver() { downloadObserver?.let { downloadLiveData.removeObserver(it) downloadObserver = null } } fun setCastingObserver(owner: LifecycleOwner, observer: Observer) { this.castingObserver = observer CastUtil.get().casting.observe(owner, observer) } private fun unsetCastingObserver() { castingObserver?.let { CastUtil.get().casting.removeObserver(it) castingObserver = null } } fun unsetObservers() { unsetChapterObserver() unsetDownloadObserver() unsetCastingObserver() } fun setNew(isNew: Boolean) { newIcon.post { newIcon.visibility = if (isNew) View.VISIBLE else View.GONE } } fun setFav(isFav: Boolean) { favIcon.post { favIcon.visibility = if (isFav) View.VISIBLE else View.GONE } } private fun setDownloaded(isDownloaded: Boolean) { downIcon.post { downIcon.visibility = if (isDownloaded) View.VISIBLE else View.GONE } } fun setSeen(seen: Boolean) { animeOverlay.setSeen(seen, false) } fun setLocked(locked: Boolean) { streaming.post { streaming.isEnabled = !locked } download.post { download.isEnabled = !locked } setOrientation(locked) } fun setCasting(casting: Boolean, fileWrapper: FileWrapper<*>) { streaming.post { streaming.text = if (casting) "CAST" else if (fileWrapper.exist) "ELIMINAR" else "STREAMING" } } @UiThread fun setState(isNetworkAvailable: Boolean, existFile: Boolean) { setDownloaded(existFile) streaming.post { streaming.text = if (existFile) "ELIMINAR" else "STREAMING" if (!existFile) streaming.isEnabled = isNetworkAvailable else streaming.isEnabled = true } download.post { download.isEnabled = isNetworkAvailable || existFile download.text = if (existFile) "REPRODUCIR" else "DESCARGA" } } fun setDownloadState(downloadObject: DownloadObject?) { progressBar.post { if (downloadObject != null && PrefsUtil.showProgress()) when (downloadObject.state) { DownloadObject.PENDING -> { progressBarRoot.visibility = View.VISIBLE progressBar.isIndeterminate = true } DownloadObject.DOWNLOADING -> { progressBarRoot.visibility = View.VISIBLE progressBar.isIndeterminate = false if (downloadObject.getEta() == -2L || PrefsUtil.downloaderType == 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(downloadObject.progress, true) else progressBar.progress = downloadObject.progress if (downloadObject.getEta() == -2L && PrefsUtil.downloaderType != 0) progressBar.secondaryProgress = 100 } else { progressBar.progress = 0 progressBar.secondaryProgress = downloadObject.progress } } DownloadObject.PAUSED -> { progressBarRoot.visibility = View.VISIBLE progressBar.isIndeterminate = false } else -> progressBarRoot.visibility = View.GONE } else progressBarRoot.visibility = View.GONE } } } } typealias UpdateListener = () -> Unit ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentsModelActivity.kt ================================================ package knf.kuma.recents import androidx.fragment.app.Fragment import knf.kuma.custom.SingleFragmentMaterialActivity class RecentsModelActivity : SingleFragmentMaterialActivity() { override fun createFragment(): Fragment = RecentModelsFragment.get(intent.getIntExtra("initial", -1)) override fun getActivityTitle(): String = "Recientes" } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentsNotReceiver.kt ================================================ package knf.kuma.recents import android.app.NotificationManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import knf.kuma.database.CacheDB import knf.kuma.jobscheduler.RecentsWork import knf.kuma.pojos.NotificationObj import org.jetbrains.anko.doAsync import org.jetbrains.anko.notificationManager class RecentsNotReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val notificationDAO = CacheDB.INSTANCE.notificationDAO() if (intent.getIntExtra("mode", 0) == 1) removeAll(context) else { doAsync { notificationDAO.delete(NotificationObj.fromIntent(intent)) if (notificationDAO.getByType(NotificationObj.RECENT).isEmpty()) context.notificationManager.cancel(RecentsWork.KEY_SUMMARY) } } } companion object { fun removeAll(context: Context) { doAsync { val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val notificationDAO = CacheDB.INSTANCE.notificationDAO() for (obj in notificationDAO.all) manager.cancel(obj.key) notificationDAO.clear() manager.cancel(RecentsWork.KEY_SUMMARY) } } } } ================================================ FILE: app/src/main/java/knf/kuma/recents/RecentsViewModel.kt ================================================ package knf.kuma.recents import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import knf.kuma.database.CacheDB import knf.kuma.pojos.RecentObject import knf.kuma.retrofit.Repository import kotlinx.coroutines.flow.Flow class RecentsViewModel : ViewModel() { private val repository = Repository() val dbLiveData: LiveData> get() = CacheDB.INSTANCE.recentsDAO().objects val dbFlow: Flow> get() = CacheDB.INSTANCE.recentsDAO().objectsFlow fun reload() { repository.reloadAllRecents() } } ================================================ FILE: app/src/main/java/knf/kuma/recents/viewholders/RecyclerRefreshHolder.kt ================================================ package knf.kuma.recents.viewholders import android.view.View import android.view.animation.AnimationUtils import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import knf.kuma.R import knf.kuma.commons.EAHelper import knf.kuma.custom.VariantLinearLayoutManager import org.jetbrains.anko.find class RecyclerRefreshHolder(view: View) { val recyclerView: RecyclerView = view.find(R.id.recycler) val refreshLayout: SwipeRefreshLayout = view.find(R.id.refresh) val error: View = view.find(R.id.error) private val layoutManager: LinearLayoutManager = VariantLinearLayoutManager(view.context) init { recyclerView.layoutManager = layoutManager recyclerView.layoutAnimation = AnimationUtils.loadLayoutAnimation(view.context, R.anim.layout_fall_down) refreshLayout.setColorSchemeResources(EAHelper.getThemeColor(), EAHelper.getThemeColorLight(), R.color.colorPrimary) } fun scrollToTop() { layoutManager.smoothScrollToPosition(recyclerView, null, 0) } fun scrollTo(position: Int) { layoutManager.smoothScrollToPosition(recyclerView, null, position) } fun setRefreshing(refreshing: Boolean) { refreshLayout.post { refreshLayout.isRefreshing = refreshing } } fun setError(visible: Boolean) { error.post { error.visibility = if (visible) View.VISIBLE else View.GONE } } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/AnimeShortObject.kt ================================================ package knf.kuma.recommended class AnimeShortObject { var key = 0 var aid = "" var img = "" var link = "" var name = "" var type = "" } ================================================ FILE: app/src/main/java/knf/kuma/recommended/BlacklistDialog.kt ================================================ package knf.kuma.recommended import android.app.Dialog import android.os.Bundle import androidx.fragment.app.DialogFragment import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.afollestad.materialdialogs.list.listItemsMultiChoice import knf.kuma.commons.transform import java.util.Arrays class BlacklistDialog : DialogFragment() { private val genres = getGenres() private var selected: MutableList = mutableListOf() private var listener: MultiChoiceListener? = null private val statesIndex: IntArray get() { val states = IntArray(selected.size) selected.forEachIndexed { index, genre -> states[index] = genres.indexOf(genre) } return states } fun init(selected: MutableList, listener: MultiChoiceListener) { this.selected = selected this.listener = listener } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return activity?.let { MaterialDialog(it).apply { lifecycleOwner() title(text = "Lista negra") listItemsMultiChoice(items = genres, initialSelection = statesIndex, allowEmptySelection = true) { _, _, items -> selected = mutableListOf().apply { addAll(items.transform()) sort() } listener?.onOkay(selected) } positiveButton(text = "SELECCIONAR") negativeButton(text = "CERRAR") } } ?: super.onCreateDialog(savedInstanceState) } interface MultiChoiceListener { fun onOkay(selected: MutableList) } companion object { fun getGenres(): MutableList { return Arrays.asList( "Acción", "Artes Marciales", "Aventuras", "Carreras", "Comedia", "Demencia", "Demonios", "Deportes", "Drama", "Ecchi", "Escolares", "Espacial", "Fantasía", "Ciencia Ficción", "Harem", "Historico", "Infantil", "Josei", "Juegos", "Magia", "Mecha", "Militar", "Misterio", "Musica", "Parodia", "Policía", "Psicológico", "Recuentos de la vida", "Romance", "Samurai", "Seinen", "Shoujo", "Shounen", "Sin Generos", "Sobrenatural", "Superpoderes", "Suspenso", "Terror", "Vampiros", "Yaoi", "Yuri") } } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RHHolder.kt ================================================ package knf.kuma.recommended import android.view.View import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import org.jetbrains.anko.find /** * Created by jordy on 26/03/2018. */ class RHHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val title: TextView = itemView.find(R.id.title) } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RIHolder.kt ================================================ package knf.kuma.recommended import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.commons.bind /** * Created by jordy on 26/03/2018. */ class RIHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val img: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val type: TextView by itemView.bind(R.id.type) } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RankType.kt ================================================ package knf.kuma.recommended /** * Created by jordy on 26/03/2018. */ enum class RankType { FAV, UNFAV, FOLLOW, UNFOLLOW, CHECK, SEARCH } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RankingActivity.kt ================================================ package knf.kuma.recommended import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.lifecycle.lifecycleScope import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerRankingBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync class RankingActivity : GenericActivity() { private val binding by lazy { RecyclerRankingBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(false) title = "Ranking" } with(binding.toolbar) { setNavigationIcon(R.drawable.ic_close) setNavigationOnClickListener { finish() } } with(binding.recycler) { layoutManager = VariantLinearLayoutManager(this@RankingActivity) lifecycleScope.launch(Dispatchers.Main){ adapter = RankingAdapterMaterial(withContext(Dispatchers.IO) { CacheDB.INSTANCE.genresDAO().ranking }) } } setResult(1234) showRandomInterstitial(this, PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_rating, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.clear -> MaterialDialog(this@RankingActivity).safeShow { message(text = "¿Deseas reiniciar la puntuación de todos los géneros?") positiveButton(text = "continuar") { setResult(4321) doAsync { CacheDB.INSTANCE.genresDAO().reset() syncData { genres() } } finish() } negativeButton(text = "cancelar") } } return super.onOptionsItemSelected(item) } companion object { fun open(activity: Activity) { activity.startActivityForResult(Intent(activity, RankingActivity::class.java), 46897) } } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RankingActivityMaterial.kt ================================================ package knf.kuma.recommended import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.lifecycle.lifecycleScope import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerRankingMaterialBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync class RankingActivityMaterial : GenericActivity() { private val binding by lazy { RecyclerRankingMaterialBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(false) title = "Ranking" } with(binding.toolbar) { setNavigationIcon(R.drawable.ic_close) setNavigationOnClickListener { finish() } } with(binding.recycler) { layoutManager = VariantLinearLayoutManager(this@RankingActivityMaterial) lifecycleScope.launch(Dispatchers.Main){ adapter = RankingAdapterMaterial(withContext(Dispatchers.IO) { CacheDB.INSTANCE.genresDAO().ranking }) } } setResult(1234) showRandomInterstitial(this, PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_rating, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.clear -> MaterialDialog(this@RankingActivityMaterial).safeShow { message(text = "¿Deseas reiniciar la puntuación de todos los géneros?") positiveButton(text = "continuar") { setResult(4321) doAsync { CacheDB.INSTANCE.genresDAO().reset() syncData { genres() } } finish() } negativeButton(text = "cancelar") } } return super.onOptionsItemSelected(item) } companion object { fun open(activity: Activity) { activity.startActivityForResult(Intent(activity, RankingActivityMaterial::class.java), 46897) } } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RankingAdapter.kt ================================================ package knf.kuma.recommended import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.pojos.GenreStatusObject import org.jetbrains.anko.find class RankingAdapter(val list: List) : RecyclerView.Adapter() { private var total = 0 init { if (list.isNotEmpty()) total = list[0].count } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RankHolder { return RankHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_ranking, parent, false)) } override fun onBindViewHolder(holder: RankHolder, position: Int) { val statusObject = list[position] holder.title.text = statusObject.name holder.count.text = statusObject.count.toString() holder.ranking.max = total holder.ranking.progress = statusObject.count } override fun getItemCount(): Int { return list.size } class RankHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val title: TextView = itemView.find(R.id.title) val count: TextView = itemView.find(R.id.count) val ranking: ProgressBar = itemView.find(R.id.ranking) } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RankingAdapterMaterial.kt ================================================ package knf.kuma.recommended import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.pojos.GenreStatusObject import org.jetbrains.anko.find class RankingAdapterMaterial(val list: List) : RecyclerView.Adapter() { private var total = 0 init { if (list.isNotEmpty()) total = list[0].count } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RankHolder { return RankHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_ranking_material, parent, false)) } override fun onBindViewHolder(holder: RankHolder, position: Int) { val statusObject = list[position] holder.title.text = statusObject.name holder.count.text = statusObject.count.toString() holder.ranking.max = total holder.ranking.progress = statusObject.count } override fun getItemCount(): Int { return list.size } class RankHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val title: TextView = itemView.find(R.id.title) val count: TextView = itemView.find(R.id.count) val ranking: ProgressBar = itemView.find(R.id.ranking) } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RecommendActivity.kt ================================================ package knf.kuma.recommended import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.LayoutRes import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.firebase.crashlytics.FirebaseCrashlytics import io.github.luizgrp.sectionedrecyclerviewadapter.SectionedRecyclerViewAdapter import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.gridColumns import knf.kuma.commons.removeAll import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantGridLayoutManager import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.pojos.GenreStatusObject import knf.kuma.recommended.sections.MultipleSection import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster /** * Created by jordy on 26/03/2018. */ class RecommendActivity : GenericActivity() { val toolbar: Toolbar by bind(R.id.toolbar) val recyclerView: RecyclerView by bind(R.id.recycler) val error: LinearLayout by bind(R.id.error) val loading: LinearLayout by bind(R.id.loading) val state: TextView by bind(R.id.state) private val dao = CacheDB.INSTANCE.animeDAO() private val defaultGridColumns = gridColumns() private val layout: Int @LayoutRes get() = if (isGrid) { R.layout.recycler_recommends } else { R.layout.recycler_recommends_grid } private val isGrid: Boolean get() = PrefsUtil.layType != "0" override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(layout) toolbar.title = "Sugeridos" setSupportActionBar(toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } find(R.id.adContainer).implBanner(AdsType.RECOMMEND_BANNER, true) setAdapter() } private fun setAdapter() { doAsync { try { val excludeList = LinkedHashSet().apply { addAll(CacheDB.INSTANCE.favsDAO().allAids) addAll(CacheDB.INSTANCE.seeingDAO().allAids) }.toList() val status = CacheDB.INSTANCE.genresDAO().top setState("Revisando generos") if (status.size == 3) { setState("Buscando sugerencias") val sectionedAdapter = SectionedRecyclerViewAdapter() val abc = getList(status[0], status[1], status[2]) val ab = getList(status[0], status[1]) ab.removeAll(abc) val ac = getList(status[0], status[2]) ac.removeAll(abc, ab) val bc = getList(status[1], status[2]) bc.removeAll(abc, ab, ac) val a = getList(status[0]) a.removeAll(abc, ab, ac, bc) val b = getList(status[1]) b.removeAll(abc, ab, ac, bc, a) val c = getList(status[2]) c.removeAll(abc, ab, ac, bc, a, b) setState("Filtrando lista") removeFavs(excludeList, abc, ab, ac, bc, a, b, c) if (abc.size > 0) sectionedAdapter.addSection(MultipleSection(this@RecommendActivity, getStringTitle(status[0], status[1], status[2]), getAnimeList(abc), isGrid)) if (ab.size > 0) sectionedAdapter.addSection(MultipleSection(this@RecommendActivity, getStringTitle(status[0], status[1]), getAnimeList(ab), isGrid)) if (ac.size > 0) sectionedAdapter.addSection(MultipleSection(this@RecommendActivity, getStringTitle(status[0], status[2]), getAnimeList(ac), isGrid)) if (bc.size > 0) sectionedAdapter.addSection(MultipleSection(this@RecommendActivity, getStringTitle(status[1], status[2]), getAnimeList(bc), isGrid)) if (a.size > 0) sectionedAdapter.addSection(MultipleSection(this@RecommendActivity, getStringTitle(status[0]), getAnimeList(a), isGrid)) if (b.size > 0) sectionedAdapter.addSection(MultipleSection(this@RecommendActivity, getStringTitle(status[1]), getAnimeList(b), isGrid)) if (c.size > 0) sectionedAdapter.addSection(MultipleSection(this@RecommendActivity, getStringTitle(status[2]), getAnimeList(c), isGrid)) val layoutManager: RecyclerView.LayoutManager if (isGrid) { val grid = VariantGridLayoutManager(this@RecommendActivity, defaultGridColumns) grid.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return try { when (sectionedAdapter.getSectionItemViewType(position)) { SectionedRecyclerViewAdapter.VIEW_TYPE_HEADER -> defaultGridColumns else -> 1 } } catch (e: Exception) { e.printStackTrace() defaultGridColumns } } } layoutManager = grid } else { layoutManager = VariantLinearLayoutManager(this@RecommendActivity) } runOnUiThread { loading.visibility = View.GONE recyclerView.layoutManager = layoutManager recyclerView.adapter = sectionedAdapter } } else runOnUiThread { loading.visibility = View.GONE error.visibility = View.VISIBLE } } catch (e: Exception) { e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) Toaster.toast("Error al cargar recomendados") runOnUiThread { loading.visibility = View.GONE } } } } @SafeVarargs private fun removeFavs(excludeList: List, vararg lists: MutableList) { lists.forEach { list -> list.removeAll(list.filter { excludeList.contains(it) }) } } private fun getList(vararg status: GenreStatusObject): MutableList { return dao.getAidsByGenres(getString(*status)) } private fun getAnimeList(list: List): MutableList { val chunk = list.chunked(900) val animes = mutableListOf() chunk.forEach { animes.addAll(CacheDB.INSTANCE.animeDAO().getAnimesByAids(it)) } return animes } private fun getString(vararg status: GenreStatusObject): String { val builder = StringBuilder("%") for (s in status) { builder.append(s.name) .append("%") } return builder.toString() } private fun getStringTitle(vararg status: GenreStatusObject): String { val builder = StringBuilder() for (s in status) { builder.append(s.name) .append(", ") } return builder.toString().substring(0, builder.length - 2) } private fun setState(stateString: String) { runOnUiThread { state.text = stateString } } private fun showBlacklist() { lifecycleScope.launch(Dispatchers.Main) { val blacklist = withContext(Dispatchers.IO) { GenreStatusObject.names(CacheDB.INSTANCE.genresDAO().blacklist) } val dialog = BlacklistDialog() dialog.init(blacklist, object : BlacklistDialog.MultiChoiceListener { override fun onOkay(selected: MutableList) { setBlacklist(selected) } }) dialog.show(supportFragmentManager, "Blacklist") } } private fun setBlacklist(selected: MutableList) { doAsync { for (s in selected) RecommendHelper.block(s) for (statusObject in CacheDB.INSTANCE.genresDAO().all) if (statusObject.isBlocked && !selected.contains(statusObject.name)) RecommendHelper.reset(statusObject.name) resetSuggestions() } } private fun resetSuggestions() { setState("Iniciando búsqueda") runOnUiThread { val adapter = recyclerView.adapter as SectionedRecyclerViewAdapter? if (adapter != null) { adapter.removeAllSections() recyclerView.adapter = adapter loading.visibility = View.VISIBLE error.visibility = View.GONE setAdapter() } } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_suggestions, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.blacklist -> showBlacklist() R.id.rating -> RankingActivity.open(this) } return super.onOptionsItemSelected(item) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == 4321) resetSuggestions() } companion object { fun open(context: Context) { context.startActivity(Intent(context, RecommendActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RecommendActivityMaterial.kt ================================================ package knf.kuma.recommended import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.LayoutRes import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.firebase.crashlytics.FirebaseCrashlytics import io.github.luizgrp.sectionedrecyclerviewadapter.SectionedRecyclerViewAdapter import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.gridColumns import knf.kuma.commons.removeAll import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantGridLayoutManager import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.pojos.GenreStatusObject import knf.kuma.recommended.sections.MultipleSectionMaterial import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.find import xdroid.toaster.Toaster /** * Created by jordy on 26/03/2018. */ class RecommendActivityMaterial : GenericActivity() { val toolbar: Toolbar by bind(R.id.toolbar) val recyclerView: RecyclerView by bind(R.id.recycler) val error: LinearLayout by bind(R.id.error) val loading: LinearLayout by bind(R.id.loading) val state: TextView by bind(R.id.state) private val dao = CacheDB.INSTANCE.animeDAO() private val defaultGridColumns = gridColumns() private val layout: Int @LayoutRes get() = if (isGrid) { R.layout.recycler_recommends_material } else { R.layout.recycler_recommends_grid_material } private val isGrid: Boolean get() = PrefsUtil.layType != "0" override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(layout) toolbar.title = "Sugeridos" setSupportActionBar(toolbar) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } find(R.id.adContainer).implBanner(AdsType.RECOMMEND_BANNER, true) setAdapter() } private fun setAdapter() { doAsync { try { val excludeList = LinkedHashSet().apply { addAll(CacheDB.INSTANCE.favsDAO().allAids) addAll(CacheDB.INSTANCE.seeingDAO().allAids) }.toList() val status = CacheDB.INSTANCE.genresDAO().top setState("Revisando generos") if (status.size == 3) { setState("Buscando sugerencias") val sectionedAdapter = SectionedRecyclerViewAdapter() val abc = getList(status[0], status[1], status[2]) val ab = getList(status[0], status[1]) ab.removeAll(abc) val ac = getList(status[0], status[2]) ac.removeAll(abc, ab) val bc = getList(status[1], status[2]) bc.removeAll(abc, ab, ac) val a = getList(status[0]) a.removeAll(abc, ab, ac, bc) val b = getList(status[1]) b.removeAll(abc, ab, ac, bc, a) val c = getList(status[2]) c.removeAll(abc, ab, ac, bc, a, b) setState("Filtrando lista") removeFavs(excludeList, abc, ab, ac, bc, a, b, c) if (abc.size > 0) sectionedAdapter.addSection(MultipleSectionMaterial(this@RecommendActivityMaterial, getStringTitle(status[0], status[1], status[2]), getAnimeList(abc), isGrid)) if (ab.size > 0) sectionedAdapter.addSection(MultipleSectionMaterial(this@RecommendActivityMaterial, getStringTitle(status[0], status[1]), getAnimeList(ab), isGrid)) if (ac.size > 0) sectionedAdapter.addSection(MultipleSectionMaterial(this@RecommendActivityMaterial, getStringTitle(status[0], status[2]), getAnimeList(ac), isGrid)) if (bc.size > 0) sectionedAdapter.addSection(MultipleSectionMaterial(this@RecommendActivityMaterial, getStringTitle(status[1], status[2]), getAnimeList(bc), isGrid)) if (a.size > 0) sectionedAdapter.addSection(MultipleSectionMaterial(this@RecommendActivityMaterial, getStringTitle(status[0]), getAnimeList(a), isGrid)) if (b.size > 0) sectionedAdapter.addSection(MultipleSectionMaterial(this@RecommendActivityMaterial, getStringTitle(status[1]), getAnimeList(b), isGrid)) if (c.size > 0) sectionedAdapter.addSection(MultipleSectionMaterial(this@RecommendActivityMaterial, getStringTitle(status[2]), getAnimeList(c), isGrid)) val layoutManager: RecyclerView.LayoutManager if (isGrid) { val grid = VariantGridLayoutManager(this@RecommendActivityMaterial, defaultGridColumns) grid.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return try { when (sectionedAdapter.getSectionItemViewType(position)) { SectionedRecyclerViewAdapter.VIEW_TYPE_HEADER -> defaultGridColumns else -> 1 } } catch (e: Exception) { e.printStackTrace() defaultGridColumns } } } layoutManager = grid } else { layoutManager = VariantLinearLayoutManager(this@RecommendActivityMaterial) } runOnUiThread { loading.visibility = View.GONE recyclerView.layoutManager = layoutManager recyclerView.adapter = sectionedAdapter } } else runOnUiThread { loading.visibility = View.GONE error.visibility = View.VISIBLE } } catch (e: Exception) { e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) Toaster.toast("Error al cargar recomendados") runOnUiThread { loading.visibility = View.GONE } } } } @SafeVarargs private fun removeFavs(excludeList: List, vararg lists: MutableList) { lists.forEach { list -> list.removeAll(list.filter { excludeList.contains(it) }) } } private fun getList(vararg status: GenreStatusObject): MutableList { return dao.getAidsByGenres(getString(*status)) } private fun getAnimeList(list: List): MutableList { val chunk = list.chunked(900) val animes = mutableListOf() chunk.forEach { animes.addAll(CacheDB.INSTANCE.animeDAO().getAnimesByAids(it)) } return animes } private fun getString(vararg status: GenreStatusObject): String { val builder = StringBuilder("%") for (s in status) { builder.append(s.name) .append("%") } return builder.toString() } private fun getStringTitle(vararg status: GenreStatusObject): String { val builder = StringBuilder() for (s in status) { builder.append(s.name) .append(", ") } return builder.toString().substring(0, builder.length - 2) } private fun setState(stateString: String) { runOnUiThread { state.text = stateString } } private fun showBlacklist() { lifecycleScope.launch(Dispatchers.Main) { val blacklist = withContext(Dispatchers.IO) { GenreStatusObject.names(CacheDB.INSTANCE.genresDAO().blacklist) } val dialog = BlacklistDialog() dialog.init(blacklist, object : BlacklistDialog.MultiChoiceListener { override fun onOkay(selected: MutableList) { setBlacklist(selected) } }) dialog.show(supportFragmentManager, "Blacklist") } } private fun setBlacklist(selected: MutableList) { doAsync { for (s in selected) RecommendHelper.block(s) for (statusObject in CacheDB.INSTANCE.genresDAO().all) if (statusObject.isBlocked && !selected.contains(statusObject.name)) RecommendHelper.reset(statusObject.name) resetSuggestions() } } private fun resetSuggestions() { setState("Iniciando búsqueda") runOnUiThread { val adapter = recyclerView.adapter as SectionedRecyclerViewAdapter? if (adapter != null) { adapter.removeAllSections() recyclerView.adapter = adapter loading.visibility = View.VISIBLE error.visibility = View.GONE setAdapter() } } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_suggestions, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.blacklist -> showBlacklist() R.id.rating -> RankingActivityMaterial.open(this) } return super.onOptionsItemSelected(item) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == 4321) resetSuggestions() } companion object { fun open(context: Context) { context.startActivity(Intent(context, RecommendActivityMaterial::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/RecommendHelper.kt ================================================ package knf.kuma.recommended import knf.kuma.backup.firestore.syncData import knf.kuma.commons.removeAll import knf.kuma.database.CacheDB import knf.kuma.pojos.GenreStatusObject import org.jetbrains.anko.doAsync /** * Created by jordy on 26/03/2018. */ object RecommendHelper { fun registerAll(list: MutableList, type: RankType) { doAsync { for (genre in list) register(genre, type) syncData { genres() } } } private fun register(name: String, type: RankType) { doAsync { val status = getStatus(name) if (!status.isBlocked) { when (type) { RankType.FAV -> status.add(3) RankType.UNFAV -> status.sub(3) RankType.FOLLOW -> status.add(2) RankType.UNFOLLOW -> status.sub(2) RankType.CHECK -> status.add(1) RankType.SEARCH -> status.add(1) } CacheDB.INSTANCE.genresDAO().insertStatus(status) } } } fun block(name: String) { doAsync { val status = getStatus(name) status.block() CacheDB.INSTANCE.genresDAO().insertStatus(status) syncData { genres() } } } fun reset(name: String) { doAsync { val status = getStatus(name) status.reset() CacheDB.INSTANCE.genresDAO().insertStatus(status) syncData { genres() } } } private fun getStatus(name: String): GenreStatusObject { var status: GenreStatusObject? = CacheDB.INSTANCE.genresDAO().getStatus(name) if (status == null) status = GenreStatusObject(name) return status } fun createRecommended(onCreate: (list: List) -> Unit) { doAsync { val status = CacheDB.INSTANCE.genresDAO().top if (status.size <= 2) { onCreate(emptyList()) return@doAsync } val excludeList = LinkedHashSet().apply { addAll(CacheDB.INSTANCE.favsDAO().allAids) addAll(CacheDB.INSTANCE.seeingDAO().allAids) }.toList() val abc = getList(status[0], status[1], status[2]) val ab = getList(status[0], status[1]) ab.removeAll(abc) val ac = getList(status[0], status[2]) ac.removeAll(abc, ab) val bc = getList(status[1], status[2]) bc.removeAll(abc, ab, ac) val a = getList(status[0]) a.removeAll(abc, ab, ac, bc) val b = getList(status[1]) b.removeAll(abc, ab, ac, bc, a) val c = getList(status[2]) c.removeAll(abc, ab, ac, bc, a, b) removeFavs(excludeList, abc, ab, ac, bc, a, b, c) val list = mutableListOf().apply { addAll(getAnimeList(abc)) addAll(getAnimeList(ab)) addAll(getAnimeList(ac)) addAll(getAnimeList(bc)) addAll(getAnimeList(a)) addAll(getAnimeList(b)) addAll(getAnimeList(c)) } onCreate(list) } } @SafeVarargs private fun removeFavs(excludeList: List, vararg lists: MutableList) { lists.forEach { list -> list.removeAll(list.filter { excludeList.contains(it) }) } } private fun getList(vararg status: GenreStatusObject): MutableList { return CacheDB.INSTANCE.animeDAO().getAidsByGenresLimited(getString(*status)) } private fun getAnimeList(list: List): MutableList { val chunk = list.chunked(900) val animes = mutableListOf() chunk.forEach { animes.addAll(CacheDB.INSTANCE.animeDAO().getAnimesByAids(it)) } return animes } private fun getString(vararg status: GenreStatusObject): String { val builder = StringBuilder("%") for (s in status) { builder.append(s.name) .append("%") } return builder.toString() } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/sections/MultipleSection.kt ================================================ package knf.kuma.recommended.sections import android.app.Activity import android.view.View import androidx.recyclerview.widget.RecyclerView import io.github.luizgrp.sectionedrecyclerviewadapter.SectionParameters import io.github.luizgrp.sectionedrecyclerviewadapter.StatelessSection import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.load import knf.kuma.recommended.AnimeShortObject import knf.kuma.recommended.RHHolder import knf.kuma.recommended.RIHolder /** * Created by jordy on 26/03/2018. */ class MultipleSection(private val activity: Activity, private val name: String, list: MutableList, isGrid: Boolean) : StatelessSection(SectionParameters.builder() .itemResourceId(if (isGrid) R.layout.item_fav_grid else R.layout.item_fav) .headerResourceId(R.layout.item_recommend_header) .build()) { private val animeObjects = list override fun getContentItemsTotal(): Int { return animeObjects.size } override fun getItemViewHolder(view: View): RecyclerView.ViewHolder { return RIHolder(view) } override fun onBindItemViewHolder(h: RecyclerView.ViewHolder, position: Int) { val holder = h as RIHolder val animeObject = animeObjects[position] holder.img.load(PatternUtil.getCover(animeObject.aid)) holder.title.text = animeObject.name holder.type.text = animeObject.type holder.cardView.setOnClickListener { ActivityAnime.open(activity, animeObject, holder.img, true, true) } } override fun getHeaderViewHolder(view: View): RecyclerView.ViewHolder { return RHHolder(view) } override fun onBindHeaderViewHolder(h: RecyclerView.ViewHolder?) { val holder = h as RHHolder? holder?.title?.text = name } } ================================================ FILE: app/src/main/java/knf/kuma/recommended/sections/MultipleSectionMaterial.kt ================================================ package knf.kuma.recommended.sections import android.app.Activity import android.view.View import androidx.recyclerview.widget.RecyclerView import io.github.luizgrp.sectionedrecyclerviewadapter.SectionParameters import io.github.luizgrp.sectionedrecyclerviewadapter.StatelessSection import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.load import knf.kuma.recommended.AnimeShortObject import knf.kuma.recommended.RHHolder import knf.kuma.recommended.RIHolder /** * Created by jordy on 26/03/2018. */ class MultipleSectionMaterial(private val activity: Activity, private val name: String, list: MutableList, isGrid: Boolean) : StatelessSection(SectionParameters.builder() .itemResourceId(if (isGrid) R.layout.item_fav_grid_material else R.layout.item_fav_material) .headerResourceId(R.layout.item_recommend_header) .build()) { private val animeObjects = list override fun getContentItemsTotal(): Int { return animeObjects.size } override fun getItemViewHolder(view: View): RecyclerView.ViewHolder { return RIHolder(view) } override fun onBindItemViewHolder(h: RecyclerView.ViewHolder, position: Int) { val holder = h as RIHolder val animeObject = animeObjects[position] holder.img.load(PatternUtil.getCover(animeObject.aid)) holder.title.text = animeObject.name holder.type.text = animeObject.type holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(activity, animeObject, holder.img, true, true) } } override fun getHeaderViewHolder(view: View): RecyclerView.ViewHolder { return RHHolder(view) } override fun onBindHeaderViewHolder(h: RecyclerView.ViewHolder?) { val holder = h as RHHolder? holder?.title?.text = name } } ================================================ FILE: app/src/main/java/knf/kuma/record/RecordActivity.kt ================================================ package knf.kuma.record import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.FrameLayout import android.widget.ProgressBar import androidx.annotation.LayoutRes import androidx.appcompat.widget.Toolbar import androidx.lifecycle.Observer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.ads.showRandomInterstitial import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.commons.showSnackbar import knf.kuma.commons.toast import knf.kuma.commons.verifyManager import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import org.jetbrains.anko.doAsync import org.jetbrains.anko.find class RecordActivity : GenericActivity() { val toolbar: Toolbar by bind(R.id.toolbar) val recyclerView: RecyclerView by bind(R.id.recycler) val progressBar: ProgressBar by bind(R.id.progress) val error: View by bind(R.id.error) private var adapter: RecordsAdapter? = null private var isFirst = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_records } else { R.layout.recycler_records_grid } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(layout) toolbar.title = "Historial" setSupportActionBar(toolbar) find(R.id.adContainer).implBanner(AdsType.RECORD_BANNER, true) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } adapter = RecordsAdapter(this) recyclerView.verifyManager() recyclerView.adapter = adapter val touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.RIGHT, ItemTouchHelper.RIGHT) { override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { adapter?.remove(viewHolder.adapterPosition) recyclerView.showSnackbar("Elemento eliminado") } }) touchHelper.attachToRecyclerView(recyclerView) CacheDB.INSTANCE.recordsDAO().allLive.observe(this, Observer { recordObjects -> adapter?.update(recordObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } else syncData { history() } if (recordObjects.isEmpty()) error.visibility = View.VISIBLE else error.visibility = View.GONE progressBar.visibility = View.GONE }) showRandomInterstitial(this,PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_records, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_clear -> MaterialDialog(this@RecordActivity).safeShow { message(text = "¿Limpiar el historial?") positiveButton(text = "Continuar") { doAsync { CacheDB.INSTANCE.recordsDAO().clear() } } negativeButton(text = "cancelar") } R.id.action_status -> doAsync { val count = CacheDB.INSTANCE.seenDAO().count doOnUI { "$count episodios vistos".toast() } } } return super.onOptionsItemSelected(item) } companion object { fun open(context: Context) { context.startActivity(Intent(context, RecordActivity::class.java)) AchievementManager.onRecordsOpened() } } } ================================================ FILE: app/src/main/java/knf/kuma/record/RecordActivityMaterial.kt ================================================ package knf.kuma.record import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.FrameLayout import android.widget.ProgressBar import androidx.annotation.LayoutRes import androidx.appcompat.widget.Toolbar import androidx.lifecycle.Observer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.ads.showRandomInterstitial import knf.kuma.backup.firestore.syncData import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUI import knf.kuma.commons.safeShow import knf.kuma.commons.setSurfaceBars import knf.kuma.commons.showSnackbar import knf.kuma.commons.toast import knf.kuma.commons.verifyManager import knf.kuma.custom.GenericActivity import knf.kuma.database.CacheDB import org.jetbrains.anko.doAsync import org.jetbrains.anko.find class RecordActivityMaterial : GenericActivity() { val toolbar: Toolbar by bind(R.id.toolbar) val recyclerView: RecyclerView by bind(R.id.recycler) val progressBar: ProgressBar by bind(R.id.progress) val error: View by bind(R.id.error) private var adapter: RecordsAdapterMaterial? = null private var isFirst = true private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.recycler_records_material } else { R.layout.recycler_records_grid_material } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(layout) toolbar.title = "Historial" setSupportActionBar(toolbar) find(R.id.adContainer).implBanner(AdsType.RECORD_BANNER, true) supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.setNavigationOnClickListener { finish() } adapter = RecordsAdapterMaterial(this) recyclerView.verifyManager() recyclerView.adapter = adapter val touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.RIGHT, ItemTouchHelper.RIGHT) { override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { adapter?.remove(viewHolder.adapterPosition) recyclerView.showSnackbar("Elemento eliminado") } }) touchHelper.attachToRecyclerView(recyclerView) CacheDB.INSTANCE.recordsDAO().allLive.observe(this, Observer { recordObjects -> adapter?.update(recordObjects) if (isFirst) { isFirst = false recyclerView.scheduleLayoutAnimation() } else syncData { history() } if (recordObjects.isEmpty()) error.visibility = View.VISIBLE else error.visibility = View.GONE progressBar.visibility = View.GONE }) showRandomInterstitial(this,PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_records, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_clear -> MaterialDialog(this@RecordActivityMaterial).safeShow { message(text = "¿Limpiar el historial?") positiveButton(text = "Continuar") { doAsync { CacheDB.INSTANCE.recordsDAO().clear() } } negativeButton(text = "cancelar") } R.id.action_status -> doAsync { val count = CacheDB.INSTANCE.seenDAO().count doOnUI { "$count episodios vistos".toast() } } } return super.onOptionsItemSelected(item) } companion object { fun open(context: Context) { context.startActivity(Intent(context, RecordActivityMaterial::class.java)) AchievementManager.onRecordsOpened() } } } ================================================ FILE: app/src/main/java/knf/kuma/record/RecordsAdapter.kt ================================================ package knf.kuma.record import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.commons.notSameContent import knf.kuma.database.CacheDB import knf.kuma.pojos.RecordObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import xdroid.toaster.Toaster class RecordsAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapter() { private var items: MutableList = ArrayList() private val dao = CacheDB.INSTANCE.recordsDAO() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_record } else { R.layout.item_record_grid } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecordItem { return RecordItem(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: RecordItem, position: Int) { val item = items[position] val animeObject = item.animeObject animeObject?.let { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) } holder.title.text = item.name holder.chapter.text = getCardText(item) holder.cardView.setOnClickListener { if (item.animeObject != null) ActivityAnime.open(activity, item, holder.imageView) else Toaster.toast("Error al abrir") } } private fun getCardText(recordObject: RecordObject): String { return if (!recordObject.chapter.startsWith("Episodio ")) "Episodio ${recordObject.chapter}" else recordObject.chapter } override fun getItemCount(): Int { return items.size } fun remove(position: Int) { activity.lifecycleScope.launch(Dispatchers.IO){ dao.delete(items[position]) items.removeAt(position) launch(Dispatchers.Main) { notifyItemRemoved(position) } } } fun update(items: MutableList) { if (this.items notSameContent items) { this.items = items notifyDataSetChanged() } } class RecordItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val chapter: TextView by itemView.bind(R.id.chapter) } } ================================================ FILE: app/src/main/java/knf/kuma/record/RecordsAdapterMaterial.kt ================================================ package knf.kuma.record import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.commons.notSameContent import knf.kuma.database.CacheDB import knf.kuma.pojos.RecordObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import xdroid.toaster.Toaster class RecordsAdapterMaterial(private val activity: AppCompatActivity) : RecyclerView.Adapter() { private var items: MutableList = ArrayList() private val dao = CacheDB.INSTANCE.recordsDAO() private val layout: Int @LayoutRes get() = if (PrefsUtil.layType == "0") { R.layout.item_record_material } else { R.layout.item_record_grid_material } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecordItem { return RecordItem(LayoutInflater.from(parent.context).inflate(layout, parent, false)) } override fun onBindViewHolder(holder: RecordItem, position: Int) { val item = items[position] val animeObject = item.animeObject animeObject?.let { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) } holder.title.text = item.name holder.chapter.text = getCardText(item) holder.cardView.setOnClickListener { if (item.animeObject != null) ActivityAnimeMaterial.open(activity, item, holder.imageView) else Toaster.toast("Error al abrir") } } private fun getCardText(recordObject: RecordObject): String { return if (!recordObject.chapter.startsWith("Episodio ")) "Episodio ${recordObject.chapter}" else recordObject.chapter } override fun getItemCount(): Int { return items.size } fun remove(position: Int) { activity.lifecycleScope.launch(Dispatchers.IO){ dao.delete(items[position]) items.removeAt(position) launch(Dispatchers.Main) { notifyItemRemoved(position) } } } fun update(items: MutableList) { if (this.items notSameContent items) { this.items = items notifyDataSetChanged() } } class RecordItem(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val title: TextView by itemView.bind(R.id.title) val chapter: TextView by itemView.bind(R.id.chapter) } } ================================================ FILE: app/src/main/java/knf/kuma/retrofit/Repository.kt ================================================ package knf.kuma.retrofit import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.paging.DataSource import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import knf.kuma.commons.Network import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.jsoupCookies import knf.kuma.commons.jsoupCookiesAdapter import knf.kuma.database.CacheDB import knf.kuma.directory.DirObject import knf.kuma.directory.DirObjectCompact import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.Recents import knf.kuma.recents.RecentsPage import knf.kuma.search.SearchCompactDataSource import knf.kuma.search.SearchObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.doAsync import pl.droidsonroids.jspoon.Jspoon import java.util.Locale import javax.inject.Singleton @Singleton class Repository { val search: Flow> get() = getSearch("") fun reloadAllRecents() { reloadRecents() reloadRecentsAlt() } fun reloadRecents() { if (Network.isConnected) { GlobalScope.launch(Dispatchers.IO) { try { val recents = Jspoon.create().adapter(Recents::class.java).fromHtml(jsoupCookies("https://www3.animeflv.net/").get().outerHtml()) val objects = RecentObject.create(recents.list) for ((i, recentObject) in objects.withIndex()) { recentObject.key = i recentObject.fileWrapper() } CacheDB.INSTANCE.recentsDAO().setCache(objects) } catch (e: Exception) { e.printStackTrace() } } } } fun reloadRecentsAlt() { if (Network.isConnected) { GlobalScope.launch(Dispatchers.IO) { try { val page = Jspoon.create().adapter(RecentsPage::class.java).fromHtml(jsoupCookies("https://animeflv.net/").get().outerHtml()) val list = page.list.apply { forEachIndexed { index, model -> model.key = index } } withContext(Dispatchers.IO) { CacheDB.INSTANCE.recentModelsDAO().setCache(list) } } catch (e: Exception) { e.printStackTrace() } } } } fun getAnime( link: String, persist: Boolean, data: MutableLiveData = MutableLiveData() ): LiveData { doAsync { var cacheUsed = false try { val dao = CacheDB.INSTANCE.animeDAO() val dbLink = "%/${link.substringAfterLast("/")}" dao.getAnimeRaw(dbLink)?.let { cacheUsed = true doOnUIGlobal { data.value = it } } if (Network.isConnected) { val animeObject = AnimeObject(link, jsoupCookiesAdapter(link, AnimeObject.WebInfo::class.java)) if (persist) dao.insert(animeObject) doOnUIGlobal { data.value = animeObject } } else if (!cacheUsed) doOnUIGlobal { data.value = null } } catch (e: Exception) { e.printStackTrace() if (!cacheUsed) doOnUIGlobal { data.value = null } } } return data } fun getAnimeDir(): Flow> { return Pager( PagingConfig(25), 0, when (PrefsUtil.dirOrder) { 1 -> CacheDB.INSTANCE.animeDAO().animeDirVotes 2 -> CacheDB.INSTANCE.animeDAO().animeDirID 3 -> CacheDB.INSTANCE.animeDAO().animeDirAdded 4 -> CacheDB.INSTANCE.animeDAO().animeDirFollowers else -> CacheDB.INSTANCE.animeDAO().animeDir }.asPagingSourceFactory(Dispatchers.IO) ).flow } fun getOvaDir(): Flow> { return Pager( PagingConfig(25), 0, when (PrefsUtil.dirOrder) { 1 -> CacheDB.INSTANCE.animeDAO().ovaDirVotes 2 -> CacheDB.INSTANCE.animeDAO().ovaDirID 3 -> CacheDB.INSTANCE.animeDAO().ovaDirAdded 4 -> CacheDB.INSTANCE.animeDAO().ovaDirFollowers else -> CacheDB.INSTANCE.animeDAO().ovaDir }.asPagingSourceFactory(Dispatchers.IO) ).flow } fun getMovieDir(): Flow> { return Pager( PagingConfig(25), 0, when (PrefsUtil.dirOrder) { 1 -> CacheDB.INSTANCE.animeDAO().movieDirVotes 2 -> CacheDB.INSTANCE.animeDAO().movieDirID 3 -> CacheDB.INSTANCE.animeDAO().movieDirAdded 4 -> CacheDB.INSTANCE.animeDAO().movieDirFollowers else -> CacheDB.INSTANCE.animeDAO().movieDir }.asPagingSourceFactory(Dispatchers.IO) ).flow } fun getSearch(query: String): Flow> { return Pager( PagingConfig(25), 0, when { query == "" -> CacheDB.INSTANCE.animeDAO().allSearch query.trim().matches("^#\\d+$".toRegex()) -> CacheDB.INSTANCE.animeDAO() .getSearchID(query.replace("#", "")) PatternUtil.isCustomSearch(query) -> getFiltered(query, null) else -> CacheDB.INSTANCE.animeDAO().getSearch("%$query%") }.asPagingSourceFactory(Dispatchers.IO) ).flow } private fun getFiltered(query: String, genres: String?): DataSource.Factory { var tQuery = PatternUtil.getCustomSearch(query).trim { it <= ' ' } var fQuery = tQuery fQuery = if (fQuery != "") "%$fQuery%" else "%" when (PatternUtil.getCustomAttr(query).lowercase(Locale.getDefault())) { "emision" -> return if (genres == null) CacheDB.INSTANCE.animeDAO().getSearchS(fQuery, "En emisión") else CacheDB.INSTANCE.animeDAO().getSearchSG(fQuery, "En emisión", genres) "finalizado" -> return if (genres == null) CacheDB.INSTANCE.animeDAO().getSearchS(fQuery, "Finalizado") else CacheDB.INSTANCE.animeDAO().getSearchSG(fQuery, "Finalizado", genres) "anime" -> return if (genres == null) CacheDB.INSTANCE.animeDAO().getSearchTY(fQuery, "Anime") else CacheDB.INSTANCE.animeDAO().getSearchTYG(fQuery, "Anime", genres) "ova" -> return if (genres == null) CacheDB.INSTANCE.animeDAO().getSearchTY(fQuery, "OVA") else CacheDB.INSTANCE.animeDAO().getSearchTYG(fQuery, "OVA", genres) "pelicula" -> return if (genres == null) CacheDB.INSTANCE.animeDAO().getSearchTY(fQuery, "Película") else CacheDB.INSTANCE.animeDAO().getSearchTYG(fQuery, "Película", genres) "personalizado" -> { if (tQuery == "") tQuery = "%" return if (genres == null) CacheDB.INSTANCE.animeDAO().getSearch(tQuery) else CacheDB.INSTANCE.animeDAO().getSearchTG(tQuery, genres) } else -> return if (genres == null) CacheDB.INSTANCE.animeDAO().getSearch(fQuery) else CacheDB.INSTANCE.animeDAO().getSearchTG(fQuery, genres) } } fun getSearch(query: String, genres: String): Flow> { return Pager( PagingConfig(25), 0, when { query == "" -> CacheDB.INSTANCE.animeDAO().getSearchG(genres) PatternUtil.isCustomSearch(query) -> getFiltered(query, genres) else -> CacheDB.INSTANCE.animeDAO().getSearchTG("%$query%", genres) }.asPagingSourceFactory(Dispatchers.IO) ).flow } fun getSearchCompact( query: String, onInit: (isEmpty: Boolean) -> Unit ): Flow> { return Pager( config = PagingConfig(24), pagingSourceFactory = { SearchCompactDataSource( query, onInit ) } ).flow } } ================================================ FILE: app/src/main/java/knf/kuma/search/FiltersSuggestion.kt ================================================ package knf.kuma.search import android.content.Context import knf.kuma.R import org.cryse.widget.persistentsearch.SearchItem import org.cryse.widget.persistentsearch.SearchSuggestionsBuilder class FiltersSuggestion(private val context: Context) : SearchSuggestionsBuilder { override fun buildEmptySearchSuggestion(maxCount: Int): Collection { return ArrayList() } override fun buildSearchSuggestion(maxCount: Int, query: String): Collection { val items = ArrayList() if (query == ":") { val drawable = context.getDrawable(R.drawable.ic_hash) items.add(SearchItem("En emisión", ":emision:", SearchItem.TYPE_SEARCH_ITEM_CUSTOM, drawable)) items.add(SearchItem("Finalizados", ":finalizado:", SearchItem.TYPE_SEARCH_ITEM_CUSTOM, drawable)) items.add(SearchItem("Animes", ":anime:", SearchItem.TYPE_SEARCH_ITEM_CUSTOM, drawable)) items.add(SearchItem("Ovas", ":ova:", SearchItem.TYPE_SEARCH_ITEM_CUSTOM, drawable)) items.add(SearchItem("Películas", ":pelicula:", SearchItem.TYPE_SEARCH_ITEM_CUSTOM, drawable)) items.add(SearchItem("Personalizado", ":personalizado:", SearchItem.TYPE_SEARCH_ITEM_CUSTOM, drawable)) } return items } } ================================================ FILE: app/src/main/java/knf/kuma/search/GenreActivity.kt ================================================ package knf.kuma.search import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import android.view.animation.AnimationUtils import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig import knf.kuma.R import knf.kuma.commons.EAHelper import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerGenreBinding import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class GenreActivity : GenericActivity() { private var adapter: GenreAdapter? = null private var isFirst = true private val binding by lazy { RecyclerGenreBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(binding.root) binding.toolbar.title = intent.getStringExtra("name") setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { finish() } binding.recycler.layoutManager = VariantLinearLayoutManager(this) binding.recycler.layoutAnimation = AnimationUtils.loadLayoutAnimation(this, R.anim.layout_fall_down) adapter = GenreAdapter(this) binding.recycler.adapter = adapter lifecycleScope.launch { Pager( config = PagingConfig(25), 0, CacheDB.INSTANCE.animeDAO().getAllGenre("%" + intent.getStringExtra("name") + "%").asPagingSourceFactory() ).flow.collectLatest { adapter?.submitData(it) binding.progress.visibility = View.GONE if (isFirst) { isFirst = false binding.recycler.scheduleLayoutAnimation() } } } } companion object { fun open(context: Context, name: String) { val intent = Intent(context, GenreActivity::class.java) intent.putExtra("name", name) context.startActivity(intent) } } } ================================================ FILE: app/src/main/java/knf/kuma/search/GenreActivityMaterial.kt ================================================ package knf.kuma.search import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import android.view.animation.AnimationUtils import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig import knf.kuma.R import knf.kuma.commons.EAHelper import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.custom.VariantLinearLayoutManager import knf.kuma.database.CacheDB import knf.kuma.databinding.RecyclerGenreMaterialBinding import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class GenreActivityMaterial : GenericActivity() { private val adapter: GenreAdapterMaterial by lazy { GenreAdapterMaterial(this) } private var isFirst = true private val binding by lazy { RecyclerGenreMaterialBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) binding.toolbar.title = intent.getStringExtra("name") setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) binding.toolbar.setNavigationOnClickListener { finish() } binding.recycler.layoutManager = VariantLinearLayoutManager(this) binding.recycler.layoutAnimation = AnimationUtils.loadLayoutAnimation(this, R.anim.layout_fall_down) binding.recycler.adapter = adapter lifecycleScope.launch { Pager( config = PagingConfig(25), 0, CacheDB.INSTANCE.animeDAO().getAllGenre("%" + intent.getStringExtra("name") + "%").asPagingSourceFactory() ).flow.collectLatest { adapter.submitData(it) binding.progress.visibility = View.GONE if (isFirst) { isFirst = false binding.recycler.scheduleLayoutAnimation() } } } } companion object { fun open(context: Context, name: String) { val intent = Intent(context, GenreActivityMaterial::class.java) intent.putExtra("name", name) context.startActivity(intent) } } } ================================================ FILE: app/src/main/java/knf/kuma/search/GenreAdapter.kt ================================================ package knf.kuma.search import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.load import org.jetbrains.anko.find internal class GenreAdapter(private val activity: Activity) : PagingDataAdapter(DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_dir, parent, false)) } override fun onBindViewHolder(holder: ItemHolder, position: Int) { val animeObject = getItem(position) animeObject?.let { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnime.open(activity, animeObject, holder.imageView, false, true) } } } internal class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView = itemView.find(R.id.card) val imageView: ImageView = itemView.find(R.id.img) val textView: TextView = itemView.find(R.id.title) } companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.key == newItem.key } override fun areContentsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.name == newItem.name } } } } ================================================ FILE: app/src/main/java/knf/kuma/search/GenreAdapterMaterial.kt ================================================ package knf.kuma.search import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.load import org.jetbrains.anko.find internal class GenreAdapterMaterial(private val activity: Activity) : PagingDataAdapter(DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_dir_material, parent, false)) } override fun onBindViewHolder(holder: ItemHolder, position: Int) { val animeObject = getItem(position) animeObject?.let { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(activity, animeObject, holder.imageView, false, true) } } } internal class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View = itemView.find(R.id.card) val imageView: ImageView = itemView.find(R.id.img) val textView: TextView = itemView.find(R.id.title) } companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.key == newItem.key } override fun areContentsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.name == newItem.name } } } } ================================================ FILE: app/src/main/java/knf/kuma/search/GenresDialog.kt ================================================ package knf.kuma.search import android.app.Dialog import android.os.Bundle import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import com.afollestad.materialdialogs.list.listItemsMultiChoice import knf.kuma.commons.transform class GenresDialog : DialogFragment() { private var genres: MutableList = ArrayList() private var selected: MutableList = ArrayList() private var listener: MultiChoiceListener? = null private val states: BooleanArray get() { val states = BooleanArray(genres.size) var index = 0 for (genre in genres) { states[index++] = selected.contains(genre) } return states } private val selectedStates: IntArray get() { val states = IntArray(selected.size) for ((index, item) in selected.withIndex()) states[index] = genres.indexOf(item) return states } fun init(genres: MutableList, selected: MutableList, listener: MultiChoiceListener) { this.genres = genres this.selected = selected this.listener = listener } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return activity?.let { MaterialDialog(it).apply { lifecycleOwner() title(text = "Géneros") listItemsMultiChoice(items = genres, initialSelection = selectedStates, allowEmptySelection = true) { _: MaterialDialog, _: IntArray, items: List -> selected.apply { clear() addAll(items.transform()) sort() } listener?.onOkay(selected) } positiveButton(text = "BUSCAR") negativeButton(text = "CERRAR") } /*AlertDialog.Builder(it) .setTitle("Generos") .setMultiChoiceItems(genres.toTypedArray(), states) { _, index, isSelected -> if (isSelected) selected.add(genres[index]) else selected.remove(genres[index]) }.setPositiveButton("BUSCAR") { _, _ -> selected.sort() listener?.onOkay(selected) }.setNegativeButton("CERRAR") { dialogInterface, _ -> dialogInterface.dismiss() } .create()*/ } ?: super.onCreateDialog(savedInstanceState) } override fun show(manager: FragmentManager, tag: String?) { try { super.show(manager, tag) } catch (e: Exception) { // } } override fun dismiss() { try { super.dismiss() } catch (e: Exception) { // } } interface MultiChoiceListener { fun onOkay(selected: MutableList) } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchActivity.kt ================================================ package knf.kuma.search import androidx.activity.addCallback import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.core.app.ActivityOptionsCompat import androidx.core.widget.addTextChangedListener import knf.kuma.R import knf.kuma.achievements.AchievementManager import knf.kuma.commons.EAHelper import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivitySearchBinding class SearchActivity : GenericActivity() { private val model by viewModels() private val binding by lazy { ActivitySearchBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.etSearch.setText(model.queryListener.value) binding.etSearch.requestFocus() binding.etSearch.addTextChangedListener(afterTextChanged = { EAHelper.checkStart(it.toString()) AchievementManager.onSearch(it.toString()) model.sendQuery(it?.toString()) }) onBackPressedDispatcher.addCallback(this) { finish() overridePendingTransition(R.anim.fade_in, R.anim.fade_out) } } override fun onSupportNavigateUp(): Boolean { onBackPressedDispatcher.onBackPressed() return super.onSupportNavigateUp() } companion object { fun open(context: Context) { context.startActivity(Intent(context, SearchActivity::class.java), ActivityOptionsCompat.makeCustomAnimation(context, R.anim.fade_in, R.anim.fade_out).toBundle()) } } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchAdapter.kt ================================================ package knf.kuma.search import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.fragment.app.Fragment import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load class SearchAdapter internal constructor(private val fragment: Fragment) : PagingDataAdapter(DIFF_CALLBACK) { private val layType = PrefsUtil.layType override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder( LayoutInflater.from(parent.context).inflate( if (layType == "0") R.layout.item_dir else R.layout.item_dir_grid, parent, false ) ) } override fun onBindViewHolder(holder: ItemHolder, position: Int) { if (fragment.context == null) return val animeObject = getItem(position) if (animeObject != null) { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.progressView.visibility = View.GONE holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnime.open(fragment, animeObject, holder.imageView, false, true) } } else { holder.progressView.visibility = View.VISIBLE holder.textView.text = null } } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar by itemView.bind(R.id.progress) val textView: TextView by itemView.bind(R.id.title) } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.key == newItem.key } override fun areContentsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.name == newItem.name && oldItem.aid == newItem.aid } } } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchAdapterCompact.kt ================================================ package knf.kuma.search import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.fragment.app.Fragment import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.directory.DirObjectCompact class SearchAdapterCompact internal constructor(private val fragment: Fragment) : PagingDataAdapter(DIFF_CALLBACK) { private val layType = PrefsUtil.layType override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder( LayoutInflater.from(parent.context).inflate( if (layType == "0") R.layout.item_dir else R.layout.item_dir_grid, parent, false ) ) } override fun onBindViewHolder(holder: ItemHolder, position: Int) { if (fragment.context == null) return val animeObject = getItem(position) if (animeObject != null) { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.progressView.visibility = View.GONE holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnime.open(fragment, animeObject, holder.imageView, persist = true, animate = true) } } else { holder.progressView.visibility = View.VISIBLE holder.textView.text = null } } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar by itemView.bind(R.id.progress) val textView: TextView by itemView.bind(R.id.title) } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DirObjectCompact, newItem: DirObjectCompact): Boolean { return oldItem.aid == newItem.aid } override fun areContentsTheSame(oldItem: DirObjectCompact, newItem: DirObjectCompact): Boolean { return oldItem.name == newItem.name && oldItem.aid == newItem.aid } } } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchAdapterCompactMaterial.kt ================================================ package knf.kuma.search import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.fragment.app.Fragment import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load import knf.kuma.directory.DirObjectCompact class SearchAdapterCompactMaterial internal constructor(private val fragment: Fragment) : PagingDataAdapter(DIFF_CALLBACK) { private val layType = PrefsUtil.layType override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder( LayoutInflater.from(parent.context).inflate( if (layType == "0") R.layout.item_dir_material else R.layout.item_dir_grid_material, parent, false ) ) } override fun onBindViewHolder(holder: ItemHolder, position: Int) { if (fragment.context == null) return val animeObject = getItem(position) if (animeObject != null) { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.progressView.visibility = View.GONE holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, animeObject, holder.imageView, persist = true, animate = true) } } else { holder.progressView.visibility = View.VISIBLE holder.textView.text = null } } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar by itemView.bind(R.id.progress) val textView: TextView by itemView.bind(R.id.title) } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DirObjectCompact, newItem: DirObjectCompact): Boolean { return oldItem.aid == newItem.aid } override fun areContentsTheSame(oldItem: DirObjectCompact, newItem: DirObjectCompact): Boolean { return oldItem.name == newItem.name && oldItem.aid == newItem.aid } } } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchAdapterMaterial.kt ================================================ package knf.kuma.search import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.fragment.app.Fragment import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.load class SearchAdapterMaterial internal constructor(private val fragment: Fragment) : PagingDataAdapter(DIFF_CALLBACK) { private val layType = PrefsUtil.layType override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { return ItemHolder( LayoutInflater.from(parent.context).inflate( if (layType == "0") R.layout.item_dir_material else R.layout.item_dir_grid_material, parent, false ) ) } override fun onBindViewHolder(holder: ItemHolder, position: Int) { if (fragment.context == null) return val animeObject = getItem(position) if (animeObject != null) { holder.imageView.load(PatternUtil.getCover(animeObject.aid)) holder.progressView.visibility = View.GONE holder.textView.text = animeObject.name holder.cardView.setOnClickListener { ActivityAnimeMaterial.open(fragment, animeObject, holder.imageView, false, true) } } else { holder.progressView.visibility = View.VISIBLE holder.textView.text = null } } class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar by itemView.bind(R.id.progress) val textView: TextView by itemView.bind(R.id.title) } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.key == newItem.key } override fun areContentsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean { return oldItem.name == newItem.name && oldItem.aid == newItem.aid } } } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchAdvObject.kt ================================================ package knf.kuma.search import com.google.gson.annotations.SerializedName class SearchAdvObject : SearchObject() { @SerializedName("adv_img") var img = "" @SerializedName("adv_type") var type = "" override fun equals(other: Any?): Boolean { return other is SearchAdvObject && key == other.key && aid == other.aid && name == other.name && link == other.link && img == other.img && type == other.type } override fun hashCode(): Int { return "$key$aid$name$link$img$type".hashCode() } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchCompactDataSource.kt ================================================ package knf.kuma.search import androidx.paging.PagingSource import androidx.paging.PagingState import knf.kuma.commons.jsoupCookiesAdapter import knf.kuma.directory.DirObjectCompact import knf.kuma.directory.DirectoryPageCompact import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.URLEncoder class SearchCompactDataSource( val query: String, val onInit: (isEmpty: Boolean) -> Unit ) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? = state.anchorPosition override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 1 try { val dir = withContext(Dispatchers.IO) { jsoupCookiesAdapter("https://www3.animeflv.net/browse?order=title&q=${URLEncoder.encode(query, "utf-8")}&page=$page", DirectoryPageCompact::class.java) } if (page == 1) onInit(dir.list.isEmpty()) return LoadResult.Page(dir.list, null, if (dir.hasNext) page + 1 else null) }catch (e:Exception){ e.printStackTrace() if (page == 1) onInit(true) return LoadResult.Page(emptyList(), null, null) } } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchFragment.kt ================================================ package knf.kuma.search import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.annotation.DrawableRes import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.snackbar.Snackbar import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.isFullMode import knf.kuma.commons.noCrash import knf.kuma.commons.showSnackbar import knf.kuma.commons.verifyManager import knf.kuma.recommended.RankType import knf.kuma.recommended.RecommendHelper import knf.kuma.retrofit.Repository import kotlinx.coroutines.launch import org.jetbrains.anko.find class SearchFragment : BottomFragment() { lateinit var recyclerView: RecyclerView lateinit var fab: ExtendedFloatingActionButton lateinit var progressBar: ProgressBar private lateinit var errorView: View private val model: SearchViewModel by activityViewModels() private var searchAdapter: SearchAdapter? = null private var searchAdapterCompact: SearchAdapterCompact? = null private var manager: RecyclerView.LayoutManager? = null private var isFirst = true private var waitingScroll = false private var query: String = "" private var selected: MutableList = ArrayList() private val needOnlineSearch: Boolean by lazy { !PrefsUtil.isDirectoryFinished && Network.isConnected && isFullMode && !PrefsUtil.isFamilyFriendly } private val genresString: String get() { return if (selected.size == 0) { "" } else { RecommendHelper.registerAll(selected, RankType.SEARCH) val builder = StringBuilder("%") for (genre in selected) { builder.append(genre) .append("%") } builder.toString() } } private val fabIcon: Int @DrawableRes get() { return when (selected.size) { 0 -> R.drawable.ic_genres_0 1 -> R.drawable.ic_genres_1 2 -> R.drawable.ic_genres_2 3 -> R.drawable.ic_genres_3 4 -> R.drawable.ic_genres_4 5 -> R.drawable.ic_genres_5 6 -> R.drawable.ic_genres_6 7 -> R.drawable.ic_genres_7 8 -> R.drawable.ic_genres_8 9 -> R.drawable.ic_genres_9 else -> R.drawable.ic_genres_more } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) noCrash { if (!needOnlineSearch) model.setSearch(query, "") { animeObjects -> searchAdapter?.submitData(animeObjects) if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } } searchAdapter?.addLoadStateListener { if (it.append.endOfPaginationReached) { errorView.visibility = if (searchAdapter?.itemCount == 0) View.VISIBLE else View.GONE } } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate( if (PrefsUtil.layType == "0") R.layout.fragment_search else R.layout.fragment_search_grid, container, false) recyclerView = view.find(R.id.recycler) fab = view.find(R.id.fab) progressBar = view.find(R.id.progress) errorView = view.find(R.id.error) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> fab.updateLayoutParams { bottomMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom * 2 } WindowInsetsCompat.CONSUMED } recyclerView.verifyManager() recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (dy > 0) fab.shrink() else if (dy < 0) fab.extend() } }) manager = recyclerView.layoutManager if (needOnlineSearch) { searchAdapterCompact = SearchAdapterCompact(this) lifecycleScope.launch { Repository().getSearchCompact("") { if (it) { errorView.visibility = if (it) View.VISIBLE else View.GONE } if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } }.collect { searchAdapterCompact?.submitData(it) } } searchAdapterCompact?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { super.onItemRangeMoved(fromPosition, toPosition, itemCount) if (toPosition == 0 && waitingScroll) { manager?.smoothScrollToPosition(recyclerView, null, 0) waitingScroll = false } } }) recyclerView.adapter = searchAdapterCompact } else { searchAdapter = SearchAdapter(this) searchAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { super.onItemRangeMoved(fromPosition, toPosition, itemCount) if (toPosition == 0 && waitingScroll) { manager?.smoothScrollToPosition(recyclerView, null, 0) fab.extend() waitingScroll = false } } override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) if (positionStart == 0 && waitingScroll) { manager?.smoothScrollToPosition(recyclerView, null, 0) fab.extend() waitingScroll = false } } }) recyclerView.adapter = searchAdapter } fab.setOnClickListener { if (needOnlineSearch){ recyclerView.showSnackbar("Se necesita el directorio completo para busquedas por genero", Snackbar.LENGTH_LONG, Snackbar.ANIMATION_MODE_SLIDE) }else{ val dialog = GenresDialog() dialog.init(genres, selected, object : GenresDialog.MultiChoiceListener { override fun onOkay(selected: MutableList) { this@SearchFragment.selected = selected setFabIcon() setSearchNormal(query) } }) dialog.show(childFragmentManager, "genres") } } } fun setSearch(q: String) = if (needOnlineSearch) setSearchCompact(q) else setSearchNormal(q) private fun setSearchCompact(q: String) { waitingScroll = true this.query = q.trim() lifecycleScope.launch { Repository().getSearchCompact(q) { errorView.visibility = if (it) View.VISIBLE else View.GONE if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } }.collect { searchAdapterCompact?.submitData(it) } } } private fun setSearchNormal(q: String) { waitingScroll = true this.query = q.trim() model.setSearch(q.trim(), genresString) { animeObjects -> searchAdapter?.submitData(animeObjects) //errorView.visibility = if (animeObjects.isEmpty()) View.VISIBLE else View.GONE if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } } } private fun setFabIcon() { fab.post { fab.setIconResource(fabIcon) } } override fun onReselect() { } companion object { @JvmOverloads operator fun get(query: String = ""): SearchFragment { val fragment = SearchFragment() fragment.query = query return fragment } val genres: MutableList get() = mutableListOf( "Acción", "Artes Marciales", "Aventuras", "Carreras", "Ciencia Ficción", "Comedia", "Demencia", "Demonios", "Deportes", "Drama", "Ecchi", "Escolares", "Espacial", "Fantasía", "Harem", "Historico", "Infantil", "Josei", "Juegos", "Magia", "Mecha", "Militar", "Misterio", "Música", "Parodia", "Policía", "Psicológico", "Recuentos de la vida", "Romance", "Samurai", "Seinen", "Shoujo", "Shounen", "Sin Generos", "Sobrenatural", "Superpoderes", "Suspenso", "Terror", "Vampiros", "Yaoi", "Yuri") } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchFragmentMaterial.kt ================================================ package knf.kuma.search import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.annotation.DrawableRes import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.recyclerview.widget.RecyclerView import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.snackbar.Snackbar import knf.kuma.BottomFragment import knf.kuma.R import knf.kuma.commons.Network import knf.kuma.commons.PrefsUtil import knf.kuma.commons.isFullMode import knf.kuma.commons.noCrash import knf.kuma.commons.showSnackbar import knf.kuma.commons.verifyManager import knf.kuma.recommended.RankType import knf.kuma.recommended.RecommendHelper import knf.kuma.retrofit.Repository import kotlinx.coroutines.launch import org.jetbrains.anko.find class SearchFragmentMaterial : BottomFragment() { lateinit var recyclerView: RecyclerView lateinit var fab: ExtendedFloatingActionButton lateinit var progressBar: ProgressBar private lateinit var errorView: View private val model: SearchViewModel by activityViewModels() private var searchAdapter: SearchAdapterMaterial? = null private var searchAdapterCompact: SearchAdapterCompactMaterial? = null private var manager: RecyclerView.LayoutManager? = null private var isFirst = true private var waitingScroll = false private var query: String = "" private var selected: MutableList = ArrayList() private val needOnlineSearch: Boolean by lazy { !PrefsUtil.isDirectoryFinished && Network.isConnected && isFullMode && !PrefsUtil.isFamilyFriendly } private val genresString: String get() { return if (selected.size == 0) { "" } else { RecommendHelper.registerAll(selected, RankType.SEARCH) val builder = StringBuilder("%") for (genre in selected) { builder.append(genre) .append("%") } builder.toString() } } private val fabIcon: Int @DrawableRes get() { return when (selected.size) { 0 -> R.drawable.ic_genres_0 1 -> R.drawable.ic_genres_1 2 -> R.drawable.ic_genres_2 3 -> R.drawable.ic_genres_3 4 -> R.drawable.ic_genres_4 5 -> R.drawable.ic_genres_5 6 -> R.drawable.ic_genres_6 7 -> R.drawable.ic_genres_7 8 -> R.drawable.ic_genres_8 9 -> R.drawable.ic_genres_9 else -> R.drawable.ic_genres_more } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) noCrash { if (!needOnlineSearch) model.setSearch(query, "") { animeObjects -> searchAdapter?.submitData(animeObjects) if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } } model.queryListener.observe(viewLifecycleOwner, { setSearch(it?.trim() ?: "") }) searchAdapter?.addLoadStateListener { if (it.append != LoadState.Loading) { progressBar.visibility = View.GONE } if (it.append.endOfPaginationReached) { errorView.visibility = if (searchAdapter?.itemCount == 0) View.VISIBLE else View.GONE } } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate( if (PrefsUtil.layType == "0") R.layout.fragment_search else R.layout.fragment_search_grid, container, false) recyclerView = view.find(R.id.recycler) fab = view.find(R.id.fab) progressBar = view.find(R.id.progress) errorView = view.find(R.id.error) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> fab.updateLayoutParams { bottomMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom * 2 } WindowInsetsCompat.CONSUMED } recyclerView.verifyManager() recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (dy > 0) fab.shrink() else if (dy < 0) fab.extend() } }) manager = recyclerView.layoutManager if (needOnlineSearch) { searchAdapterCompact = SearchAdapterCompactMaterial(this) lifecycleScope.launch { Repository().getSearchCompact("") { if (it) { errorView.visibility = if (it) View.VISIBLE else View.GONE } if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } }.collect { searchAdapterCompact?.submitData(it) } } searchAdapterCompact?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { super.onItemRangeMoved(fromPosition, toPosition, itemCount) if (toPosition == 0 && waitingScroll) { manager?.smoothScrollToPosition(recyclerView, null, 0) waitingScroll = false } } }) recyclerView.adapter = searchAdapterCompact } else { searchAdapter = SearchAdapterMaterial(this) searchAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { super.onItemRangeMoved(fromPosition, toPosition, itemCount) if (toPosition == 0 && waitingScroll) { manager?.smoothScrollToPosition(recyclerView, null, 0) fab.extend() waitingScroll = false } } override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) if (positionStart == 0 && waitingScroll) { manager?.smoothScrollToPosition(recyclerView, null, 0) fab.extend() waitingScroll = false } } }) recyclerView.adapter = searchAdapter } fab.setOnClickListener { if (needOnlineSearch){ recyclerView.showSnackbar("Se necesita el directorio completo para busquedas por genero", Snackbar.LENGTH_LONG,Snackbar.ANIMATION_MODE_SLIDE) }else{ val dialog = GenresDialog() dialog.init(SearchFragment.genres, selected, object : GenresDialog.MultiChoiceListener { override fun onOkay(selected: MutableList) { this@SearchFragmentMaterial.selected = selected setFabIcon() setSearchNormal(query) } }) dialog.show(childFragmentManager, "genres") } } } private fun setSearch(q: String) = if (needOnlineSearch) setSearchCompact(q) else setSearchNormal(q) private fun setSearchCompact(q: String) { waitingScroll = true this.query = q.trim() lifecycleScope.launch { Repository().getSearchCompact(q) { errorView.visibility = if (it) View.VISIBLE else View.GONE if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } }.collect { searchAdapterCompact?.submitData(it) } } } private fun setSearchNormal(q: String) { Log.e("Search", "On search: ") waitingScroll = true this.query = q.trim() model.setSearch(q.trim(), genresString) { animeObjects -> searchAdapter?.submitData(animeObjects) //errorView.visibility = if (animeObjects.isEmpty()) View.VISIBLE else View.GONE if (isFirst) { progressBar.visibility = View.GONE isFirst = false recyclerView.scheduleLayoutAnimation() } } } private fun setFabIcon() { fab.post { fab.setIconResource(fabIcon) } } override fun onReselect() { } companion object { @JvmOverloads operator fun get(query: String = ""): SearchFragmentMaterial { val fragment = SearchFragmentMaterial() fragment.query = query return fragment } val genres: MutableList get() = mutableListOf( "Acción", "Artes Marciales", "Aventuras", "Carreras", "Ciencia Ficción", "Comedia", "Demencia", "Demonios", "Deportes", "Drama", "Ecchi", "Escolares", "Espacial", "Fantasía", "Harem", "Historico", "Infantil", "Josei", "Juegos", "Magia", "Mecha", "Militar", "Misterio", "Música", "Parodia", "Policía", "Psicológico", "Recuentos de la vida", "Romance", "Samurai", "Seinen", "Shoujo", "Shounen", "Sin Generos", "Sobrenatural", "Superpoderes", "Suspenso", "Terror", "Vampiros", "Yaoi", "Yuri") } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchObject.kt ================================================ package knf.kuma.search import androidx.recyclerview.widget.DiffUtil import com.google.gson.annotations.SerializedName open class SearchObject { @SerializedName("adv_key") var key = 0 @SerializedName("adv_aid") var aid = "" @SerializedName("adv_name") var name = "" @SerializedName("adv_link") var link = "" override fun equals(other: Any?): Boolean { return other is SearchObject && key == other.key && aid == other.aid && name == other.name && link == other.link } override fun hashCode(): Int { return "$key$aid$name$link".hashCode() } companion object{ val DIFF = object : DiffUtil.ItemCallback(){ override fun areItemsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean = oldItem.key == newItem.key override fun areContentsTheSame(oldItem: SearchObject, newItem: SearchObject): Boolean = oldItem == newItem } } } ================================================ FILE: app/src/main/java/knf/kuma/search/SearchObjectFav.kt ================================================ package knf.kuma.search import androidx.recyclerview.widget.DiffUtil import knf.kuma.database.CacheDB open class SearchObjectFav(val key: Int, val aid: String, val name: String, val link: String) { var isFav = CacheDB.INSTANCE.favsDAO().isFav(aid.toInt()) override fun equals(other: Any?): Boolean { return other is SearchObjectFav && key == other.key && aid == other.aid && name == other.name && link == other.link } override fun hashCode(): Int { return "$key$aid$name$link".hashCode() } companion object { val DIFF = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SearchObjectFav, newItem: SearchObjectFav): Boolean = oldItem.key == newItem.key override fun areContentsTheSame(oldItem: SearchObjectFav, newItem: SearchObjectFav): Boolean = oldItem == newItem } } } fun SearchObject.forFav(): SearchObjectFav = SearchObjectFav(key, aid, name, link) ================================================ FILE: app/src/main/java/knf/kuma/search/SearchViewModel.kt ================================================ package knf.kuma.search import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import knf.kuma.retrofit.Repository import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class SearchViewModel : ViewModel() { private val repository = Repository() private var searchJob: Job? = null private var queryLive = MutableLiveData(null) fun sendQuery(query: String?) { queryLive.value = query } val queryListener: LiveData get() = queryLive fun setSearch( query: String, genres: String, callback: suspend (PagingData) -> Unit ) { searchJob?.cancel() searchJob = viewModelScope.launch { (if (query == "" && genres == "") repository.search else if (genres == "") repository.getSearch(query) else repository.getSearch(query, genres)).collectLatest { callback(it) } } } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/FavToSeeing.kt ================================================ package knf.kuma.seeing import android.content.Context import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.WhichButton import com.afollestad.materialdialogs.actions.getActionButton import com.afollestad.materialdialogs.checkbox.checkBoxPrompt import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked import com.afollestad.materialdialogs.lifecycle.lifecycleOwner import knf.kuma.backup.firestore.syncData import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.noCrash import knf.kuma.commons.noCrashLet import knf.kuma.commons.safeShow import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.SeeingObject import knf.kuma.pojos.SeenObject import org.jetbrains.anko.doAsync object FavToSeeing { fun onConfirmation(context: Context) { MaterialDialog(context).safeShow { title(text = "Convertir favoritos") message(text = "Se marcarán todos los animes FINALIZADOS en favoritos como COMPLETADOS, continuar?") checkBoxPrompt(text = "Marcar todos los episodios como vistos") {} positiveButton(text = "Continuar") { start(context, it.isCheckPromptChecked()) } } } private fun start(context: Context, withChapters: Boolean) { doAsync { noCrash { val favList = CacheDB.INSTANCE.favsDAO().allRaw var count = 0 doOnUIGlobal { val dialog = MaterialDialog(context).apply { lifecycleOwner() message(text = "Procesando favoritos... ($count/${favList.size})") cancelable(false) positiveButton(text = "Aceptar") { it.dismiss() } } dialog.getActionButton(WhichButton.POSITIVE).isEnabled = false dialog.safeShow() doAsync { var needSeeingUpdate = false var needSeenUpdate = false favList.forEach { favoriteObject -> if (favoriteObject.isCompleted) { if (!favoriteObject.isSeeing) { CacheDB.INSTANCE.seeingDAO().add(SeeingObject.fromAnime(favoriteObject).apply { state = SeeingObject.STATE_COMPLETED }) needSeeingUpdate = true } if (withChapters) { CacheDB.INSTANCE.seenDAO().addAll(favoriteObject.chapters.map { SeenObject.fromChapter(it) }) needSeenUpdate = true } } count++ doOnUIGlobal { dialog.message(text = "Procesando favoritos... ($count/${favList.size})") } } syncData { if (needSeeingUpdate) seeing() if (needSeenUpdate) seen() } doOnUIGlobal { dialog.getActionButton(WhichButton.POSITIVE).isEnabled = true } } } } } } fun getLast(list: List): SeenObject? = list.maxByOrNull { noCrashLet(-1.0) { "(\\d+\\.?\\d?)".toRegex().findAll(it.number).last().destructured.component1() .toDouble() } } private val FavoriteObject.isCompleted: Boolean get() = CacheDB.INSTANCE.animeDAO().isCompleted(aid) private val FavoriteObject.isSeeing: Boolean get() = CacheDB.INSTANCE.seeingDAO().isSeeingAll(aid) private val FavoriteObject.chapters: List get() = CacheDB.INSTANCE.animeDAO().getFullByAid(aid)?.chapters ?: listOf() } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingActivity.kt ================================================ package knf.kuma.seeing import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.widget.Toolbar import androidx.viewpager.widget.ViewPager import com.google.android.material.tabs.TabLayout import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.custom.GenericActivity class SeeingActivity : GenericActivity() { val toolbar: Toolbar by bind(R.id.toolbar) val tabs: TabLayout by bind(R.id.tabs) val pager: ViewPager by bind(R.id.pager) override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setContentView(R.layout.activity_seening) toolbar.title = "Siguiendo" setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) toolbar.setNavigationOnClickListener { finish() } pager.adapter = SeeingPagerAdapter(supportFragmentManager) pager.offscreenPageLimit = 5 tabs.setupWithViewPager(pager) tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) { ((pager.adapter as? SeeingPagerAdapter)?.fragmentList)?.let { it[pager.currentItem].onSelected() } } override fun onTabUnselected(p0: TabLayout.Tab?) { } override fun onTabSelected(p0: TabLayout.Tab?) { } }) pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) { } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { } override fun onPageSelected(position: Int) { ((pager.adapter as? SeeingPagerAdapter)?.fragmentList)?.let { it[position].clickCount = 0 } } }) showRandomInterstitial(this, PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_seeing_auto, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.auto -> FavToSeeing.onConfirmation(this) } return super.onOptionsItemSelected(item) } companion object { fun open(context: Context) { context.startActivity(Intent(context, SeeingActivity::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingActivityMaterial.kt ================================================ package knf.kuma.seeing import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.widget.Toolbar import androidx.viewpager.widget.ViewPager import com.google.android.material.tabs.TabLayout import knf.kuma.R import knf.kuma.ads.showRandomInterstitial import knf.kuma.commons.EAHelper import knf.kuma.commons.PrefsUtil import knf.kuma.commons.bind import knf.kuma.commons.setSurfaceBars import knf.kuma.custom.GenericActivity class SeeingActivityMaterial : GenericActivity() { val toolbar: Toolbar by bind(R.id.toolbar) val tabs: TabLayout by bind(R.id.tabs) val pager: ViewPager by bind(R.id.pager) override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getTheme()) super.onCreate(savedInstanceState) setSurfaceBars() setContentView(R.layout.activity_seening_material) toolbar.title = "Siguiendo" setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(false) toolbar.setNavigationOnClickListener { finish() } pager.adapter = SeeingPagerAdapterMaterial(supportFragmentManager) pager.offscreenPageLimit = 5 tabs.setupWithViewPager(pager) tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) { ((pager.adapter as? SeeingPagerAdapterMaterial)?.fragmentList)?.let { it[pager.currentItem].onSelected() } } override fun onTabUnselected(p0: TabLayout.Tab?) { } override fun onTabSelected(p0: TabLayout.Tab?) { } }) pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) { } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { } override fun onPageSelected(position: Int) { ((pager.adapter as? SeeingPagerAdapter)?.fragmentList)?.let { it[position].clickCount = 0 } } }) showRandomInterstitial(this, PrefsUtil.fullAdsExtraProbability) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_seeing_auto, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.auto -> FavToSeeing.onConfirmation(this) } return super.onOptionsItemSelected(item) } companion object { fun open(context: Context) { context.startActivity(Intent(context, SeeingActivityMaterial::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingAdapter.kt ================================================ package knf.kuma.seeing import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupMenu import android.widget.ProgressBar import android.widget.TextView import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnime import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.database.CacheDB import knf.kuma.pojos.SeeingObject import org.jetbrains.anko.doAsync internal class SeeingAdapter(private val activity: Activity, private val isFullList: Boolean) : PagingDataAdapter(SeeingObject.diffCallback), FastScrollRecyclerView.SectionedAdapter { private val seeingDAO = CacheDB.INSTANCE.seeingDAO() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { 0 -> SeeingItem(LayoutInflater.from(parent.context).inflate(R.layout.item_record_grid, parent, false)) else -> SeeingItemNormal(LayoutInflater.from(parent.context).inflate(R.layout.item_dir_grid, parent, false)) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList) { val item = getItem(position) if (payloads.isEmpty() || item == null) super.onBindViewHolder(holder, position, payloads) else if (holder is SeeingItem) { holder.chapter.text = getCardText(item) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val seeingObject = getItem(position) ?: return if (holder is SeeingItem) holder.chapter.text = getCardText(seeingObject) (holder as? SeeingItemNormal)?.apply { imageView.load(PatternUtil.getCover(seeingObject.aid)) title.text = seeingObject.title progressView?.visibility = View.GONE cardView.setOnClickListener { ActivityAnime.open(activity, seeingObject) } cardView.setOnLongClickListener { view -> val popupMenu = PopupMenu(activity, view) popupMenu.inflate(R.menu.menu_seeing) when (seeingObject.state) { SeeingObject.STATE_WATCHING -> popupMenu.menu.findItem(R.id.watching).isVisible = false SeeingObject.STATE_CONSIDERING -> popupMenu.menu.findItem(R.id.considering).isVisible = false SeeingObject.STATE_COMPLETED -> popupMenu.menu.findItem(R.id.completed).isVisible = false SeeingObject.STATE_DROPPED -> popupMenu.menu.findItem(R.id.droped).isVisible = false } popupMenu.setOnMenuItemClickListener { menuItem -> doAsync { when (menuItem.itemId) { R.id.watching -> seeingDAO.update(seeingObject.also { it.state = 1 }) R.id.considering -> seeingDAO.update(seeingObject.also { it.state = 2 }) R.id.completed -> seeingDAO.update(seeingObject.also { it.state = 3 }) R.id.droped -> seeingDAO.update(seeingObject.also { it.state = 4 }) R.id.paused -> seeingDAO.update(seeingObject.also { it.state = 5 }) } syncData { seeing() } if (isFullList) doOnUIGlobal { (holder as? SeeingItem)?.chapter?.text = getCardText(seeingObject) } } true } popupMenu.show() true } } } override fun getSectionName(position: Int): String { return getItem(position)?.title?.substring(0, 1) ?: "" } private fun getCardText(seeingObject: SeeingObject): String { return if (isFullList) { getStateText(seeingObject.state) } else { val lastChapter = seeingObject.lastChapter val number = lastChapter?.number if (number == null) "No empezado" else if (!lastChapter.number.startsWith("Episodio ")) "Episodio ${lastChapter.number}" else lastChapter.number } } private fun getStateText(state: Int): String { return when (state) { 1 -> "Viendo" 2 -> "Considerando" 3 -> "Completado" 4 -> "Dropeado" else -> "Pausado" } } override fun getItemViewType(position: Int): Int { val seeingObject = getItem(position) ?: return 0 return when { isFullList || seeingObject.state in 0..1 -> 0 else -> 1 } } internal class SeeingItem(itemView: View) : SeeingItemNormal(itemView) { val chapter: TextView by itemView.bind(R.id.chapter) } internal open class SeeingItemNormal(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: MaterialCardView by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar? by itemView.optionalBind(R.id.progress) val title: TextView by itemView.bind(R.id.title) } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingAdapterMaterial.kt ================================================ package knf.kuma.seeing import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupMenu import android.widget.ProgressBar import android.widget.TextView import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import knf.kuma.R import knf.kuma.animeinfo.ActivityAnimeMaterial import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PatternUtil import knf.kuma.commons.bind import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.load import knf.kuma.commons.optionalBind import knf.kuma.database.CacheDB import knf.kuma.pojos.SeeingObject import org.jetbrains.anko.doAsync internal class SeeingAdapterMaterial(private val activity: Activity, private val isFullList: Boolean) : PagingDataAdapter(SeeingObject.diffCallback), FastScrollRecyclerView.SectionedAdapter { private val seeingDAO = CacheDB.INSTANCE.seeingDAO() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { 0 -> SeeingItem(LayoutInflater.from(parent.context).inflate(R.layout.item_record_grid_material, parent, false)) else -> SeeingItemNormal(LayoutInflater.from(parent.context).inflate(R.layout.item_dir_grid_material, parent, false)) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList) { val item = getItem(position) if (payloads.isEmpty() || item == null) super.onBindViewHolder(holder, position, payloads) else if (holder is SeeingItem) { holder.chapter.text = getCardText(item) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val seeingObject = getItem(position) ?: return if (holder is SeeingItem) holder.chapter.text = getCardText(seeingObject) (holder as? SeeingItemNormal)?.apply { imageView.load(PatternUtil.getCover(seeingObject.aid)) title.text = seeingObject.title progressView?.visibility = View.GONE cardView.setOnClickListener { ActivityAnimeMaterial.open(activity, seeingObject) } cardView.setOnLongClickListener { view -> val popupMenu = PopupMenu(activity, view) popupMenu.inflate(R.menu.menu_seeing) when (seeingObject.state) { SeeingObject.STATE_WATCHING -> popupMenu.menu.findItem(R.id.watching).isVisible = false SeeingObject.STATE_CONSIDERING -> popupMenu.menu.findItem(R.id.considering).isVisible = false SeeingObject.STATE_COMPLETED -> popupMenu.menu.findItem(R.id.completed).isVisible = false SeeingObject.STATE_DROPPED -> popupMenu.menu.findItem(R.id.droped).isVisible = false } popupMenu.setOnMenuItemClickListener { menuItem -> doAsync { when (menuItem.itemId) { R.id.watching -> seeingDAO.update(seeingObject.also { it.state = 1 }) R.id.considering -> seeingDAO.update(seeingObject.also { it.state = 2 }) R.id.completed -> seeingDAO.update(seeingObject.also { it.state = 3 }) R.id.droped -> seeingDAO.update(seeingObject.also { it.state = 4 }) R.id.paused -> seeingDAO.update(seeingObject.also { it.state = 5 }) } syncData { seeing() } if (isFullList) doOnUIGlobal { (holder as? SeeingItem)?.chapter?.text = getCardText(seeingObject) } } true } popupMenu.show() true } } } override fun getSectionName(position: Int): String { return getItem(position)?.title?.substring(0, 1) ?: "" } private fun getCardText(seeingObject: SeeingObject): String { return if (isFullList) { getStateText(seeingObject.state) } else { val lastChapter = seeingObject.lastChapter val number = lastChapter?.number if (number == null) "No empezado" else if (!lastChapter.number.startsWith("Episodio ")) "Episodio ${lastChapter.number}" else lastChapter.number } } private fun getStateText(state: Int): String { return when (state) { 1 -> "Viendo" 2 -> "Considerando" 3 -> "Completado" 4 -> "Dropeado" else -> "Pausado" } } override fun getItemViewType(position: Int): Int { val seeingObject = getItem(position) ?: return 0 return when { isFullList || seeingObject.state in 0..1 -> 0 else -> 1 } } internal class SeeingItem(itemView: View) : SeeingItemNormal(itemView) { val chapter: TextView by itemView.bind(R.id.chapter) } internal open class SeeingItemNormal(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: View by itemView.bind(R.id.card) val imageView: ImageView by itemView.bind(R.id.img) val progressView: ProgressBar? by itemView.optionalBind(R.id.progress) val title: TextView by itemView.bind(R.id.title) } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingFragment.kt ================================================ package knf.kuma.seeing import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.databinding.FragmentSeeingBinding import knf.kuma.pojos.SeeingObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import xdroid.toaster.Toaster class SeeingFragment : Fragment() { var clickCount = 0 private lateinit var binding: FragmentSeeingBinding private val adapter: SeeingAdapter? by lazy { activity?.let { SeeingAdapter(it, arguments?.getInt("state", 0) == 0) } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) lifecycleScope.launch { liveData.collectLatest { binding.progress.visibility = View.GONE adapter?.submitData(it) } } adapter?.addLoadStateListener { binding.error.isVisible = it.append.endOfPaginationReached && adapter?.itemCount == 0 } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_seeing, container, false).also { binding = FragmentSeeingBinding.bind(it) } } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { delay(1000) binding.adContainer.implBanner(AdsType.SEEING_BANNER, true) } when (arguments?.getInt("state", 0)) { 1 -> { binding.errorText.text = "No estas viendo ningún anime" binding.errorImg.setImageResource(R.drawable.ic_watching) } 2 -> { binding.errorText.text = "No consideras ningún anime" binding.errorImg.setImageResource(R.drawable.ic_considering) } 3 -> { binding.errorText.text = "No has terminado ningún anime" binding.errorImg.setImageResource(R.drawable.ic_completed) } 4 -> { binding.errorText.text = "No has dropeado ningún anime" binding.errorImg.setImageResource(R.drawable.ic_droped) } 5 -> { binding.errorText.text = "No tienes pausado ningún anime" binding.errorImg.setImageResource(R.drawable.ic_paused) } else -> binding.errorText.text = "No has marcado ningún anime" } binding.recycler.verifyManager() binding.recycler.adapter = adapter } val liveData: Flow> get() { return Pager( PagingConfig(15, enablePlaceholders = false), 0, (if (arguments?.getInt("state", 0) ?: 0 == 0) CacheDB.INSTANCE.seeingDAO().allPaging else CacheDB.INSTANCE.seeingDAO().getLiveByStatePaging( arguments?.getInt("state", 0) ?: 0 )).asPagingSourceFactory() ).flow } val title: String get() { return when (arguments?.getInt("state", 0)) { 1 -> "Viendo" 2 -> "Considerando" 3 -> "Completado" 4 -> "Dropeado" 5 -> "Pausado" else -> "Todos" } } fun onSelected() { clickCount++ if (clickCount == 3) { lifecycleScope.launch(Dispatchers.Main) { val state = arguments?.getInt("state", -1) ?: -1 if (state == -1) return@launch val num = if (state == 0) CacheDB.INSTANCE.seeingDAO().countAll else CacheDB.INSTANCE.seeingDAO().countByState(state) if (num > 0) Toaster.toast("$num anime" + adapter?.let { if (num > 1) "s" else "" }) } clickCount = 0 } } companion object { operator fun get(state: Int): SeeingFragment { val emissionFragment = SeeingFragment() val bundle = Bundle() bundle.putInt("state", state) emissionFragment.arguments = bundle return emissionFragment } } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingFragmentMaterial.kt ================================================ package knf.kuma.seeing import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import knf.kuma.R import knf.kuma.ads.AdsType import knf.kuma.ads.implBanner import knf.kuma.commons.verifyManager import knf.kuma.database.CacheDB import knf.kuma.databinding.FragmentSeeingBinding import knf.kuma.pojos.SeeingObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import xdroid.toaster.Toaster class SeeingFragmentMaterial : Fragment() { private var clickCount = 0 private lateinit var binding: FragmentSeeingBinding private val adapter: SeeingAdapterMaterial? by lazy { activity?.let { SeeingAdapterMaterial(it, arguments?.getInt("state", 0) == 0) } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) lifecycleScope.launch { liveData.collectLatest { binding.progress.visibility = View.GONE adapter?.submitData(it) } } adapter?.addLoadStateListener { binding.error.isVisible = it.append.endOfPaginationReached && adapter?.itemCount == 0 } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_seeing, container, false).also { binding = FragmentSeeingBinding.bind(it) } } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { delay(1000) binding.adContainer.implBanner(AdsType.SEEING_BANNER, true) } when (arguments?.getInt("state", 0)) { 1 -> { binding.errorText.text = "No estas viendo ningún anime" binding.errorImg.setImageResource(R.drawable.ic_watching) } 2 -> { binding.errorText.text = "No consideras ningún anime" binding.errorImg.setImageResource(R.drawable.ic_considering) } 3 -> { binding.errorText.text = "No has terminado ningún anime" binding.errorImg.setImageResource(R.drawable.ic_completed) } 4 -> { binding.errorText.text = "No has dropeado ningún anime" binding.errorImg.setImageResource(R.drawable.ic_droped) } 5 -> { binding.errorText.text = "No tienes pausado ningún anime" binding.errorImg.setImageResource(R.drawable.ic_paused) } else -> binding.errorText.text = "No has marcado ningún anime" } binding.recycler.verifyManager() binding.recycler.adapter = adapter } val liveData: Flow> get() { return Pager( PagingConfig(15, enablePlaceholders = false), 0, (if (arguments?.getInt("state", 0) ?: 0 == 0) CacheDB.INSTANCE.seeingDAO().allPaging else CacheDB.INSTANCE.seeingDAO().getLiveByStatePaging( arguments?.getInt("state", 0) ?: 0 )).asPagingSourceFactory() ).flow } val title: String get() { return when (arguments?.getInt("state", 0)) { 1 -> "Viendo" 2 -> "Considerando" 3 -> "Completado" 4 -> "Dropeado" 5 -> "Pausado" else -> "Todos" } } fun onSelected() { clickCount++ if (clickCount == 3) { lifecycleScope.launch(Dispatchers.Main) { val state = arguments?.getInt("state", -1) ?: -1 if (state == -1) return@launch val num = withContext(Dispatchers.IO) { if (state == 0) CacheDB.INSTANCE.seeingDAO().countAll else CacheDB.INSTANCE.seeingDAO().countByState(state) } if (num > 0) Toaster.toast("$num anime" + adapter?.let { if (num > 1) "s" else "" }) } clickCount = 0 } } companion object { operator fun get(state: Int): SeeingFragmentMaterial { val emissionFragment = SeeingFragmentMaterial() val bundle = Bundle() bundle.putInt("state", state) emissionFragment.arguments = bundle return emissionFragment } } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingPagerAdapter.kt ================================================ package knf.kuma.seeing import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter class SeeingPagerAdapter(fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager) { val fragmentList: MutableList = mutableListOf() init { for (i in 0..5) { fragmentList.add(SeeingFragment[i]) } } override fun getItem(position: Int): Fragment { return fragmentList[position] } override fun getCount(): Int { return fragmentList.size } override fun getPageTitle(position: Int): CharSequence { return fragmentList[position].title } } ================================================ FILE: app/src/main/java/knf/kuma/seeing/SeeingPagerAdapterMaterial.kt ================================================ package knf.kuma.seeing import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter class SeeingPagerAdapterMaterial(fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { val fragmentList: MutableList = mutableListOf() init { for (i in 0..5) { fragmentList.add(SeeingFragmentMaterial[i]) } } override fun getItem(position: Int): Fragment { return fragmentList[position] } override fun getCount(): Int { return fragmentList.size } override fun getPageTitle(position: Int): CharSequence { return fragmentList[position].title } } ================================================ FILE: app/src/main/java/knf/kuma/shortcuts/DummyActivity.kt ================================================ package knf.kuma.shortcuts import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityOptionsCompat abstract class DummyActivity : AppCompatActivity() { abstract val intentClass: Class<*> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) finish() startActivity(Intent(this, intentClass).apply { data = intent.data putExtras(intent) }, ActivityOptionsCompat.makeCustomAnimation(this, android.R.anim.fade_in, android.R.anim.fade_out).toBundle()) } } ================================================ FILE: app/src/main/java/knf/kuma/shortcuts/DummyEmissionActivity.kt ================================================ package knf.kuma.shortcuts import knf.kuma.commons.DesignUtils class DummyEmissionActivity : DummyActivity() { override val intentClass: Class<*> = DesignUtils.emissionClass } ================================================ FILE: app/src/main/java/knf/kuma/shortcuts/DummyExplorerActivity.kt ================================================ package knf.kuma.shortcuts import knf.kuma.commons.DesignUtils class DummyExplorerActivity : DummyActivity() { override val intentClass: Class<*> = DesignUtils.explorerClass } ================================================ FILE: app/src/main/java/knf/kuma/shortcuts/DummyMainActivity.kt ================================================ package knf.kuma.shortcuts import knf.kuma.commons.DesignUtils class DummyMainActivity : DummyActivity() { override val intentClass: Class<*> = DesignUtils.mainClass } ================================================ FILE: app/src/main/java/knf/kuma/slices/AnimeSliceObject.kt ================================================ package knf.kuma.slices import androidx.core.graphics.drawable.IconCompat import androidx.room.Ignore import androidx.room.TypeConverters import knf.kuma.pojos.AnimeObject import knf.kuma.search.SearchObject @TypeConverters(AnimeObject.Converter::class) class AnimeSliceObject : SearchObject() { var genres = listOf() @Ignore lateinit var icon: IconCompat val genresString: String get() { if (genres.isEmpty()) return "Sin generos" val builder = StringBuilder() for (genre in genres) { builder.append(genre) .append(", ") } val g = builder.toString() return g.substring(0, g.lastIndexOf(",")) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/AnimeRow.kt ================================================ package knf.kuma.tv import androidx.leanback.widget.ArrayObjectAdapter class AnimeRow { internal var page: Int = 0 internal var id: Int = 0 internal var adapter: ArrayObjectAdapter? = null internal var title: String? = null fun getPage(): Int { return page } fun setPage(page: Int): AnimeRow { this.page = page return this } fun getId(): Int { return id } fun setId(id: Int): AnimeRow { this.id = id return this } fun getAdapter(): ArrayObjectAdapter? { return adapter } fun setAdapter(adapter: ArrayObjectAdapter): AnimeRow { this.adapter = adapter return this } fun getTitle(): String? { return title } fun setTitle(title: String): AnimeRow { this.title = title return this } fun setList(list: List) { adapter?.apply { clear() addAll(0, list) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/BindableCardView.kt ================================================ package knf.kuma.tv import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.ImageView import androidx.annotation.LayoutRes import androidx.leanback.widget.BaseCardView abstract class BindableCardView : BaseCardView { abstract val imageView: ImageView @get:LayoutRes abstract val layoutResource: Int constructor(context: Context) : super(context) { initLayout() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initLayout() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initLayout() } private fun initLayout() { isFocusable = true isFocusableInTouchMode = true val inflater = LayoutInflater.from(context) inflater.inflate(layoutResource, this) } abstract fun bind(data: T) } ================================================ FILE: app/src/main/java/knf/kuma/tv/ChannelUtils.kt ================================================ package knf.kuma.tv import android.content.Context import android.graphics.BitmapFactory import android.net.Uri import androidx.tvprovider.media.tv.PreviewChannel import androidx.tvprovider.media.tv.PreviewChannelHelper import androidx.tvprovider.media.tv.PreviewProgram import androidx.tvprovider.media.tv.TvContractCompat import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.PrefsUtil import knf.kuma.commons.urlFixed import knf.kuma.database.CacheDB import knf.kuma.pojos.RecentObject import knf.kuma.retrofit.Repository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch object ChannelUtils { fun createIfNeeded(context: Context) { if (!context.resources.getBoolean(R.bool.isTv)) return if (!PrefsUtil.tvRecentsChannelCreated) { GlobalScope.launch(Dispatchers.IO) { try { val channelBuilder = PreviewChannel.Builder() .setDisplayName("Episodios recientes") .setAppLinkIntentUri(Uri.parse("ukiku://tv/home")) .setLogo( BitmapFactory.decodeResource( context.resources, R.drawable.ukiku_logo_plain ) ) val channelId = PreviewChannelHelper(context).publishDefaultChannel(channelBuilder.build()) PrefsUtil.tvRecentsChannelCreated = true PrefsUtil.tvRecentsChannelId = channelId } catch (e: Exception) { e.printStackTrace() } } } initChannelIfNeeded(context) } fun initChannelIfNeeded(context: Context) { if (!context.resources.getBoolean(R.bool.isTv) || !PrefsUtil.tvRecentsChannelCreated) return if (!PrefsUtil.tvRecentsPreFilled) GlobalScope.launch(Dispatchers.IO) { delay(5000) val recents = CacheDB.INSTANCE.recentsDAO().allSimple if (recents.isNotEmpty()) { PrefsUtil.tvRecentsPreFilled = true val programIds = mutableSetOf() recents.forEach { val chapUri = Uri.Builder().scheme("ukiku") .authority("tv") .appendPath("chapter") .appendQueryParameter("aid", it.aid) .appendQueryParameter("chapter", it.chapter) .appendQueryParameter("eid", it.eid) .appendQueryParameter("url", it.url.urlFixed) .appendQueryParameter("name", it.name) .build() val program = PreviewProgram.Builder() .setChannelId(PrefsUtil.tvRecentsChannelId) .setType(TvContractCompat.PreviewPrograms.TYPE_TV_EPISODE) .setTitle(it.name) .setEpisodeNumber(it.chapter.substringAfterLast(" ").toInt()) .setIntentUri(chapUri) .setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_3_2) .setPosterArtUri(Uri.parse(PatternUtil.getThumb(it.aid))) programIds.add( PreviewChannelHelper(context).publishPreviewProgram(program.build()) .toString() ) } PrefsUtil.tvRecentsChannelLastEid = recents.first().eid PrefsUtil.tvRecentsChannelIds = programIds } else Repository().reloadAllRecents() } } fun addProgram(context: Context, recentObject: RecentObject): Long { val chapUri = Uri.Builder().scheme("ukiku") .authority("tv") .appendPath("chapter") .appendQueryParameter("aid", recentObject.aid) .appendQueryParameter("chapter", recentObject.chapter) .appendQueryParameter("eid", recentObject.eid) .appendQueryParameter("url", recentObject.url.urlFixed) .appendQueryParameter("name", recentObject.name) .build() val program = PreviewProgram.Builder() .setContentId(recentObject.eid) .setChannelId(PrefsUtil.tvRecentsChannelId) .setType(TvContractCompat.PreviewPrograms.TYPE_TV_EPISODE) .setTitle(recentObject.name) .setEpisodeNumber(recentObject.chapter.substringAfterLast(" ").toInt()) .setIntentUri(chapUri) .setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_3_2) .setPosterArtUri(Uri.parse(PatternUtil.getThumb(recentObject.aid))) .setWeight(999) return PreviewChannelHelper(context).publishPreviewProgram(program.build()) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/GlideBackgroundManager.kt ================================================ package knf.kuma.tv import android.app.Activity import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import androidx.leanback.app.BackgroundManager import androidx.palette.graphics.Palette import com.bumptech.glide.Glide import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import knf.kuma.App import knf.kuma.commons.doOnUIGlobal import java.lang.ref.WeakReference import java.util.Timer import java.util.TimerTask class GlideBackgroundManager(activity: Activity) { private val mActivityWeakReference: WeakReference = WeakReference(activity) private val mBackgroundManager: BackgroundManager? = BackgroundManager.getInstance(activity) private var mBackgroundURI: String? = null private var mBackgroundTimer: Timer? = null private val mGlideDrawableSimpleTarget = object : SimpleTarget() { override fun onResourceReady(resource: Drawable, transition: Transition?) { Palette.from((resource as BitmapDrawable).bitmap).generate { palette -> val textSwatch = palette?.darkMutedSwatch if (textSwatch != null) setBackground(ColorDrawable(textSwatch.rgb)) } } } init { if (mBackgroundManager?.isAttached == false) mBackgroundManager.attach(activity.window) } fun loadImage(imageUrl: String) { mBackgroundURI = imageUrl startBackgroundTimer() } fun setBackground(drawable: Drawable) { if (mBackgroundManager != null) { if (!mBackgroundManager.isAttached) { mBackgroundManager.attach(mActivityWeakReference.get()?.window) } mBackgroundManager.drawable = drawable } } /** * Cancels an ongoing background change */ fun cancelBackgroundChange() { mBackgroundURI = null cancelTimer() } /** * Stops the timer */ private fun cancelTimer() { mBackgroundTimer?.cancel() } /** * Starts the background change timer */ private fun startBackgroundTimer() { cancelTimer() mBackgroundTimer = Timer() /* set delay time to reduce too much background image loading process */ mBackgroundTimer?.schedule(UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY.toLong()) } /** * Updates the background with the last known URI */ fun updateBackground() { Glide.with(App.context) .load(mBackgroundURI) .into>(mGlideDrawableSimpleTarget) } private inner class UpdateBackgroundTask : TimerTask() { override fun run() { doOnUIGlobal { if (mBackgroundURI != null) { updateBackground() } } } } companion object { private val TAG = GlideBackgroundManager::class.java.simpleName private const val BACKGROUND_UPDATE_DELAY = 200 var instance: GlideBackgroundManager? = null } } ================================================ FILE: app/src/main/java/knf/kuma/tv/TVBaseActivity.kt ================================================ package knf.kuma.tv import android.os.Bundle import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import knf.kuma.R import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.commons.SSLSkipper open class TVBaseActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.tv_activity_main) SSLSkipper.skip() FirestoreManager.start() } fun addFragment(fragment: Fragment) { val fragmentTransaction = supportFragmentManager.beginTransaction() fragmentTransaction.replace(R.id.tv_frame_content, fragment) fragmentTransaction.commit() } } ================================================ FILE: app/src/main/java/knf/kuma/tv/TVServersFactory.kt ================================================ package knf.kuma.tv import android.app.Activity import android.content.Intent import android.net.Uri import android.util.Log import androidx.leanback.widget.Presenter import knf.kuma.App import knf.kuma.backup.firestore.syncData import knf.kuma.commons.doOnUIGlobal import knf.kuma.commons.iterator import knf.kuma.commons.jsoupCookies import knf.kuma.database.CacheDB import knf.kuma.player.openWebPlayer import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.DownloadObject import knf.kuma.pojos.RecordObject import knf.kuma.pojos.SeenObject import knf.kuma.tv.exoplayer.TVPlayer import knf.kuma.tv.streaming.TVMultiSelection import knf.kuma.tv.streaming.TVServerSelection import knf.kuma.tv.streaming.TVServerSelectionFragment import knf.kuma.videoservers.Option import knf.kuma.videoservers.Server import knf.kuma.videoservers.VideoServer import knf.kuma.videoservers.WebServer import org.jetbrains.anko.doAsync import org.json.JSONArray import org.json.JSONObject import xdroid.toaster.Toaster import java.util.Locale class TVServersFactory private constructor( private val activity: Activity, private val url: String, private val chapter: AnimeObject.WebInfo.AnimeChapter, val viewHolder: Presenter.ViewHolder?, private val serversInterface: ServersInterface ) { private val downloadObject: DownloadObject = DownloadObject.fromChapter(chapter, false) private var jsonObject: JSONObject? = null private var servers: MutableList = ArrayList() private var current: VideoServer? = null fun showServerList() { doOnUIGlobal { try { if (servers.isEmpty()) { Toaster.toast("Sin servidores disponibles") serversInterface.onFinish(false, false) } else { activity.startActivityForResult( Intent(activity, TVServerSelection::class.java) .putExtra(TVServerSelectionFragment.SERVERS_DATA, Server.getNames(servers) as ArrayList), REQUEST_CODE_LIST ) } } catch (e: Exception) { e.printStackTrace() } } } fun analyzeMulti(position: Int) { doAsync { val main = jsoupCookies(url).get() val downloads = main.select("table.RTbl.Dwnl tr:contains(${if (position == 0) "SUB" else "LAT"}) a.Button.Sm.fa-download") for (e in downloads) { var z = e.attr("href") z = z.substring(z.lastIndexOf("http")) val server = Server.check(activity, z) if (server != null) servers.add(server) } val jsonArray = jsonObject?.getJSONArray(if (position == 0) "SUB" else "LAT") ?: JSONArray() for (baseLink in jsonArray) { val server = Server.check(activity, baseLink.optString("code")) if (server != null) try { var skip = false servers.forEach { if (it.name == server.name) { skip = true return@forEach } } if (!skip) servers.add(server) } catch (e: Exception) { e.printStackTrace() } } servers.sort() showServerList() } } fun analyzeServer(position: Int) { doAsync { try { val text = servers[position].name val server = servers[position].verified if (server == null && servers.size == 1) { Toaster.toast("Error en servidor, intente mas tarde") serversInterface.onFinish(false, false) } else if (server == null) { Toaster.toast("Error en servidor") showServerList() } else if (server.options.size == 0) { Toaster.toast("Error en servidor") showServerList() } else if (server.haveOptions()) { showOptions(server) } else if (servers[position] is WebServer) { try { openWebPlayer(activity, server.option.url!!) doAsync { CacheDB.INSTANCE.seenDAO().addChapter(SeenObject.fromChapter(chapter)) CacheDB.INSTANCE.recordsDAO().add(RecordObject.fromChapter(chapter)) syncData { history() seen() } } serversInterface.onFinish(false, true) } catch (_: Exception) { Toaster.toast("Error al abrir explorador web") showServerList() } } else { when (text.lowercase(Locale.getDefault())) { "mega" -> { Toaster.toast("No se puede usar Mega en TV") showServerList() } else -> startStreaming(server.option) } } } catch (e: Exception) { e.printStackTrace() } } } fun analyzeOption(position: Int) { current?.let { startStreaming(it.options[position]) } } private fun showOptions(server: VideoServer) { this.current = server activity.startActivityForResult( Intent(activity, TVServerSelection::class.java) .putExtra("name", server.name) .putExtra( TVServerSelectionFragment.VIDEO_DATA, (Option.getNames(server.options) as? ArrayList) ?: arrayListOf() ), REQUEST_CODE_OPTION ) } private fun startStreaming(option: Option) { doAsync { CacheDB.INSTANCE.seenDAO().addChapter(SeenObject.fromChapter(chapter)) CacheDB.INSTANCE.recordsDAO().add(RecordObject.fromChapter(chapter)) syncData { history() seen() } } activity.startActivity(Intent(activity, TVPlayer::class.java).apply { setDataAndType(Uri.parse(option.url), "video/*") putExtra("title", downloadObject.name) putExtra("chapter", downloadObject.chapter) putStringArrayListExtra("headers", ArrayList(option.headers?.createHeadersList()?: emptyList())) }) serversInterface.onFinish(false, true) } fun get() { try { Log.e("Url", url) val main = jsoupCookies(url).get() val servers = ArrayList() val sScript = main.select("script") var j = "" for (element in sScript) { val sEl = element.outerHtml() if ("\\{\"[SUBLAT]+\":\\[.*\\]\\}".toRegex().containsMatchIn(sEl)) { j = sEl break } } jsonObject = JSONObject("\\{\"[SUBLAT]+\":\\[.*\\]\\}".toRegex().find(j)?.value) if (jsonObject?.length() ?: 0 > 1) { this.servers = servers activity.startActivityForResult( Intent(activity, TVMultiSelection::class.java), REQUEST_CODE_MULTI ) } else { val downloads = main.select("table.RTbl.Dwnl tr:contains(SUB) a.Button.Sm.fa-download") for (e in downloads) { var z = e.attr("href") z = z.substring(z.lastIndexOf("http")) val server = Server.check(activity, z) if (server != null) servers.add(server) } val jsonArray = jsonObject?.getJSONArray("SUB") ?: JSONArray() for (baseLink in jsonArray) { val server = Server.check(activity, baseLink.optString("code")) if (server != null) servers.add(server) else if (!baseLink.optString("code").contains("linkinpork")) servers.add(WebServer(App.context, baseLink.optString("code"), baseLink.optString("title"))) } servers.sort() this.servers = servers.filter { it is WebServer || it.canStream }.distinctBy { it.baseLink.dropLastWhile { it == '/' }.substringAfterLast("/") }.toMutableList() showServerList() } } catch (e: Exception) { e.printStackTrace() this.servers = ArrayList() serversInterface.onFinish(false, false) } } interface ServersInterface { fun onReady(serversFactory: TVServersFactory) fun onFinish(started: Boolean, success: Boolean) } companion object { var REQUEST_CODE_LIST = 4456 var REQUEST_CODE_OPTION = 6157 var REQUEST_CODE_MULTI = 6497 fun start(activity: Activity, url: String, chapter: AnimeObject.WebInfo.AnimeChapter, serversInterface: ServersInterface) { start(activity, url, chapter, null, serversInterface) } fun start(activity: Activity, url: String, chapter: AnimeObject.WebInfo.AnimeChapter, viewHolder: Presenter.ViewHolder?, serversInterface: ServersInterface?) { doAsync { serversInterface?.let { val factory = TVServersFactory(activity, url, chapter, viewHolder, it) serversInterface.onReady(factory) factory.get() } } } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/AnimePresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.tv.cards.AnimeCardView import knf.kuma.tv.search.BasicAnimeObject class AnimePresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(AnimeCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as AnimeCardView).bind(item as BasicAnimeObject) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/ChapterPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.pojos.AnimeObject import knf.kuma.tv.cards.ChapterCardView class ChapterPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(ChapterCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as ChapterCardView).bind(item as AnimeObject.WebInfo.AnimeChapter) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/EmissionPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.search.SearchObject import knf.kuma.tv.cards.EmissionCardView class EmissionPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(EmissionCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as EmissionCardView).bind(item as SearchObject) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/FavPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.pojos.FavoriteObject import knf.kuma.tv.cards.FavCardView class FavPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(FavCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as FavCardView).bind(item as FavoriteObject) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/RecentsPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.database.CacheDB import knf.kuma.pojos.RecentObject import knf.kuma.tv.cards.RecentsCardView import knf.kuma.tv.details.TVAnimesDetails import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class RecentsPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(RecentsCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as RecentsCardView).bind(item as RecentObject) viewHolder.view.setOnLongClickListener { v -> GlobalScope.launch(Dispatchers.Main){ val animeObject = withContext(Dispatchers.IO) { CacheDB.INSTANCE.animeDAO().getByAid(item.aid) } animeObject?.let { TVAnimesDetails.start(v.context, it.link) } } true } } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/RecordPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.pojos.RecordObject import knf.kuma.tv.cards.RecordCardView class RecordPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(RecordCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as RecordCardView).bind(item as RecordObject) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/RelatedPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.pojos.AnimeObject import knf.kuma.tv.cards.RelatedCardView class RelatedPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(RelatedCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as RelatedCardView).bind(item as AnimeObject.WebInfo.AnimeRelated) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/SectionPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.tv.cards.SectionCardView import knf.kuma.tv.sections.SectionObject class SectionPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(SectionCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { (item as? SectionObject)?.let { (viewHolder.view as? SectionCardView)?.bind(it) } } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/anime/SyncPresenter.kt ================================================ package knf.kuma.tv.anime import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.tv.cards.SyncCardView import knf.kuma.tv.sync.SyncObject class SyncPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(SyncCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { (item as? SyncObject)?.let { (viewHolder.view as? SyncCardView)?.bind(it) } } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/AnimeCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.tv.BindableCardView import knf.kuma.tv.search.BasicAnimeObject import org.jetbrains.anko.find class AnimeCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card override fun bind(data: BasicAnimeObject) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/ChapterCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.view.View import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.loadGlide import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject import knf.kuma.tv.BindableCardView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.find class ChapterCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card_chapter_preview override fun bind(data: AnimeObject.WebInfo.AnimeChapter) { imageView.loadGlide(data.img) GlobalScope.launch(Dispatchers.Main) { find(R.id.indicator).visibility = if (withContext(Dispatchers.IO) { CacheDB.INSTANCE.seenDAO().chapterIsSeen(data.aid, data.number) }) VISIBLE else GONE } find(R.id.chapter).text = data.number } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/DirAdvCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.directory.DirObject import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class DirAdvCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card_adv override fun bind(data: DirObject) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name find(R.id.rating).text = "\u2605${data.rate_stars ?: "?.?"}" find(R.id.type).text = data.type } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/DirCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.directory.DirObject import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class DirCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card_rate override fun bind(data: DirObject) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name find(R.id.rating).text = "\u2605${data.rate_stars ?: "?.?"}" } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/EmissionCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.search.SearchObject import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class EmissionCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card override fun bind(data: SearchObject) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/FavCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.pojos.FavoriteObject import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class FavCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card override fun bind(data: FavoriteObject) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/RecentsCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.pojos.RecentObject import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class RecentsCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card_chapter override fun bind(data: RecentObject) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name find(R.id.chapter).text = data.chapter } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/RecordCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.pojos.RecordObject import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class RecordCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card_chapter override fun bind(data: RecordObject) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name find(R.id.chapter).text = data.chapter } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/RelatedCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.PatternUtil import knf.kuma.commons.loadGlide import knf.kuma.pojos.AnimeObject import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class RelatedCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_card_chapter override fun bind(data: AnimeObject.WebInfo.AnimeRelated) { imageView.loadGlide(PatternUtil.getCover(data.aid)) find(R.id.title).text = data.name find(R.id.chapter).text = data.relation } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/SectionCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.commons.doOnUIGlobal import knf.kuma.tv.BindableCardView import knf.kuma.tv.sections.SectionObject import org.jetbrains.anko.find class SectionCardView(context: Context) : BindableCardView(context) { override val layoutResource: Int get() = R.layout.item_tv_card_section override val imageView: ImageView get() = find(R.id.img) override fun bind(data: SectionObject) { doOnUIGlobal { imageView.setImageResource(data.image) } find(R.id.title).text = data.title } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/SyncCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.tv.BindableCardView import knf.kuma.tv.sync.SyncObject import org.jetbrains.anko.find class SyncCardView(context: Context) : BindableCardView(context) { override val layoutResource: Int get() = R.layout.item_tv_card_sync override val imageView: ImageView get() = find(R.id.img) override fun bind(data: SyncObject) { imageView.setImageResource(data.image) find(R.id.title).text = data.title } } ================================================ FILE: app/src/main/java/knf/kuma/tv/cards/TagCardView.kt ================================================ package knf.kuma.tv.cards import android.content.Context import android.widget.ImageView import android.widget.TextView import knf.kuma.R import knf.kuma.tv.BindableCardView import org.jetbrains.anko.find class TagCardView(context: Context) : BindableCardView(context) { override val imageView: ImageView get() = find(R.id.img) override val layoutResource: Int get() = R.layout.item_tv_tag override fun bind(data: String) { find(R.id.title).text = data } } ================================================ FILE: app/src/main/java/knf/kuma/tv/details/ChaptersListPresenter.kt ================================================ package knf.kuma.tv.details import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.RowPresenter class ChaptersListPresenter(val position: Int) : ListRowPresenter() { override fun onBindRowViewHolder(holder: RowPresenter.ViewHolder, item: Any) { super.onBindRowViewHolder(holder, item) val vh = holder as ViewHolder vh.gridView.selectedPosition = position } } ================================================ FILE: app/src/main/java/knf/kuma/tv/details/ChaptersListRow.kt ================================================ package knf.kuma.tv.details import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.ListRow import androidx.leanback.widget.ObjectAdapter class ChaptersListRow : ListRow { constructor(header: HeaderItem, adapter: ObjectAdapter) : super(header, adapter) constructor(id: Long, header: HeaderItem, adapter: ObjectAdapter) : super(id, header, adapter) constructor(adapter: ObjectAdapter) : super(adapter) } ================================================ FILE: app/src/main/java/knf/kuma/tv/details/CustomFullWidthDetailsOverviewRowPresenter.kt ================================================ package knf.kuma.tv.details import android.view.ViewGroup import androidx.leanback.widget.FullWidthDetailsOverviewRowPresenter import androidx.leanback.widget.Presenter class CustomFullWidthDetailsOverviewRowPresenter internal constructor(detailsPresenter: Presenter) : FullWidthDetailsOverviewRowPresenter(detailsPresenter) { private var mPreviousState = STATE_FULL init { initialState = STATE_FULL } override fun onLayoutLogo(viewHolder: ViewHolder, oldState: Int, logoChanged: Boolean) { val v = viewHolder.logoViewHolder.view val lp = v.layoutParams as ViewGroup.MarginLayoutParams lp.marginStart = v.resources.getDimensionPixelSize( androidx.leanback.R.dimen.lb_details_v2_logo_margin_start) lp.topMargin = v.resources.getDimensionPixelSize(androidx.leanback.R.dimen.lb_details_v2_blank_height) - lp.height / 2 val offset = (v.resources.getDimensionPixelSize(androidx.leanback.R.dimen.lb_details_v2_actions_height) + v .resources.getDimensionPixelSize(androidx.leanback.R.dimen.lb_details_v2_description_margin_top) + lp.height / 2).toFloat() when (viewHolder.state) { STATE_FULL -> if (mPreviousState == STATE_HALF) { v.animate().translationYBy(-offset) } STATE_HALF -> if (mPreviousState == STATE_FULL) { v.animate().translationYBy(offset) } else -> if (mPreviousState == STATE_HALF) { v.animate().translationYBy(-offset) } } mPreviousState = viewHolder.state v.layoutParams = lp } } ================================================ FILE: app/src/main/java/knf/kuma/tv/details/DetailsDescriptionPresenter.kt ================================================ package knf.kuma.tv.details import androidx.annotation.ColorInt import androidx.leanback.widget.AbstractDetailsDescriptionPresenter import knf.kuma.pojos.AnimeObject class DetailsDescriptionPresenter : AbstractDetailsDescriptionPresenter { @ColorInt private var titleColor: Int = 0 @ColorInt private var bodyColor: Int = 0 internal constructor(titleColor: Int, bodyColor: Int) { this.titleColor = titleColor this.bodyColor = bodyColor } internal constructor() { this.titleColor = 0 this.bodyColor = 0 } override fun onBindDescription(viewHolder: ViewHolder, itemData: Any) { val animeObject = itemData as AnimeObject viewHolder.title.text = animeObject.name viewHolder.subtitle.text = animeObject.genresString viewHolder.body.text = animeObject.description if (titleColor != 0) viewHolder.title.setTextColor(titleColor) if (bodyColor != 0) { viewHolder.subtitle.setTextColor(bodyColor) viewHolder.body.setTextColor(bodyColor) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/details/TVAnimesDetails.kt ================================================ package knf.kuma.tv.details import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import knf.kuma.R import knf.kuma.commons.doOnUI import knf.kuma.tv.TVBaseActivity import knf.kuma.tv.TVServersFactory class TVAnimesDetails : TVBaseActivity(), TVServersFactory.ServersInterface { private var fragment: TVAnimesDetailsFragment? = null private var serversFactory: TVServersFactory? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) fragment = TVAnimesDetailsFragment[intent.getStringExtra(keyUrl) ?: ""] addFragment(fragment as TVAnimesDetailsFragment) } override fun onReady(serversFactory: TVServersFactory) { this.serversFactory = serversFactory } override fun onFinish(started: Boolean, success: Boolean) { if (fragment != null && success) { fragment?.onStartStreaming() doOnUI { serversFactory?.viewHolder?.view?.apply { findViewById(R.id.indicator).visibility = View.VISIBLE invalidate() } } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == RESULT_OK) { val bundle = data?.extras if (requestCode == TVServersFactory.REQUEST_CODE_MULTI) serversFactory?.analyzeMulti(bundle?.getInt(keyPosition, 0) ?: 0) else { if (bundle?.getBoolean(keyIsVideoServer, false) == true) serversFactory?.analyzeOption(bundle.getInt(keyPosition, 0)) else serversFactory?.analyzeServer(bundle?.getInt(keyPosition, 0) ?: 0) } } else if (resultCode == RESULT_CANCELED && data?.extras?.getBoolean(keyIsVideoServer, false) == true) serversFactory?.showServerList() } companion object { private const val keyPosition = "position" private const val keyUrl = "url" private const val keyIsVideoServer = "is_video_server" fun start(context: Context, url: String?) { url ?: return context.startActivity(Intent(context, TVAnimesDetails::class.java).putExtra(keyUrl, url)) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/details/TVAnimesDetailsFragment.kt ================================================ package knf.kuma.tv.details import android.graphics.Bitmap import android.graphics.Color import android.os.Bundle import androidx.core.content.ContextCompat import androidx.leanback.app.DetailsSupportFragment import androidx.leanback.widget.Action import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.ClassPresenterSelector import androidx.leanback.widget.DetailsOverviewRow import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.ListRow import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.OnActionClickedListener import androidx.leanback.widget.OnItemViewClickedListener import androidx.leanback.widget.Presenter import androidx.leanback.widget.Row import androidx.leanback.widget.RowPresenter import androidx.leanback.widget.SparseArrayObjectAdapter import androidx.lifecycle.lifecycleScope import androidx.palette.graphics.Palette import com.bumptech.glide.Glide import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import knf.kuma.App import knf.kuma.R import knf.kuma.backup.firestore.syncData import knf.kuma.commons.PatternUtil import knf.kuma.commons.noCrash import knf.kuma.database.CacheDB import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.FavoriteObject import knf.kuma.retrofit.Repository import knf.kuma.tv.TVServersFactory import knf.kuma.tv.anime.ChapterPresenter import knf.kuma.tv.anime.RelatedPresenter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class TVAnimesDetailsFragment : DetailsSupportFragment(), OnItemViewClickedListener, OnActionClickedListener { private var mRowsAdapter: ArrayObjectAdapter? = null private var favoriteObject: FavoriteObject? = null private var currentChapter: AnimeObject.WebInfo.AnimeChapter? = null private var chapters: MutableList? = ArrayList() private var actionAdapter: SparseArrayObjectAdapter? = null private var listRowAdapter: ArrayObjectAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) buildDetails() onItemViewClickedListener = this } private suspend fun getLastSeen(chapters: MutableList?): Int { if (chapters?.isNotEmpty() == true) { val eids = chapters.sortedBy { it.number.substringAfterLast(" ").toFloat() }.map { it.eid } eids.chunked(50).forEach { list -> val chapter = withContext(Dispatchers.IO) { CacheDB.INSTANCE.seenDAO().getLast(list) } if (chapter != null) { val position = chapters.indexOf(chapters.find { it.eid == chapter.eid }) if (position >= 0) return position } } } return 0 } private fun buildDetails() { val activity = activity ?: return Repository().getAnime( arguments?.getString("url") ?: "", true ).observe(activity) { animeObject -> if (animeObject != null) { Glide.with(App.context).asBitmap().load(PatternUtil.getCoverGlide(animeObject.aid)) .into(object : SimpleTarget() { override fun onResourceReady( resource: Bitmap, transition: Transition? ) { Palette.from(resource).generate { palette -> val swatch = palette?.darkMutedSwatch favoriteObject = FavoriteObject(animeObject) chapters = animeObject.chapters chapters?.reversed() val selector = ClassPresenterSelector() val rowPresenter = CustomFullWidthDetailsOverviewRowPresenter( if (swatch == null) DetailsDescriptionPresenter() else DetailsDescriptionPresenter( swatch.titleTextColor, swatch.bodyTextColor ) ) if (swatch != null) { rowPresenter.backgroundColor = swatch.rgb val hsv = FloatArray(3) val color = swatch.rgb Color.colorToHSV(color, hsv) hsv[2] *= 0.8f rowPresenter.actionsBackgroundColor = Color.HSVToColor(hsv) } selector.addClassPresenter( DetailsOverviewRow::class.java, rowPresenter ) lifecycleScope.launch { selector.addClassPresenter( ChaptersListRow::class.java, ChaptersListPresenter(getLastSeen(chapters)) ) selector.addClassPresenter( ListRow::class.java, ListRowPresenter() ) mRowsAdapter = ArrayObjectAdapter(selector) val detailsOverview = DetailsOverviewRow(animeObject) // Add images and action buttons to the details view detailsOverview.setImageBitmap(activity, resource) detailsOverview.isImageScaleUpAllowed = true actionAdapter = SparseArrayObjectAdapter() if (withContext(Dispatchers.IO) { CacheDB.INSTANCE.favsDAO().isFav(animeObject.key) }) { actionAdapter?.set( 1, Action( 1, "Quitar favorito", null, ContextCompat.getDrawable( App.context, R.drawable.heart_full ) ) ) } else { actionAdapter?.set( 1, Action( 1, "Añadir favorito", null, ContextCompat.getDrawable( App.context, R.drawable.heart_empty ) ) ) } actionAdapter?.apply { set( 2, Action( 2, "${animeObject.rate_stars}/5.0 (${animeObject.rate_count})", null, ContextCompat.getDrawable( App.context, R.drawable.ic_seeing ) ) ) detailsOverview.actionsAdapter = this } rowPresenter.onActionClickedListener = this@TVAnimesDetailsFragment mRowsAdapter?.add(detailsOverview) // Add a Chapters items row if (chapters?.isNotEmpty() == true) { chapters?.let { listRowAdapter = ArrayObjectAdapter( ChapterPresenter() ) for (chapter in it) listRowAdapter?.add(chapter) val header = HeaderItem(0, "Episodios") mRowsAdapter?.add( ChaptersListRow( header, listRowAdapter ?: ArrayObjectAdapter() ) ) } } // Add a Related items row if (animeObject.related?.isNotEmpty() == true) { val listRowAdapter = ArrayObjectAdapter( RelatedPresenter() ) for (related in animeObject.related ?: listOf()) listRowAdapter.add(related) val header = HeaderItem(0, "Relacionados") mRowsAdapter?.add(ListRow(header, listRowAdapter)) } noCrash { adapter = mRowsAdapter } } } } }) } } } override fun onItemClicked(itemViewHolder: Presenter.ViewHolder, item: Any, rowViewHolder: RowPresenter.ViewHolder, row: Row) { val activity = activity ?: return if (item is AnimeObject.WebInfo.AnimeRelated) { TVAnimesDetails.start(activity, "https://www3.animeflv.net" + item.link) } else if (item is AnimeObject.WebInfo.AnimeChapter) { currentChapter = item TVServersFactory.start(activity, item.link, item, itemViewHolder, activity as? TVServersFactory.ServersInterface) } } fun onStartStreaming() { currentChapter?.let { listRowAdapter?.notifyArrayItemRangeChanged(chapters?.indexOf(it)?:0, 1) } } override fun onActionClicked(action: Action) { if (action.id == 1L) { actionAdapter?.clear() favoriteObject?.let { lifecycleScope.launch(Dispatchers.IO) { if (CacheDB.INSTANCE.favsDAO().isFav(it.key)) { CacheDB.INSTANCE.favsDAO().deleteFav(it) launch(Dispatchers.Main){ action.label1 = "Añadir favorito" action.icon = ContextCompat.getDrawable(App.context, R.drawable.heart_empty) } } else { CacheDB.INSTANCE.favsDAO().addFav(it) launch(Dispatchers.Main){ action.label1 = "Quitar favorito" action.icon = ContextCompat.getDrawable(App.context, R.drawable.heart_full) } } syncData { favs() } } } actionAdapter?.set(1, action) } } companion object { operator fun get(url: String): TVAnimesDetailsFragment { val fragment = TVAnimesDetailsFragment() val bundle = Bundle() bundle.putString("url", url) fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/directory/DirAdvPresenter.kt ================================================ package knf.kuma.tv.directory import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.directory.DirObject import knf.kuma.tv.cards.DirAdvCardView class DirAdvPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(DirAdvCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as DirAdvCardView).bind(item as DirObject) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/directory/DirPresenter.kt ================================================ package knf.kuma.tv.directory import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.directory.DirObject import knf.kuma.tv.cards.DirCardView class DirPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(DirCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as DirCardView).bind(item as DirObject) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/directory/TVDir.kt ================================================ package knf.kuma.tv.directory import android.content.Context import android.content.Intent import android.os.Bundle import knf.kuma.tv.TVBaseActivity class TVDir : TVBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) addFragment(TVDirFragment()) } companion object { fun start(context: Context?) { context?.startActivity(Intent(context, TVDir::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/directory/TVDirFragment.kt ================================================ package knf.kuma.tv.directory import android.os.Bundle import androidx.leanback.app.VerticalGridSupportFragment import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.OnItemViewClickedListener import androidx.leanback.widget.Presenter import androidx.leanback.widget.Row import androidx.leanback.widget.RowPresenter import androidx.leanback.widget.VerticalGridPresenter import androidx.lifecycle.Observer import knf.kuma.commons.doOnUI import knf.kuma.database.CacheDB import knf.kuma.directory.DirObject import knf.kuma.tv.details.TVAnimesDetails import org.jetbrains.anko.doAsync class TVDirFragment : VerticalGridSupportFragment(), OnItemViewClickedListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) title = "Directorio" setGridPresenter( VerticalGridPresenter().apply { numberOfColumns = 4 } ) onItemViewClickedListener = this CacheDB.INSTANCE.animeDAO().allLive.observe(this, Observer { if (!it.isNullOrEmpty()) { doAsync { val arrayAdapter = ArrayObjectAdapter(DirAdvPresenter()).apply { addAll(0, it) } doOnUI { adapter = arrayAdapter } } } }) } override fun onItemClicked(itemViewHolder: Presenter.ViewHolder?, item: Any?, rowViewHolder: RowPresenter.ViewHolder?, row: Row?) { if (item is DirObject) context?.let { TVAnimesDetails.start(it, item.link) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/emission/TVEmission.kt ================================================ package knf.kuma.tv.emission import android.content.Context import android.content.Intent import android.os.Bundle import knf.kuma.tv.TVBaseActivity class TVEmission : TVBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) addFragment(TVEmissionFragment()) } companion object { fun start(context: Context?) { context?.startActivity(Intent(context, TVEmission::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/emission/TVEmissionFragment.kt ================================================ package knf.kuma.tv.emission import android.graphics.Color import android.os.Bundle import android.util.Log import android.util.SparseArray import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.ListRow import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.OnItemViewClickedListener import androidx.leanback.widget.Presenter import androidx.leanback.widget.Row import androidx.leanback.widget.RowPresenter import androidx.lifecycle.Observer import knf.kuma.commons.distinct import knf.kuma.database.CacheDB import knf.kuma.directory.DirObject import knf.kuma.pojos.AnimeObject import knf.kuma.tv.AnimeRow import knf.kuma.tv.details.TVAnimesDetails import knf.kuma.tv.directory.DirPresenter class TVEmissionFragment : BrowseSupportFragment(), OnItemViewClickedListener { private val mRows: SparseArray = SparseArray() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) headersState = HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true title = "Emisión" brandColor = Color.parseColor("#424242") createDataRows() prepareEntranceTransition() fetchData() } private fun createDataRows() { mRows.put(AnimeObject.Day.MONDAY.value, AnimeRow() .setId(AnimeObject.Day.MONDAY.value) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Lunes") .setPage(1)) mRows.put(AnimeObject.Day.TUESDAY.value, AnimeRow() .setId(AnimeObject.Day.TUESDAY.value) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Martes") .setPage(1)) mRows.put(AnimeObject.Day.WEDNESDAY.value, AnimeRow() .setId(AnimeObject.Day.WEDNESDAY.value) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Miercoles") .setPage(1)) mRows.put(AnimeObject.Day.THURSDAY.value, AnimeRow() .setId(AnimeObject.Day.THURSDAY.value) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Jueves") .setPage(1)) mRows.put(AnimeObject.Day.FRIDAY.value, AnimeRow() .setId(AnimeObject.Day.FRIDAY.value) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Viernes") .setPage(1)) mRows.put(AnimeObject.Day.SATURDAY.value, AnimeRow() .setId(AnimeObject.Day.SATURDAY.value) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Sabado") .setPage(1)) mRows.put(AnimeObject.Day.SUNDAY.value, AnimeRow() .setId(AnimeObject.Day.SUNDAY.value) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Domingo") .setPage(1)) createRows() } private fun createRows() { val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) for (i in AnimeObject.Day.values()) { Log.e("Emission", "Key: ${i.value}") val row = mRows.get(i.value) ?: continue rowsAdapter.add(ListRow(HeaderItem(row.id.toLong(), row.title), row.adapter)) } adapter = rowsAdapter onItemViewClickedListener = this } private fun fetchData() { CacheDB.INSTANCE.animeDAO().getByDayDir(AnimeObject.Day.MONDAY.value).distinct.observe(viewLifecycleOwner, Observer { mRows.get(AnimeObject.Day.MONDAY.value)?.apply { page = page.plus(1) setList(it) } startEntranceTransition() }) CacheDB.INSTANCE.animeDAO().getByDayDir(AnimeObject.Day.TUESDAY.value).distinct.observe(viewLifecycleOwner, Observer { mRows.get(AnimeObject.Day.TUESDAY.value)?.apply { page = page.plus(1) setList(it) } startEntranceTransition() }) CacheDB.INSTANCE.animeDAO().getByDayDir(AnimeObject.Day.WEDNESDAY.value).distinct.observe(viewLifecycleOwner, Observer { mRows.get(AnimeObject.Day.WEDNESDAY.value)?.apply { page = page.plus(1) setList(it) } startEntranceTransition() }) CacheDB.INSTANCE.animeDAO().getByDayDir(AnimeObject.Day.THURSDAY.value).distinct.observe(viewLifecycleOwner, Observer { mRows.get(AnimeObject.Day.THURSDAY.value)?.apply { page = page.plus(1) setList(it) } startEntranceTransition() }) CacheDB.INSTANCE.animeDAO().getByDayDir(AnimeObject.Day.FRIDAY.value).distinct.observe(viewLifecycleOwner, Observer { mRows.get(AnimeObject.Day.FRIDAY.value)?.apply { page = page.plus(1) setList(it) } startEntranceTransition() }) CacheDB.INSTANCE.animeDAO().getByDayDir(AnimeObject.Day.SATURDAY.value).distinct.observe(viewLifecycleOwner, Observer { mRows.get(AnimeObject.Day.SATURDAY.value)?.apply { page = page.plus(1) setList(it) } startEntranceTransition() }) CacheDB.INSTANCE.animeDAO().getByDayDir(AnimeObject.Day.SUNDAY.value).distinct.observe(viewLifecycleOwner, Observer { mRows.get(AnimeObject.Day.SUNDAY.value)?.apply { page = page.plus(1) setList(it) } startEntranceTransition() }) } override fun onItemClicked(itemViewHolder: Presenter.ViewHolder?, item: Any?, rowViewHolder: RowPresenter.ViewHolder?, row: Row?) { if (item is DirObject) { context?.let { TVAnimesDetails.start(it, item.link) } } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/exoplayer/LeanbackPlayerAdapter.kt ================================================ package knf.kuma.tv.exoplayer import android.content.Context import android.os.Handler import android.os.Looper import android.view.Surface import android.view.SurfaceHolder import androidx.leanback.media.PlaybackGlueHost import androidx.leanback.media.PlayerAdapter import androidx.leanback.media.SurfaceHolderGlueHost import com.google.android.exoplayer2.C import com.google.android.exoplayer2.ExoPlaybackException import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayerLibraryInfo import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.Timeline import com.google.android.exoplayer2.util.ErrorMessageProvider import com.google.android.exoplayer2.video.VideoSize import knf.kuma.commons.findActivity import xdroid.toaster.Toaster /** * Leanback `PlayerAdapter` implementation for [SimpleExoPlayer]. */ class LeanbackPlayerAdapter /** * Builds an instance. Note that the `PlayerAdapter` does not manage the lifecycle of the * [SimpleExoPlayer] instance. The caller remains responsible for releasing the exoPlayer when * it's no longer required. * * @param context The current context (activity). * @param player Instance of your exoplayer that needs to be configured. * @param updatePeriodMs The delay between exoPlayer control updates, in milliseconds. */ (private val context: Context, private val player: ExoPlayer, updatePeriodMs: Int) : PlayerAdapter() { private val handler: Handler = Handler(Looper.getMainLooper()) private val componentListener: ComponentListener private val updateProgressRunnable: Runnable //private var controlDispatcher: ControlDispatcher? = null private var errorMessageProvider: ErrorMessageProvider? = null private var surfaceHolderGlueHost: SurfaceHolderGlueHost? = null private var hasSurface: Boolean = false private var lastNotifiedPreparedState: Boolean = false init { componentListener = ComponentListener() //controlDispatcher = DefaultControlDispatcher() updateProgressRunnable = object : Runnable { override fun run() { callback?.apply { onCurrentPositionChanged(this@LeanbackPlayerAdapter) onBufferedPositionChanged(this@LeanbackPlayerAdapter) } handler.postDelayed(this, updatePeriodMs.toLong()) } } } // PlayerAdapter implementation. override fun onAttachedToHost(host: PlaybackGlueHost) { if (host is SurfaceHolderGlueHost) { surfaceHolderGlueHost = host surfaceHolderGlueHost?.setSurfaceHolderCallback(componentListener) } notifyStateChanged() player.addListener(componentListener) } override fun onDetachedFromHost() { player.removeListener(componentListener) surfaceHolderGlueHost?.setSurfaceHolderCallback(null) surfaceHolderGlueHost = null hasSurface = false val callback = callback callback?.apply { onBufferingStateChanged(this@LeanbackPlayerAdapter, false) onPlayStateChanged(this@LeanbackPlayerAdapter) maybeNotifyPreparedStateChanged(callback) } } override fun setProgressUpdatingEnabled(enabled: Boolean) { handler.removeCallbacks(updateProgressRunnable) if (enabled) { handler.post(updateProgressRunnable) } } override fun isPlaying(): Boolean { val playbackState = player.playbackState return (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED && player.playWhenReady) } override fun getDuration(): Long { val durationMs = player.duration return if (durationMs == C.TIME_UNSET) -1 else durationMs } override fun getCurrentPosition(): Long { return if (player.playbackState == Player.STATE_IDLE) -1 else player.currentPosition } override fun play() { if (player.playbackState == Player.STATE_ENDED) { player.seekTo(player.currentMediaItemIndex, C.TIME_UNSET) //controlDispatcher?.dispatchSeekTo(player, player.currentWindowIndex, C.TIME_UNSET) } player.play() /*if (player.playWhenReady) { callback.onPlayStateChanged(this) }*/ } override fun pause() { player.pause() /*if (controlDispatcher?.dispatchSetPlayWhenReady(player, false) == true) { callback.onPlayStateChanged(this) }*/ } override fun seekTo(positionMs: Long) { //controlDispatcher?.dispatchSeekTo(player, player.currentWindowIndex, positionMs) player.seekTo(player.currentMediaItemIndex, positionMs) } override fun getBufferedPosition(): Long { return player.bufferedPosition } override fun isPrepared(): Boolean { return player.playbackState != Player.STATE_IDLE && (surfaceHolderGlueHost == null || hasSurface) } // Internal methods. /* package */ internal fun setVideoSurface(surface: Surface?) { hasSurface = surface != null player.setVideoSurface(surface) maybeNotifyPreparedStateChanged(callback) } /* package */ internal fun notifyStateChanged() { val playbackState = player.playbackState val callback = callback maybeNotifyPreparedStateChanged(callback) callback?.apply { onPlayStateChanged(this@LeanbackPlayerAdapter) onBufferingStateChanged(this@LeanbackPlayerAdapter, playbackState == Player.STATE_BUFFERING) if (playbackState == Player.STATE_ENDED) { onPlayCompleted(this@LeanbackPlayerAdapter) } } } private fun maybeNotifyPreparedStateChanged(callback: Callback?) { val isPrepared = isPrepared if (lastNotifiedPreparedState != isPrepared) { lastNotifiedPreparedState = isPrepared callback?.onPreparedStateChanged(this) } } private inner class ComponentListener : Player.Listener, SurfaceHolder.Callback { // SurfaceHolder.Callback implementation. override fun surfaceCreated(surfaceHolder: SurfaceHolder) { setVideoSurface(surfaceHolder.surface) } override fun surfaceChanged( surfaceHolder: SurfaceHolder, format: Int, width: Int, height: Int ) { // Do nothing. } override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) { setVideoSurface(null) } // Player.EventListener implementation. override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { callback?.onPlayStateChanged(this@LeanbackPlayerAdapter) notifyStateChanged() } override fun onPlayerError(error: PlaybackException) { context.findActivity()?.finish() Toaster.toast("Error al reproducir") callback?.onError(this@LeanbackPlayerAdapter, error.errorCode, error.message) } override fun onTimelineChanged(timeline: Timeline, reason: Int) { callback?.apply { onDurationChanged(this@LeanbackPlayerAdapter) onCurrentPositionChanged(this@LeanbackPlayerAdapter) onBufferedPositionChanged(this@LeanbackPlayerAdapter) } } override fun onPositionDiscontinuity(oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int) { callback?.apply { onCurrentPositionChanged(this@LeanbackPlayerAdapter) onBufferedPositionChanged(this@LeanbackPlayerAdapter) } } // SimpleExoplayerView.Callback implementation. override fun onVideoSizeChanged(videoSize: VideoSize) { if (videoSize.width > 0 && videoSize.height > 0) { callback?.onVideoSizeChanged( this@LeanbackPlayerAdapter, videoSize.width, videoSize.height ) } } override fun onRenderedFirstFrame() { // Do nothing. } } companion object { init { ExoPlayerLibraryInfo.registerModule("goog.exo.leanback") } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/exoplayer/PlaybackFragment.kt ================================================ package knf.kuma.tv.exoplayer import android.annotation.TargetApi import android.content.Context import android.net.Uri import android.os.Build import android.os.Bundle import android.webkit.MimeTypeMap import androidx.leanback.app.VideoSupportFragment import androidx.leanback.app.VideoSupportFragmentGlueHost import androidx.lifecycle.lifecycleScope import com.google.android.exoplayer2.DefaultRenderersFactory import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.TrackSelector import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import com.google.android.exoplayer2.util.Util import knf.kuma.database.CacheDB import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class PlaybackFragment : VideoSupportFragment() { private var mPlayerGlue: VideoPlayerGlue? = null private var mPlayerAdapter: LeanbackPlayerAdapter? = null private var mPlayer: ExoPlayer? = null private var mTrackSelector: TrackSelector? = null private var mVideo: Video? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) activity?.intent?.let { mVideo = Video(it) } } override fun onStart() { super.onStart() if (Util.SDK_INT > 23) { initializePlayer() } } override fun onResume() { super.onResume() if (Util.SDK_INT <= 23 || mPlayer == null) { initializePlayer() } } /** * Pauses the exoPlayer. */ @TargetApi(Build.VERSION_CODES.N) override fun onPause() { super.onPause() mPlayerGlue?.save(mVideo) if (mPlayerGlue?.isPlaying == true) mPlayerGlue?.pause() if (Util.SDK_INT <= 23) { releasePlayer() } } override fun onStop() { super.onStop() if (Util.SDK_INT > 23) { releasePlayer() } } private fun initializePlayer() { val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory() mTrackSelector = DefaultTrackSelector(requireContext(), videoTrackSelectionFactory) mPlayer = ExoPlayer.Builder(requireContext(), DefaultRenderersFactory(requireContext())).build() mPlayerAdapter = LeanbackPlayerAdapter(activity as Context, mPlayer as ExoPlayer, UPDATE_DELAY) mPlayerGlue = VideoPlayerGlue(activity as Context, mPlayerAdapter as LeanbackPlayerAdapter) mPlayerGlue?.host = VideoSupportFragmentGlueHost(this) mPlayerGlue?.playWhenPrepared() play(mVideo) } private fun releasePlayer() { mPlayer?.release() mPlayer = null mTrackSelector = null mPlayerGlue = null mPlayerAdapter = null } private fun play(video: Video?) { lifecycleScope.launch { mPlayerGlue?.title = video?.title mPlayerGlue?.subtitle = video?.chapter prepareMediaForPlaying(video?.uri ?: Uri.EMPTY, video?.headers) val state = withContext(Dispatchers.IO) { CacheDB.INSTANCE.playerStateDAO().find("${video?.title}: ${video?.chapter}") } if (state != null) { mPlayerGlue?.seekTo(state.position) } mPlayerGlue?.play() } } private fun prepareMediaForPlaying(mediaSourceUri: Uri, headers: Map?) { activity?.let { val httpFactory = DefaultHttpDataSource.Factory().apply { if (headers?.containsKey("User-Agent") != true) { setUserAgent(Util.getUserAgent(it, "UKIKU")) } else { setUserAgent(headers["User-Agent"]) } setDefaultRequestProperties(headers ?: emptyMap()) } val mediaSource = when(MimeTypeMap.getFileExtensionFromUrl(mediaSourceUri.toString())) { "m3u8" -> HlsMediaSource.Factory(httpFactory) else -> ProgressiveMediaSource.Factory(httpFactory) }.createMediaSource(MediaItem.fromUri(mediaSourceUri)) mPlayer?.setMediaSource(mediaSource) mPlayer?.prepare() } } fun skipToNext() { mPlayerGlue?.next() } fun skipToPrevious() { mPlayerGlue?.previous() } fun rewind() { mPlayerGlue?.rewind() } fun fastForward() { mPlayerGlue?.fastForward() } companion object { private const val UPDATE_DELAY = 16 operator fun get(bundle: Bundle?): PlaybackFragment { val fragment = PlaybackFragment() fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/exoplayer/TVPlayer.kt ================================================ package knf.kuma.tv.exoplayer import android.os.Bundle import android.view.WindowManager import knf.kuma.tv.TVBaseActivity class TVPlayer : TVBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) addFragment(PlaybackFragment[intent.extras]) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/exoplayer/Video.kt ================================================ package knf.kuma.tv.exoplayer import android.content.Intent import android.net.Uri class Video(intent: Intent) { internal var uri: Uri = Uri.parse(intent.dataString) internal var title: String? = null internal var chapter: String? = null internal var headers: Map? = null init { this.title = intent.getStringExtra("title") this.chapter = intent.getStringExtra("chapter") this.headers = intent.getStringArrayListExtra("headers")?.chunked(2)?.associate { Pair(it[0], it[1]) }?.ifEmpty { null } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/exoplayer/VideoPlayerGlue.kt ================================================ package knf.kuma.tv.exoplayer import android.content.Context import androidx.leanback.media.PlaybackTransportControlGlue import androidx.leanback.widget.Action import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.PlaybackControlsRow import knf.kuma.database.CacheDB import knf.kuma.player.PlayerState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit /** * Manages customizing the actions in the [PlaybackControlsRow]. Adds and manages the * following actions to the primary and secondary controls: * * * * [androidx.leanback.widget.PlaybackControlsRow.RepeatAction] * * [androidx.leanback.widget.PlaybackControlsRow.ThumbsDownAction] * * [androidx.leanback.widget.PlaybackControlsRow.ThumbsUpAction] * * [androidx.leanback.widget.PlaybackControlsRow.SkipPreviousAction] * * [androidx.leanback.widget.PlaybackControlsRow.SkipNextAction] * * [androidx.leanback.widget.PlaybackControlsRow.FastForwardAction] * * [androidx.leanback.widget.PlaybackControlsRow.RewindAction] * * * * Note that the superclass, [PlaybackTransportControlGlue], manages the playback controls * row. */ class VideoPlayerGlue( context: Context, playerAdapter: LeanbackPlayerAdapter) : PlaybackTransportControlGlue(context, playerAdapter) { private val mRepeatAction: PlaybackControlsRow.RepeatAction private val mThumbsUpAction: PlaybackControlsRow.ThumbsUpAction = PlaybackControlsRow.ThumbsUpAction(context) private val mThumbsDownAction: PlaybackControlsRow.ThumbsDownAction private val mSkipPreviousAction: PlaybackControlsRow.SkipPreviousAction = PlaybackControlsRow.SkipPreviousAction(context) private val mSkipNextAction: PlaybackControlsRow.SkipNextAction = PlaybackControlsRow.SkipNextAction(context) private val mFastForwardAction: PlaybackControlsRow.FastForwardAction = PlaybackControlsRow.FastForwardAction(context) private val mRewindAction: PlaybackControlsRow.RewindAction = PlaybackControlsRow.RewindAction(context) init { mThumbsUpAction.index = PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE mThumbsDownAction = PlaybackControlsRow.ThumbsDownAction(context) mThumbsDownAction.index = PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE mRepeatAction = PlaybackControlsRow.RepeatAction(context) } override fun onCreatePrimaryActions(adapter: ArrayObjectAdapter) { // Order matters, super.onCreatePrimaryActions() will create the play / pause action. // Will display as follows: // play/pause, previous, rewind, fast forward, next // > /|| |< << >> >| //adapter.add(mSkipPreviousAction); adapter.add(mRewindAction) super.onCreatePrimaryActions(adapter) adapter.add(mFastForwardAction) //adapter.add(mSkipNextAction); } override fun onActionClicked(action: Action) { if (shouldDispatchAction(action)) { dispatchAction(action) return } // Super class handles play/pause and delegates to abstract methods next()/previous(). super.onActionClicked(action) } // Should dispatch actions that the super class does not supply callbacks for. private fun shouldDispatchAction(action: Action): Boolean { return (action === mRewindAction || action === mFastForwardAction || action === mThumbsDownAction || action === mThumbsUpAction || action === mRepeatAction) } private fun dispatchAction(action: Action) { // Primary actions are handled manually. when { action === mRewindAction -> rewind() action === mFastForwardAction -> fastForward() action is PlaybackControlsRow.MultiAction -> { action.nextIndex() // Notify adapter of action changes to handle secondary actions, such as, thumbs up/down // and repeat. controlsRow?.secondaryActionsAdapter?.apply { notifyActionChanged(action, this as ArrayObjectAdapter) } } } } fun save(video: Video?) { if (video != null) { val pos = currentPosition GlobalScope.launch(Dispatchers.IO) { CacheDB.INSTANCE.playerStateDAO().set(PlayerState("${video.title}: ${video.chapter}", pos)) } } } private fun notifyActionChanged( action: PlaybackControlsRow.MultiAction, adapter: ArrayObjectAdapter?) { if (adapter != null) { val index = adapter.indexOf(action) if (index >= 0) { adapter.notifyArrayItemRangeChanged(index, 1) } } } override fun next() {} override fun previous() {} /** * Skips backwards 10 seconds. */ fun rewind() { var newPosition = currentPosition - TEN_SECONDS newPosition = if (newPosition < 0) 0 else newPosition playerAdapter.seekTo(newPosition) } /** * Skips forward 10 seconds. */ fun fastForward() { if (duration > -1) { var newPosition = currentPosition + TEN_SECONDS newPosition = if (newPosition > duration) duration else newPosition playerAdapter.seekTo(newPosition) } } /** * Listens for when skip to next and previous actions have been dispatched. */ interface OnActionClickedListener { /** * Skip to the previous item in the queue. */ fun onPrevious() /** * Skip to the next item in the queue. */ fun onNext() } companion object { private val TEN_SECONDS = TimeUnit.SECONDS.toMillis(30) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/search/BasicAnimeObject.kt ================================================ package knf.kuma.tv.search data class BasicAnimeObject( var key: Int = 0, var aid: String = "", var name: String = "", var link: String = "" ) ================================================ FILE: app/src/main/java/knf/kuma/tv/search/TVSearch.kt ================================================ package knf.kuma.tv.search import android.content.Context import android.content.Intent import android.os.Bundle import knf.kuma.tv.TVBaseActivity class TVSearch : TVBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) addFragment(TVSearchFragment()) } companion object { fun start(context: Context) { context.startActivity(Intent(context, TVSearch::class.java)) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/search/TVSearchFragment.kt ================================================ package knf.kuma.tv.search import android.os.Bundle import androidx.leanback.app.SearchSupportFragment import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.ListRow import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.ObjectAdapter import androidx.leanback.widget.OnItemViewClickedListener import androidx.leanback.widget.Presenter import androidx.leanback.widget.Row import androidx.leanback.widget.RowPresenter import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import knf.kuma.database.CacheDB import knf.kuma.search.SearchFragment import knf.kuma.tv.anime.AnimePresenter import knf.kuma.tv.details.TVAnimesDetails class TVSearchFragment : SearchSupportFragment(), SearchSupportFragment.SearchResultProvider, OnItemViewClickedListener { private var arrayObjectAdapter: ArrayObjectAdapter? = null private lateinit var liveData: LiveData> private lateinit var observer: Observer> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arrayObjectAdapter = ArrayObjectAdapter(ListRowPresenter()) setSearchResultProvider(this) setOnItemViewClickedListener(this) val headerItem = HeaderItem("Géneros") val objectAdapter = ArrayObjectAdapter(TagPresenter()).also { it.addAll(0, SearchFragment.genres) } arrayObjectAdapter?.clear() arrayObjectAdapter?.add(ListRow(headerItem, objectAdapter)) setResult("") } override fun getResultsAdapter(): ObjectAdapter? { return arrayObjectAdapter } override fun onQueryTextChange(newQuery: String): Boolean { setResult(newQuery.trim()) return true } override fun onQueryTextSubmit(query: String): Boolean { setResult(query.trim()) return true } private fun setResult(query: String) { if (::liveData.isInitialized && ::observer.isInitialized) liveData.removeObserver(observer) activity?.let { liveData = CacheDB.INSTANCE.animeDAO().getSearchList("%$query%") observer = Observer { animeObjects -> liveData.removeObservers(it) if ((arrayObjectAdapter?.size() ?: 0) > 1) arrayObjectAdapter?.removeItems(1, 1) val objectAdapter = ArrayObjectAdapter(AnimePresenter()) for (animeObject in animeObjects) objectAdapter.add(animeObject) val headerItem = HeaderItem( when { query.isEmpty() -> "Todos los animes" animeObjects.isNotEmpty() -> "Resultados para '$query'" else -> "Sin resultados" } ) arrayObjectAdapter?.add(ListRow(headerItem, objectAdapter)) } liveData.observe(it, observer) } } override fun onItemClicked(itemViewHolder: Presenter.ViewHolder, item: Any, rowViewHolder: RowPresenter.ViewHolder, row: Row) { when (item) { is BasicAnimeObject -> context?.let { TVAnimesDetails.start(it, item.link) } is String -> context?.let { TVTag.start(it, item) } } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/search/TVTag.kt ================================================ package knf.kuma.tv.search import android.content.Context import android.content.Intent import android.os.Bundle import knf.kuma.tv.TVBaseActivity class TVTag : TVBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) addFragment(TVTagFragment().apply { arguments = Bundle().apply { putString(keyGenre, intent.getStringExtra(keyGenre)) } }) } companion object { private const val keyGenre = "genre" fun start(context: Context, genre: String) { context.startActivity(Intent(context, TVTag::class.java).putExtra(keyGenre, genre)) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/search/TVTagFragment.kt ================================================ package knf.kuma.tv.search import android.os.Bundle import androidx.leanback.app.VerticalGridSupportFragment import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.OnItemViewClickedListener import androidx.leanback.widget.Presenter import androidx.leanback.widget.Row import androidx.leanback.widget.RowPresenter import androidx.leanback.widget.VerticalGridPresenter import androidx.lifecycle.Observer import knf.kuma.commons.doOnUI import knf.kuma.database.CacheDB import knf.kuma.tv.anime.AnimePresenter import knf.kuma.tv.details.TVAnimesDetails import org.jetbrains.anko.doAsync class TVTagFragment : VerticalGridSupportFragment(), OnItemViewClickedListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) title = arguments?.getString("genre") setGridPresenter( VerticalGridPresenter().apply { numberOfColumns = 4 } ) onItemViewClickedListener = this CacheDB.INSTANCE.animeDAO().getAllGenreLive("%" + arguments?.getString("genre") + "%").observe(this, Observer { if (!it.isNullOrEmpty()) { doAsync { val arrayAdapter = ArrayObjectAdapter(AnimePresenter()).apply { addAll(0, it) } doOnUI { adapter = arrayAdapter } } } }) } override fun onItemClicked(itemViewHolder: Presenter.ViewHolder?, item: Any?, rowViewHolder: RowPresenter.ViewHolder?, row: Row?) { val anime = item as? BasicAnimeObject if (anime != null) context?.let { TVAnimesDetails.start(it, anime.link) } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/search/TagPresenter.kt ================================================ package knf.kuma.tv.search import android.view.ViewGroup import androidx.leanback.widget.Presenter import knf.kuma.tv.cards.TagCardView class TagPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { return ViewHolder(TagCardView(parent.context)) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) { if (item == null) return (viewHolder.view as TagCardView).bind(item as String) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { } } ================================================ FILE: app/src/main/java/knf/kuma/tv/sections/DirSection.kt ================================================ package knf.kuma.tv.sections import android.content.Context import android.content.Intent import knf.kuma.R import knf.kuma.tv.directory.TVDir class DirSection : SectionObject() { override val image: Int get() = R.drawable.ic_directory_not override val title: String get() = "Directorio" override fun open(context: Context?) { context?.startActivity(Intent(context, TVDir::class.java)) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/sections/EmissionSection.kt ================================================ package knf.kuma.tv.sections import android.content.Context import android.content.Intent import knf.kuma.R import knf.kuma.tv.emission.TVEmission class EmissionSection : SectionObject() { override val image: Int get() = R.drawable.ic_emision_not override val title: String get() = "Emisión" override fun open(context: Context?) { context?.startActivity(Intent(context, TVEmission::class.java)) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/sections/SectionObject.kt ================================================ package knf.kuma.tv.sections import android.content.Context abstract class SectionObject { abstract val image: Int abstract val title: String abstract fun open(context: Context?) } ================================================ FILE: app/src/main/java/knf/kuma/tv/streaming/StreamTvActivity.kt ================================================ package knf.kuma.tv.streaming import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle import knf.kuma.commons.EAHelper import knf.kuma.commons.doOnUI import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.DownloadObject import knf.kuma.tv.TVBaseActivity import knf.kuma.tv.TVServersFactory import org.jetbrains.anko.doAsync class StreamTvActivity : TVBaseActivity() { private lateinit var downloadObject: DownloadObject private var serversFactory: TVServersFactory? = null override fun onCreate(savedInstanceState: Bundle?) { setTheme(EAHelper.getThemeDialog()) super.onCreate(savedInstanceState) title = " " window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setFinishOnTouchOutside(false) doAsync { try { val data = intent.data if (data != null) { val chapter = AnimeObject.WebInfo.AnimeChapter .fromData( data.getQueryParameter("aid"), data.getQueryParameter("chapter"), data.getQueryParameter("eid"), data.getQueryParameter("url"), data.getQueryParameter("name") ) downloadObject = DownloadObject.fromChapter(chapter, false) doOnUI { try { TVServersFactory.start( this@StreamTvActivity, chapter.link, chapter, object : TVServersFactory.ServersInterface { override fun onReady(serversFactory: TVServersFactory) { this@StreamTvActivity.serversFactory = serversFactory } override fun onFinish(started: Boolean, success: Boolean) { finish() } }) } catch (e: Exception) { e.printStackTrace() finish() } } } else { finish() } } catch (e: Exception) { e.printStackTrace() finish() } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) try { if (data != null) if (resultCode == RESULT_OK) { val bundle = data.extras if (requestCode == TVServersFactory.REQUEST_CODE_MULTI) serversFactory?.analyzeMulti(bundle?.getInt("position", 0) ?: 0) else { if (bundle?.getBoolean("is_video_server", false) == true) serversFactory?.analyzeOption(bundle.getInt("position", 0)) else serversFactory?.analyzeServer(bundle?.getInt("position", 0) ?: 0) } } else if (resultCode == RESULT_CANCELED && data.extras?.getBoolean( "is_video_server", false ) == true ) serversFactory?.showServerList() } catch (e: Exception) { e.printStackTrace() } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/streaming/TVMultiSelection.kt ================================================ package knf.kuma.tv.streaming import android.os.Bundle import androidx.fragment.app.FragmentActivity import androidx.leanback.app.GuidedStepSupportFragment class TVMultiSelection : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) GuidedStepSupportFragment.addAsRoot(this,TVMultiSelectionFragment(),android.R.id.content) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/streaming/TVMultiSelectionFragment.kt ================================================ package knf.kuma.tv.streaming import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.leanback.app.GuidedStepSupportFragment import androidx.leanback.widget.GuidanceStylist import androidx.leanback.widget.GuidedAction class TVMultiSelectionFragment : GuidedStepSupportFragment(){ override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) activity?.setResult(Activity.RESULT_CANCELED, Intent()) } override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance { return GuidanceStylist.Guidance("Selecciona idioma","","",null) } override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) { actions.apply { add(GuidedAction.Builder(context).id(0).title("Subtitulado").build()) add(GuidedAction.Builder(context).id(1).title("Latino").build()) } } override fun onGuidedActionClicked(action: GuidedAction) { super.onGuidedActionClicked(action) activity?.setResult(Activity.RESULT_OK, Intent() .putExtra("position", action.id.toInt()) ) activity?.finish() } } ================================================ FILE: app/src/main/java/knf/kuma/tv/streaming/TVServerSelection.kt ================================================ package knf.kuma.tv.streaming import android.os.Bundle import androidx.fragment.app.FragmentActivity import androidx.leanback.app.GuidedStepSupportFragment class TVServerSelection : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) if (intent.hasExtra(TVServerSelectionFragment.SERVERS_DATA)) GuidedStepSupportFragment.addAsRoot(this, TVServerSelectionFragment[intent.getStringArrayListExtra(TVServerSelectionFragment.SERVERS_DATA) ?: ArrayList(), intent.getStringExtra("name"), false], android.R.id.content) else if (intent.hasExtra(TVServerSelectionFragment.VIDEO_DATA)) GuidedStepSupportFragment.addAsRoot(this, TVServerSelectionFragment[intent.getStringArrayListExtra(TVServerSelectionFragment.VIDEO_DATA) ?: ArrayList(), intent.getStringExtra("name"), true], android.R.id.content) } } ================================================ FILE: app/src/main/java/knf/kuma/tv/streaming/TVServerSelectionFragment.kt ================================================ package knf.kuma.tv.streaming import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.leanback.app.GuidedStepSupportFragment import androidx.leanback.widget.GuidanceStylist import androidx.leanback.widget.GuidedAction class TVServerSelectionFragment : GuidedStepSupportFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) activity?.setResult(Activity.RESULT_CANCELED, Intent() .putExtra(keyIsVideoServer, arguments?.getBoolean(IS_SERVER_DATA, false))) } override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance { return if (arguments?.getBoolean(IS_SERVER_DATA, false) == true) GuidanceStylist.Guidance(arguments?.getString(keyServerName) ?: "", "Selecciona calidad", "", null) else GuidanceStylist.Guidance("Selecciona servidor", "", "", null) } override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) { val list = arguments?.getStringArrayList(SERVERS_DATA) ?: arrayListOf() for ((id, name) in list.withIndex()) { if (!name.contains("mega", true)) actions.add(GuidedAction.Builder(context) .id(id.toLong()) .title(name) .build()) } } override fun onGuidedActionClicked(action: GuidedAction) { super.onGuidedActionClicked(action) activity?.setResult(Activity.RESULT_OK, Intent() .putExtra(keyIsVideoServer, arguments?.getBoolean(IS_SERVER_DATA, false)) .putExtra(keyPosition, action.id.toInt()) ) activity?.finish() } companion object { const val keyIsVideoServer = "is_video_server" const val keyPosition = "position" const val keyServerName = "server_name" const val VIDEO_DATA = "option_data" const val SERVERS_DATA = "list_data" const val IS_SERVER_DATA = "is_server" operator fun get(servers: ArrayList, name: String?, isServerData: Boolean): TVServerSelectionFragment { val fragment = TVServerSelectionFragment() val bundle = Bundle() bundle.putStringArrayList(SERVERS_DATA, servers) bundle.putBoolean(IS_SERVER_DATA, isServerData) if (name != null) bundle.putString(keyServerName, name) fragment.arguments = bundle return fragment } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/sync/BypassObject.kt ================================================ package knf.kuma.tv.sync import knf.kuma.R class BypassObject : SyncObject() { override val image: Int get() = R.drawable.banner_cloudflare override val title: String get() = "Recrear bypass" } ================================================ FILE: app/src/main/java/knf/kuma/tv/sync/LogOutObject.kt ================================================ package knf.kuma.tv.sync import knf.kuma.R class LogOutObject : SyncObject() { override val image: Int get() = R.drawable.banner_signout override val title: String get() = "Cerrar sesión" } ================================================ FILE: app/src/main/java/knf/kuma/tv/sync/SyncObject.kt ================================================ package knf.kuma.tv.sync import androidx.annotation.DrawableRes import knf.kuma.R import knf.kuma.backup.Backups open class SyncObject { var type = Backups.Type.NONE open val image: Int @DrawableRes get() = when (type) { Backups.Type.DROPBOX -> R.drawable.banner_dropbox Backups.Type.FIRESTORE -> R.drawable.banner_firestore else -> R.drawable.banner_drive } open val title: String get() = when (type) { Backups.Type.DROPBOX -> "Dropbox" Backups.Type.FIRESTORE -> "Firestore" else -> "Google Drive" } internal constructor() constructor(type: Backups.Type) { this.type = type } } ================================================ FILE: app/src/main/java/knf/kuma/tv/ui/TVMain.kt ================================================ package knf.kuma.tv.ui import android.content.Intent import android.os.Bundle import androidx.lifecycle.lifecycleScope import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.security.ProviderInstaller import com.google.firebase.crashlytics.FirebaseCrashlytics import knf.kuma.commons.BypassUtil import knf.kuma.commons.DesignUtils import knf.kuma.commons.PicassoSingle import knf.kuma.commons.PrefsUtil import knf.kuma.commons.doOnUI import knf.kuma.commons.isTV import knf.kuma.commons.toast import knf.kuma.custom.GenericActivity import knf.kuma.directory.DirManager import knf.kuma.directory.DirectoryService import knf.kuma.jobscheduler.DirUpdateWork import knf.kuma.jobscheduler.RecentsWork import knf.kuma.recents.RecentsNotReceiver import knf.kuma.retrofit.Repository import knf.kuma.tv.ChannelUtils import knf.kuma.tv.TVBaseActivity import knf.kuma.tv.TVServersFactory import knf.kuma.uagen.randomUA import knf.kuma.updater.UpdateActivity import knf.kuma.updater.UpdateChecker import knf.tools.bypass.startBypass import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class TVMain : TVBaseActivity(), TVServersFactory.ServersInterface, UpdateChecker.CheckListener { private var fragment: TVMainFragment? = null private var serversFactory: TVServersFactory? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //SSLManager.disable() if (!isTV) { finish() startActivity(Intent(this, DesignUtils.mainClass)) return } if (savedInstanceState == null) { fragment = TVMainFragment.get().also { addFragment(it) } DirUpdateWork.schedule(this) RecentsNotReceiver.removeAll(this) UpdateChecker.check(this, this) RecentsWork.schedule(this@TVMain) lifecycleScope.launch(Dispatchers.IO) { DirManager.checkPreDir() DirectoryService.run(this@TVMain) installSecurityProvider() } ChannelUtils.createIfNeeded(this) } } private suspend fun installSecurityProvider() { withContext(Dispatchers.IO) { try { ProviderInstaller.installIfNeeded(this@TVMain) PrefsUtil.isSecurityUpdated = true PrefsUtil.spErrorType = null } catch (e: GooglePlayServicesRepairableException) { PrefsUtil.isSecurityUpdated = false PrefsUtil.spErrorType = "Gplay services deshabilitado o desactualizado" e.printStackTrace() } catch (e: GooglePlayServicesNotAvailableException) { PrefsUtil.isSecurityUpdated = false PrefsUtil.spErrorType = "GPlay services no esta disponible" e.printStackTrace() } catch (e: Throwable) { PrefsUtil.isSecurityUpdated = false //Toaster.toastLong("SProvider: Unknown error, ${e.message}") PrefsUtil.spErrorType = "Error desconocido: ${e.message}" e.printStackTrace() } if (!PrefsUtil.isSecurityUpdated && FirebaseCrashlytics.getInstance().didCrashOnPreviousExecution()) { PrefsUtil.spProtectionEnabled = true //Toaster.toastLong("Proteccion de SP reactivada") } } } private fun doBlockTests(): Boolean { var blockCount = 0 repeat(3) { if (BypassUtil.isCloudflareActiveRandom()) blockCount++ if (blockCount >= 2) return true } return false } override fun onNeedUpdate(o_code: String, n_code: String) { runOnUiThread { UpdateActivity.start(this@TVMain, true, n_code) } } override fun onUpdateNotRequired() { lifecycleScope.launch(Dispatchers.Main) { if (PrefsUtil.mayUseRandomUA) PrefsUtil.alwaysGenerateUA = !withContext(Dispatchers.IO) { doBlockTests() } else PrefsUtil.alwaysGenerateUA = false if (withContext(Dispatchers.IO) { BypassUtil.isNeeded() }) { startBypass( 7425, BypassUtil.createRequest() ) //startBypass(this@TVMain, 7425, "https://www3.animeflv.net", true) } } } override fun onReady(serversFactory: TVServersFactory) { this.serversFactory = serversFactory } override fun onFinish(started: Boolean, success: Boolean) { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 7425) { val cookiesUpdated = data?.let { PrefsUtil.useDefaultUserAgent = false PrefsUtil.userAgent = it.getStringExtra("user_agent") ?: randomUA() BypassUtil.saveCookies(this, it.getStringExtra("cookies") ?: "null") } ?: false GenericActivity.bypassLive.postValue(Pair(first = cookiesUpdated, second = false)) Repository().reloadAllRecents() BypassUtil.isLoading = false PicassoSingle.clear() RecentsWork.run() doOnUI { "Bypass actualizado".toast() } ChannelUtils.initChannelIfNeeded(this) if (!PrefsUtil.isDirectoryFinished) { lifecycleScope.launch(Dispatchers.IO) { DirManager.checkPreDir() DirectoryService.run(this@TVMain) } } } else try { if (data != null) if (resultCode == RESULT_OK) { val bundle = data.extras if (requestCode == TVServersFactory.REQUEST_CODE_MULTI) serversFactory?.analyzeMulti(bundle?.getInt("position", 0) ?: 0) else { if (bundle?.getBoolean("is_video_server", false) == true) serversFactory?.analyzeOption(bundle.getInt("position", 0)) else serversFactory?.analyzeServer(bundle?.getInt("position", 0) ?: 0) } } else if (resultCode == RESULT_CANCELED && data.extras?.getBoolean( "is_video_server", false ) == true ) serversFactory?.showServerList() } catch (e: Exception) { e.printStackTrace() } } } ================================================ FILE: app/src/main/java/knf/kuma/tv/ui/TVMainFragment.kt ================================================ package knf.kuma.tv.ui import android.app.Activity import android.content.Intent import android.graphics.Color import android.os.Bundle import android.util.SparseArray import android.view.View import androidx.core.content.ContextCompat import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.ListRow import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.OnItemViewClickedListener import androidx.leanback.widget.Presenter import androidx.leanback.widget.Row import androidx.leanback.widget.RowPresenter import androidx.lifecycle.lifecycleScope import com.dropbox.core.android.Auth import knf.kuma.App import knf.kuma.Diagnostic import knf.kuma.R import knf.kuma.backup.Backups import knf.kuma.backup.firestore.FirestoreManager import knf.kuma.backup.framework.BackupService import knf.kuma.backup.framework.DropBoxService import knf.kuma.commons.distinct import knf.kuma.database.CacheDB import knf.kuma.directory.DirObject import knf.kuma.home.StaffRecommendations import knf.kuma.pojos.AnimeObject import knf.kuma.pojos.FavoriteObject import knf.kuma.pojos.RecentObject import knf.kuma.pojos.RecordObject import knf.kuma.retrofit.Repository import knf.kuma.tv.AnimeRow import knf.kuma.tv.GlideBackgroundManager import knf.kuma.tv.TVServersFactory import knf.kuma.tv.anime.FavPresenter import knf.kuma.tv.anime.RecentsPresenter import knf.kuma.tv.anime.RecordPresenter import knf.kuma.tv.anime.SectionPresenter import knf.kuma.tv.anime.SyncPresenter import knf.kuma.tv.details.TVAnimesDetails import knf.kuma.tv.directory.DirPresenter import knf.kuma.tv.search.TVSearch import knf.kuma.tv.sections.DirSection import knf.kuma.tv.sections.EmissionSection import knf.kuma.tv.sections.SectionObject import knf.kuma.tv.sync.BypassObject import knf.kuma.tv.sync.LogOutObject import knf.kuma.tv.sync.SyncObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import xdroid.toaster.Toaster class TVMainFragment : BrowseSupportFragment(), OnItemViewClickedListener, View.OnClickListener { private var mRows: SparseArray? = null private var backgroundManager: GlideBackgroundManager? = null private var service: BackupService? = null private var waitingLoginDropbox = false private var waitingLoginFirestore = false override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) backgroundManager = GlideBackgroundManager(activity as Activity) headersState = HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true title = "UKIKU" brandColor = Color.parseColor("#424242") searchAffordanceColor = ContextCompat.getColor(App.context, R.color.colorAccent) setOnSearchClickedListener(this) createDataRows() createRows() prepareEntranceTransition() fetchData() service = Backups.createService() onLogin() } private fun createDataRows() { mRows = SparseArray() mRows?.put(RECENTS, AnimeRow() .setId(RECENTS) .setAdapter(ArrayObjectAdapter(RecentsPresenter())) .setTitle("Recientes") .setPage(1)) mRows?.put(LAST_SEEN, AnimeRow() .setId(LAST_SEEN) .setAdapter(ArrayObjectAdapter(RecordPresenter())) .setTitle("Ultimos vistos") .setPage(1)) mRows?.put(FAVORITES, AnimeRow() .setId(FAVORITES) .setAdapter(ArrayObjectAdapter(FavPresenter())) .setTitle("Favoritos") .setPage(1)) mRows?.put(STAFF, AnimeRow() .setId(STAFF) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Recomendados") .setPage(1)) mRows?.put(BEST, AnimeRow() .setId(BEST) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Mejores en emisión") .setPage(1)) mRows?.put(BESTGLOBAL, AnimeRow() .setId(BESTGLOBAL) .setAdapter(ArrayObjectAdapter(DirPresenter())) .setTitle("Mejores en general") .setPage(1)) mRows?.put(SECTIONS, AnimeRow() .setId(SECTIONS) .setAdapter(ArrayObjectAdapter(SectionPresenter())) .setTitle("Secciones") .setPage(1)) } private fun createRows() { val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) for (i in 0 until (mRows?.size() ?: 0)) { val row = mRows?.get(i) val headerItem = HeaderItem(row?.id?.toLong() ?: 0, row?.title) val listRow = ListRow(headerItem, row?.adapter) rowsAdapter.add(listRow) } adapter = rowsAdapter onItemViewClickedListener = this } private fun fetchData() { Repository().reloadRecents() activity?.let { CacheDB.INSTANCE.recentsDAO().objects.distinct.observe(it) { recentObjects -> mRows?.get(RECENTS)?.apply { page = page.plus(1) adapter?.apply { clear() addAll(0, recentObjects) } } startEntranceTransition() } CacheDB.INSTANCE.recordsDAO().allLive.distinct.observe(it) { recordObjects -> mRows?.get(LAST_SEEN)?.apply { page = page.plus(1) adapter?.apply { clear() addAll(0, recordObjects) } } startEntranceTransition() } CacheDB.INSTANCE.favsDAO().all.distinct.observe(it) { favoriteObjects -> mRows?.get(FAVORITES)?.apply { page = page.plus(1) adapter?.apply { clear() addAll(0, favoriteObjects) } } startEntranceTransition() } viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { var recList = emptyList() while (recList.size < 15) { delay(100) recList = CacheDB.INSTANCE.animeDAO() .animesDirWithIDRandomNL(StaffRecommendations.randomIds(15)) } launch(Dispatchers.Main) { mRows?.get(STAFF)?.apply { page = page.plus(1) adapter?.apply { clear() addAll(0, recList) } } startEntranceTransition() } } CacheDB.INSTANCE.animeDAO().emissionVotesLimited.distinct.observe( it ) { emissionObjects -> mRows?.get(BEST)?.apply { page = page.plus(1) adapter?.apply { clear() addAll(0, emissionObjects) } } startEntranceTransition() } CacheDB.INSTANCE.animeDAO().allVotesLimited.distinct.observe(it) { emissionObjects -> mRows?.get(BESTGLOBAL)?.apply { page = page.plus(1) adapter?.apply { clear() addAll(0, emissionObjects) } } startEntranceTransition() } arrayListOf(DirSection(), EmissionSection()).let { sections -> mRows?.get(SECTIONS)?.apply { page = page.plus(1) adapter?.apply { clear() addAll(0, sections) } } startEntranceTransition() } } } override fun onClick(v: View) { context?.let { TVSearch.start(it) } } override fun onItemClicked(itemViewHolder: Presenter.ViewHolder?, item: Any?, rowViewHolder: RowPresenter.ViewHolder?, row: Row?) { if (item is RecentObject) { TVServersFactory.start(activity as Activity, item.url, AnimeObject.WebInfo.AnimeChapter.fromRecent(item), activity as TVServersFactory.ServersInterface) } else if (item is RecordObject) { if (item.animeObject != null) context?.let { TVAnimesDetails.start(it, item.animeObject.link) } else Toaster.toast("Anime no encontrado") } else if (item is FavoriteObject) { context?.let { TVAnimesDetails.start(it, item.link) } } else if (item is DirObject) { context?.let { TVAnimesDetails.start(it, item.link) } } else if (item is SectionObject) { item.open(context) } else if (item is SyncObject) { when (item) { is LogOutObject -> { service?.logOut() activity?.let { FirestoreManager.doSignOut(it) } Backups.type = Backups.Type.NONE onLogin() } is BypassObject -> startActivity(Intent(context, Diagnostic.FullBypass::class.java)) else -> { when (item.type) { Backups.Type.DROPBOX -> { waitingLoginDropbox = true if (item.type == Backups.Type.DROPBOX) service = DropBoxService().also { it.logIn() } } Backups.Type.FIRESTORE -> { waitingLoginFirestore = true activity?.let { FirestoreManager.doLogin(it) } } else -> { } } } } } } override fun onResume() { super.onResume() if (waitingLoginDropbox) { val token = Auth.getOAuth2Token() if (service is DropBoxService && service?.logIn(token) == true) { Backups.type = Backups.Type.DROPBOX } onLogin() } if (waitingLoginFirestore) { onLogin() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) activity?.let { FirestoreManager.handleLogin(it, requestCode, resultCode, data) } } private fun onLogin() { if (service?.isLoggedIn == false && waitingLoginDropbox) { Toaster.toast("Error al iniciar sesión") } else { val adapter = ArrayObjectAdapter(SyncPresenter()) val headerItem = HeaderItem(SYNC.toLong(), "Sincronización") if (service?.isLoggedIn == true || FirestoreManager.isLoggedIn) { adapter.add(LogOutObject()) Backups.restoreAll() } else { adapter.add(SyncObject(Backups.Type.DROPBOX)) adapter.add(SyncObject(Backups.Type.FIRESTORE)) } adapter.add(BypassObject()) if (getAdapter().size() == 7) (getAdapter() as ArrayObjectAdapter).add(SYNC, ListRow(headerItem, adapter)) else (getAdapter() as ArrayObjectAdapter).replace(SYNC, ListRow(headerItem, adapter)) } waitingLoginDropbox = false waitingLoginFirestore = false } companion object { private const val RECENTS = 0 private const val LAST_SEEN = 1 private const val FAVORITES = 2 private const val STAFF = 3 private const val BEST = 4 private const val BESTGLOBAL = 5 private const val SECTIONS = 6 private const val SYNC = 7 fun get(): TVMainFragment { return TVMainFragment() } } } ================================================ FILE: app/src/main/java/knf/kuma/updater/UpdateActivity.kt ================================================ package knf.kuma.updater import androidx.activity.addCallback import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.graphics.drawable.AnimationDrawable import android.net.Uri import android.os.Build import android.os.Bundle import android.view.View import android.view.WindowManager import android.view.animation.AnimationUtils import androidx.activity.viewModels import androidx.core.content.FileProvider import androidx.lifecycle.Observer import com.afollestad.materialdialogs.MaterialDialog import knf.kuma.R import knf.kuma.commons.getUpdateDir import knf.kuma.commons.safeShow import knf.kuma.custom.GenericActivity import knf.kuma.databinding.ActivityUpdaterBinding import knf.kuma.download.DownloadManagerCentral import java.io.File class UpdateActivity : GenericActivity() { private val binding by lazy { ActivityUpdaterBinding.inflate(layoutInflater) } private val updaterViewModel: UpdaterViewModel by viewModels() private val update: File by lazy { File.createTempFile("update", ".apk", filesDir) } private var isUpdateDownloaded = false @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) binding.download.setOnClickListener { install() } binding.retry.setOnClickListener { prepareForStart() start() } binding.progress.max = 100 val animationDrawable = binding.relBack.background as AnimationDrawable if (!animationDrawable.isRunning) { animationDrawable.setEnterFadeDuration(2500) animationDrawable.setExitFadeDuration(2500) animationDrawable.start() } onBackPressedDispatcher.addCallback(this) { if (intent.getBooleanExtra("canExit", true)) { isEnabled = false onBackPressedDispatcher.onBackPressed() isEnabled = true } } start() } private fun start() { updaterViewModel.start(update, "https://github.com/jordyamc/UKIKU/raw/master/app/$getUpdateDir/app-$getUpdateDir.apk") .observe(this, Observer { when (it.first) { UpdaterType.TYPE_IDLE -> { binding.progress.isIndeterminate = true } UpdaterType.TYPE_PROGRESS -> { setDownProgress(it.second as Int) } UpdaterType.TYPE_ERROR -> { prepareForRetry() onShowAlternativeDownload() } UpdaterType.TYPE_COMPLETED -> { isUpdateDownloaded = true binding.progress.progress = 100 binding.progressText.text = "100%" prepareForInstall() } } }) } override fun onStart() { super.onStart() if (!intent.getBooleanExtra("canExit", true)) { MaterialDialog(this).safeShow { message(text = "Parece que la versión ${intent.getStringExtra("version")} está disponible, es obligatoria") positiveButton(text = "ok") } } } private fun install() { DownloadManagerCentral.pauseAll() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val intent = Intent(Intent.ACTION_INSTALL_PACKAGE, FileProvider.getUriForFile(this, "${applicationContext.packageName}.fileprovider", update)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) .putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) .putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, packageName) startActivity(intent) } else { val intent = Intent(Intent.ACTION_VIEW) .setDataAndType(Uri.fromFile(update), "application/vnd.android.package-archive") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) startActivity(intent) } } private fun setDownProgress(p: Int) { try { binding.progress.apply { isIndeterminate = false progress = p } binding.progressText.text = String.format("%d%%", p) } catch (e: Exception) { e.printStackTrace() } } private fun prepareForInstall() { setDownProgress(100) val fadein = AnimationUtils.loadAnimation(this, R.anim.fadein) fadein.duration = 1000 val fadeout = AnimationUtils.loadAnimation(this, R.anim.fadeout) fadeout.duration = 1000 binding.progressText.post { with(binding.progressText) { visibility = View.INVISIBLE startAnimation(fadeout) } } binding.download.post { with(binding.download) { visibility = View.VISIBLE startAnimation(fadein) } } } private fun prepareForStart() { setDownProgress(100) val fadein = AnimationUtils.loadAnimation(this, R.anim.fadein) fadein.duration = 1000 val fadeout = AnimationUtils.loadAnimation(this, R.anim.fadeout) fadeout.duration = 1000 binding.retry.post { with(binding.retry) { visibility = View.INVISIBLE startAnimation(fadeout) } } binding.progressText.post { with(binding.progressText) { visibility = View.VISIBLE startAnimation(fadein) } } } private fun prepareForRetry() { setDownProgress(100) val fadein = AnimationUtils.loadAnimation(this, R.anim.fadein) fadein.duration = 1000 val fadeout = AnimationUtils.loadAnimation(this, R.anim.fadeout) fadeout.duration = 1000 binding.progressText.post { with(binding.progressText) { visibility = View.INVISIBLE startAnimation(fadeout) } } binding.retry.post { with(binding.retry) { visibility = View.VISIBLE startAnimation(fadein) } } } private fun onShowAlternativeDownload() { MaterialDialog(this).safeShow { title(text = "¿Error al actualizar?") message(text = "Puedes descargar la actualizacion desde la pagina web oficial!") positiveButton(text = "Descargar") { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://ukiku.app"))) } } } override fun onResume() { super.onResume() if (isUpdateDownloaded) { onShowAlternativeDownload() } } companion object { fun start(context: Context, canExit: Boolean, version: String) { context.startActivity(Intent(context, UpdateActivity::class.java).apply { putExtra("canExit", canExit) putExtra("version", version) }) } } } ================================================ FILE: app/src/main/java/knf/kuma/updater/UpdateChecker.kt ================================================ package knf.kuma.updater import android.util.Log import androidx.core.content.pm.PackageInfoCompat import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import knf.kuma.commons.Network import knf.kuma.commons.isFullMode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jsoup.Jsoup object UpdateChecker { fun check(context: FragmentActivity, listener: CheckListener) { if (Network.isConnected && isFullMode) context.lifecycleScope.launch(Dispatchers.IO) { try { val document = Jsoup.connect("https://raw.githubusercontent.com/jordyamc/UKIKU/master/version.num") .get() val nCode = Integer.parseInt(document.select("body").first().ownText().trim()) val oCode = PackageInfoCompat.getLongVersionCode( context.packageManager.getPackageInfo( context.packageName, 0 ) ).toInt() if (nCode > oCode) { delay(2000) listener.onNeedUpdate(oCode.toString(), nCode.toString()) } else { context.filesDir.listFiles() ?.filter { !it.isDirectory && it.name.startsWith("update") && it.name.endsWith( ".apk" ) } ?.forEach { it.delete() } Log.e("Version", "Up to date: $oCode") listener.onUpdateNotRequired() } } catch (e: Exception) { e.printStackTrace() listener.onUpdateNotRequired() } } else listener.onUpdateNotRequired() } interface CheckListener { fun onNeedUpdate(o_code: String, n_code: String) fun onUpdateNotRequired() } } ================================================ FILE: app/src/main/java/knf/kuma/updater/UpdaterViewModel.kt ================================================ package knf.kuma.updater import android.net.Uri import android.util.Log import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.crashlytics.FirebaseCrashlytics import com.thin.downloadmanager.DefaultRetryPolicy import com.thin.downloadmanager.DownloadRequest import com.thin.downloadmanager.DownloadStatusListenerV1 import com.thin.downloadmanager.ThinDownloadManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import xdroid.toaster.Toaster import java.io.File class UpdaterViewModel : ViewModel() { private val liveData = MutableLiveData>(UpdaterType.TYPE_IDLE to null) private val manager = ThinDownloadManager() private var isStarted = false fun start(file: File, link: String): LiveData> { if (isStarted) return liveData isStarted = true manager.add( DownloadRequest(link.toUri()) .setDestinationURI(Uri.fromFile(file)) .setDownloadResumable(false) .setRetryPolicy(DefaultRetryPolicy(5000, 3, 1f)) .setStatusListener(object : DownloadStatusListenerV1 { override fun onDownloadComplete(downloadRequest: DownloadRequest?) { viewModelScope.launch(Dispatchers.Main) { liveData.value = UpdaterType.TYPE_COMPLETED to null } } override fun onDownloadFailed(downloadRequest: DownloadRequest?, errorCode: Int, errorMessage: String?) { viewModelScope.launch(Dispatchers.Main) { liveData.value = UpdaterType.TYPE_ERROR to errorMessage } Log.e("Update Error", "Code: $errorCode Message: $errorMessage") Toaster.toast("Error al actualizar: $errorMessage") FirebaseCrashlytics.getInstance().recordException(IllegalStateException("Update failed\nCode: $errorCode Message: $errorMessage")) } override fun onProgress(downloadRequest: DownloadRequest?, totalBytes: Long, downloadedBytes: Long, progress: Int) { viewModelScope.launch(Dispatchers.Main) { liveData.value = UpdaterType.TYPE_PROGRESS to progress } } })) return liveData } override fun onCleared() { super.onCleared() manager.cancelAll() isStarted = false liveData.value = UpdaterType.TYPE_IDLE to null } } enum class UpdaterType { TYPE_ERROR, TYPE_PROGRESS, TYPE_COMPLETED, TYPE_IDLE } ================================================ FILE: app/src/main/java/knf/kuma/videoservers/FembedServer.kt ================================================ package knf.kuma.videoservers import android.content.Context import knf.kuma.commons.PatternUtil import knf.kuma.commons.iterator import knf.kuma.videoservers.VideoServer.Names.FEMBED import org.json.JSONObject import org.jsoup.Connection import org.jsoup.Jsoup class FembedServer internal constructor(context: Context, baseLink: String) : Server(context, baseLink) { override val isValid: Boolean get() = baseLink.contains("fembed") || baseLink.contains("embedsito.com") override val name: String get() = FEMBED override val videoServer: VideoServer? get() { return try { val downLink = PatternUtil.extractLink(baseLink) val fLink = if (downLink.contains("value=")) "https://embedsito.com/v/${downLink.substring(downLink.lastIndexOf("=") + 1)}" else downLink val json = JSONObject(Jsoup.connect(fLink.replace("/v/", "/api/source/")).ignoreContentType(true).method(Connection.Method.POST).execute().body()) check(json.getBoolean("success")) { "Request was not succeeded" } val array = json.getJSONArray("data") val options = mutableListOf